Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
561911056a | ||
|
|
835f64d47a | ||
|
|
fc40d84092 | ||
|
|
709f14708e | ||
|
|
5f4c4ab5e7 | ||
|
|
71205618ec | ||
|
|
5014ba864b | ||
|
|
e96205de35 | ||
|
|
273f31ad56 | ||
|
|
2093a00365 | ||
|
|
b1dad6b61b | ||
|
|
3a587f4e4b | ||
|
|
d9fc7bbb0e | ||
|
|
b0cfc45f83 | ||
|
|
450c80b9d5 | ||
|
|
d3eefb50f5 | ||
|
|
29d4955980 | ||
|
|
2412c08595 | ||
|
|
d769e5c296 | ||
|
|
d21af3f133 | ||
|
|
70672ed663 | ||
|
|
858484f250 | ||
|
|
83f9478977 | ||
|
|
b5b851ac7c | ||
|
|
e5175bf3e6 | ||
|
|
8c8f69accb | ||
|
|
f9037f831d | ||
|
|
9e0bd52c6a | ||
|
|
f1b017dc08 | ||
|
|
81dcbe294d | ||
|
|
e09a3d2e93 | ||
|
|
978a86bf12 | ||
|
|
0139a32213 | ||
|
|
d57158b59d | ||
|
|
4a7ce1c093 | ||
|
|
333677c40e | ||
|
|
b459b894bd | ||
|
|
c5a4860209 | ||
|
|
4f79bda7c9 | ||
|
|
7d22bb7d5a | ||
|
|
36822cc6ce | ||
|
|
e676f0b5b5 | ||
|
|
57de66a65f | ||
|
|
99022f2bac | ||
|
|
47b1e2837f | ||
|
|
d604ebaffd | ||
|
|
c1a0d3c662 | ||
|
|
16866a47a0 | ||
|
|
77ab0ad9a1 | ||
|
|
1c306dd243 | ||
|
|
404d52fe5c | ||
|
|
d9a50b6b08 | ||
|
|
e41d339f5f | ||
|
|
e374fe2118 | ||
|
|
9feadd3c6d | ||
|
|
3b5f71b0be | ||
|
|
b269415048 | ||
|
|
c9e421cff3 | ||
|
|
d9999c1ae4 | ||
|
|
31ba042121 | ||
|
|
2747effd9e | ||
|
|
6ed1e47a3d | ||
|
|
e8842cf8ae | ||
|
|
d426b78967 | ||
|
|
1d85f7a74e |
No files matched your search
@@ -18,6 +18,9 @@ exemptLabels:
|
||||
- 'Triage :white_flag:'
|
||||
- 'Extension: Discussion'
|
||||
- 'Announcement 🎉'
|
||||
- 'IDC:priority'
|
||||
- 'IDC:candidate'
|
||||
- 'IDC:collaboration'
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: 'Stale :baguette_bread:'
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
@@ -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>
|
||||
@@ -126,12 +126,39 @@ globally defined styles/scripts.
|
||||
> We're trying to embed the OHIF Viewer into an existing React App, but seeing
|
||||
> react-dom and react conflicts. What can we do?
|
||||
|
||||
If you are installing OHIF viewer inside another react app, you may use `installViewer` as follows:
|
||||
```
|
||||
import { installViewer } from '@ohif/viewer'
|
||||
|
||||
const ohifViewerConfig = window.config // or set it here
|
||||
const containerId = 'ohif'
|
||||
const componentRenderedOrUpdatedCallback = function() {
|
||||
console.log('OHIF Viewer rendered/updated');
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
installViewer(
|
||||
ohifViewerConfig,
|
||||
containerId,
|
||||
componentRenderedOrUpdatedCallback
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
...
|
||||
//you can render in any element you wish
|
||||
<AnyTag id={containerId}/>
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`installViewer` is a convenience method that pulls in some dependencies that may
|
||||
not be compatible with existing `react` apps. `@ohif/viewer` also exports `App`
|
||||
which is a react component that takes the `configuration` outlined above as
|
||||
props. You can use it as a reusable component, and to avoid `react` version
|
||||
conflict issues.
|
||||
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
@@ -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,11 +1,11 @@
|
||||
# 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
|
||||
- `sopClassUIDs`: an array of string SOP Class UIDs that the
|
||||
`getDisplaySetFromSeries` method should be applied to.
|
||||
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
|
||||
display set
|
||||
@@ -15,13 +15,13 @@ A `displaySet` has the following shape:
|
||||
```js
|
||||
return {
|
||||
plugin: 'html',
|
||||
modality: 'SR',
|
||||
displaySetInstanceUid: 0,
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
```
|
||||
@@ -50,7 +50,7 @@ export default {
|
||||
getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values(SOP_CLASS_UIDS),
|
||||
sopClassUIDs: Object.values(SOP_CLASS_UIDS),
|
||||
|
||||
/**
|
||||
* @param {object} series -
|
||||
@@ -63,12 +63,12 @@ getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0,
|
||||
displaySetInstanceUID: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,7 +11,59 @@ are:
|
||||
```js
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
/**
|
||||
* "White Labeling" is used to change the branding, look, and feel of the OHIF
|
||||
* Viewer. These settings, and the color variables that are used by our components,
|
||||
* are the easiest way to rebrand the application.
|
||||
*
|
||||
* More extensive changes are made possible through swapping out the UI library,
|
||||
* Viewer project, or extensions.
|
||||
*/
|
||||
whiteLabeling: {
|
||||
/* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
||||
createLogoComponentFn: function(React) {
|
||||
return React.createElement('a', {
|
||||
target: '_self',
|
||||
rel: 'noopener noreferrer',
|
||||
className: 'header-brand',
|
||||
href: '/',
|
||||
style: {
|
||||
display: 'block',
|
||||
textIndent: '-9999px',
|
||||
background: 'url(/svg-file-hosted-at-domain-root.svg)',
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
width: '200px',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Internally, the OHIF Viewer fetches data primarily with the
|
||||
* `cornerstoneWADOImageLoader` and the `DICOMWebClient`. If either of these
|
||||
* receive a non-200 response, this method allows you to handle that error.
|
||||
*
|
||||
* Common use cases include:
|
||||
* - Showing a notification with the UINotificationService
|
||||
* - Redirecting the user
|
||||
* - Refreshing an auth token
|
||||
*
|
||||
* @param {Object} error - JS new Error()
|
||||
* @param {XMLHttpRequest} error.request - The XHR request that's onreadystate change triggered this callback
|
||||
* @param {string} error.response - The XHR's response property
|
||||
* @param {number} error.status - The XHR's status property
|
||||
*/
|
||||
httpErrorHandler: error => {
|
||||
const { request: xhr, response, status } = err;
|
||||
const { responseType, statusText } = xhr;
|
||||
|
||||
// In local files, status is 0 upon success in Firefox
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
console.log(statusText, response, responseType);
|
||||
} else {
|
||||
console.warn('Likely CORS error');
|
||||
}
|
||||
},
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
|
||||
@@ -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).
|
||||
@@ -66,11 +66,11 @@ open to pull requests and discussion issues.
|
||||
|
||||
Current white-labeling options are limited. We expose the ability to replace the
|
||||
"Logo" section of the application with a custom "Logo" component. You can do
|
||||
this by adding a `whiteLabelling` key to your
|
||||
this by adding a `whiteLabeling` key to your
|
||||
[configuration file](./configuration.md).
|
||||
|
||||
```js
|
||||
function RadicalImagingLogo() {
|
||||
function RadicalImagingLogo(React) {
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
@@ -83,8 +83,8 @@ function RadicalImagingLogo() {
|
||||
);
|
||||
}
|
||||
|
||||
props.whiteLabelling = {
|
||||
logoComponent: RadicalImagingLogo(),
|
||||
props.whiteLabeling = {
|
||||
createLogoComponentFn: RadicalImagingLogo,
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
+17
-12
@@ -20,10 +20,10 @@ abbrev@~1.0.9:
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
|
||||
integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU=
|
||||
|
||||
agent-base@4, agent-base@^4.1.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
|
||||
integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
|
||||
agent-base@4, agent-base@^4.1.0, agent-base@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
|
||||
integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
|
||||
dependencies:
|
||||
es6-promisify "^5.0.0"
|
||||
|
||||
@@ -773,9 +773,9 @@ err-code@^1.0.0:
|
||||
prr "~1.0.1"
|
||||
|
||||
es6-promise@^4.0.3:
|
||||
version "4.2.6"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f"
|
||||
integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
es6-promisify@^5.0.0:
|
||||
version "5.0.0"
|
||||
@@ -1289,11 +1289,11 @@ http-signature@~1.2.0:
|
||||
sshpk "^1.7.0"
|
||||
|
||||
https-proxy-agent@^2.1.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
|
||||
integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
|
||||
integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==
|
||||
dependencies:
|
||||
agent-base "^4.1.0"
|
||||
agent-base "^4.3.0"
|
||||
debug "^3.1.0"
|
||||
|
||||
humanize-ms@^1.2.1:
|
||||
@@ -1807,11 +1807,16 @@ ms@2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
ms@^2.0.0, ms@^2.1.1:
|
||||
ms@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
mute-stream@~0.0.4:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Lesion Tracker - Audit Trail
|
||||
|
||||
1. To view Audit Trails, select **View Audit Log** on the Configuration menu.
|
||||
2. To filter audit logs for a column or multiple columns, type or select in the related column or columns.
|
||||
2. To filter audit logs for a column or multiple columns, type or select in the
|
||||
related column or columns.
|
||||
|
||||

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

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

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

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

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

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

|
||||
|
||||
### Anonymize (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Send (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Export (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Delete (Under Development)
|
||||
|
||||
...
|
||||
@@ -63,7 +63,7 @@ const commandsModule = {
|
||||
*/
|
||||
const sopClassHandlerModule = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values({
|
||||
sopClassUIDs: Object.values({
|
||||
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
|
||||
@@ -77,12 +77,12 @@ const sopClassHandlerModule = {
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0, //utils.guid(),
|
||||
displaySetInstanceUID: 0, //utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -3,6 +3,74 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.1...@ohif/extension-cornerstone@2.7.0) (2020-04-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.0...@ohif/extension-cornerstone@2.6.1) (2020-04-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Multiframe fix ([#1661](https://github.com/OHIF/Viewers/issues/1661)) ([7120561](https://github.com/OHIF/Viewers/commit/71205618ecb8b592247c5acb32284bfe7e18fce5))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.2...@ohif/extension-cornerstone@2.6.0) (2020-04-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.3...@ohif/extension-cornerstone@2.5.0) (2020-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.2...@ohif/extension-cornerstone@2.4.3) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.1...@ohif/extension-cornerstone@2.4.2) (2020-03-09)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.4.2",
|
||||
"version": "2.7.0",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -34,9 +34,9 @@
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.12.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.8.2",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "^0.12.2",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -51,7 +51,6 @@
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"query-string": "^6.8.3",
|
||||
"react-cornerstone-viewport": "^2.3.6"
|
||||
}
|
||||
}
|
||||
@@ -4,28 +4,9 @@ import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
// Metadata configuration
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
const SOP_CLASSES = {
|
||||
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
};
|
||||
|
||||
const specialCaseHandlers = {};
|
||||
specialCaseHandlers[
|
||||
SOP_CLASSES.SEGMENTATION_STORAGE
|
||||
] = handleSegmentationStorage;
|
||||
|
||||
class OHIFCornerstoneViewport extends Component {
|
||||
state = {
|
||||
viewportData: null,
|
||||
@@ -58,34 +39,34 @@ class OHIFCornerstoneViewport extends Component {
|
||||
* Obtain the CornerstoneTools Stack for the specified display set.
|
||||
*
|
||||
* @param {Object[]} studies
|
||||
* @param {String} studyInstanceUid
|
||||
* @param {String} displaySetInstanceUid
|
||||
* @param {String} [sopInstanceUid]
|
||||
* @param {String} StudyInstanceUID
|
||||
* @param {String} displaySetInstanceUID
|
||||
* @param {String} [SOPInstanceUID]
|
||||
* @param {Number} [frameIndex=1]
|
||||
* @return {Object} CornerstoneTools Stack
|
||||
*/
|
||||
static getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex = 0
|
||||
) {
|
||||
if (!studies || !studies.length) {
|
||||
throw new Error('Studies not provided.');
|
||||
}
|
||||
|
||||
if (!studyInstanceUid) {
|
||||
if (!StudyInstanceUID) {
|
||||
throw new Error('StudyInstanceUID not provided.');
|
||||
}
|
||||
|
||||
if (!displaySetInstanceUid) {
|
||||
if (!displaySetInstanceUID) {
|
||||
throw new Error('StudyInstanceUID not provided.');
|
||||
}
|
||||
|
||||
// Create shortcut to displaySet
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
if (!study) {
|
||||
@@ -93,7 +74,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
}
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
return set.displaySetInstanceUID === displaySetInstanceUID;
|
||||
});
|
||||
|
||||
if (!displaySet) {
|
||||
@@ -107,17 +88,14 @@ class OHIFCornerstoneViewport extends Component {
|
||||
const stack = Object.assign({}, storedStack);
|
||||
stack.currentImageIdIndex = frameIndex;
|
||||
|
||||
if (sopInstanceUid) {
|
||||
if (SOPInstanceUID) {
|
||||
const index = stack.imageIds.findIndex(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
'sopCommonModule',
|
||||
const imageIdSOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
return sopCommonModule.sopInstanceUID === sopInstanceUid;
|
||||
return imageIdSOPInstanceUID === SOPInstanceUID;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
@@ -134,44 +112,26 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
getViewportData = async (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) => {
|
||||
let viewportData;
|
||||
|
||||
switch (sopClassUid) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
const specialCaseHandler =
|
||||
specialCaseHandlers[SOP_CLASSES.SEGMENTATION_STORAGE];
|
||||
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
viewportData = await specialCaseHandler(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
break;
|
||||
default:
|
||||
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
viewportData = {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
stack,
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
viewportData = {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
stack,
|
||||
};
|
||||
|
||||
return viewportData;
|
||||
};
|
||||
@@ -179,31 +139,28 @@ class OHIFCornerstoneViewport extends Component {
|
||||
setStateFromProps() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUids,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUIDs,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
} = displaySet;
|
||||
|
||||
if (!studyInstanceUid || !displaySetInstanceUid) {
|
||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sopClassUids && sopClassUids.length > 1) {
|
||||
if (sopClassUIDs && sopClassUIDs.length > 1) {
|
||||
console.warn(
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
'More than one SOPClassUID in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
const sopClassUid = sopClassUids && sopClassUids[0];
|
||||
|
||||
this.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
).then(viewportData => {
|
||||
this.setState({
|
||||
@@ -217,13 +174,13 @@ class OHIFCornerstoneViewport extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUid !==
|
||||
prevDisplaySet.displaySetInstanceUid ||
|
||||
displaySet.sopInstanceUid !== prevDisplaySet.sopInstanceUid ||
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
this.setStateFromProps();
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* We use this component to leverage "Code Splitting"
|
||||
*
|
||||
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export default function asyncComponent(importComponent) {
|
||||
class AsyncComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
component: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Add dynamically loaded component to state
|
||||
const { default: component } = await importComponent();
|
||||
|
||||
this.setState({
|
||||
component: component,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const C = this.state.component;
|
||||
|
||||
// Render the loaded component, or null
|
||||
return C ? <C {...this.props} /> : null;
|
||||
}
|
||||
}
|
||||
|
||||
return AsyncComponent;
|
||||
}
|
||||
@@ -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 }) => {
|
||||
@@ -247,6 +250,7 @@ const commandsModule = ({ servicesManager }) => {
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
|
||||
viewport.voi = {
|
||||
windowWidth: Number(window),
|
||||
windowCenter: Number(level),
|
||||
@@ -254,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 && 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: [],
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
|
||||
import OHIF from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
const { StackManager, DicomLoaderService } = OHIF.utils;
|
||||
|
||||
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
});
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
function getDisplaySetsBySeries(studies, studyInstanceUid, seriesInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
return study.displaySets.filter(set => {
|
||||
return set.seriesInstanceUid === seriesInstanceUid;
|
||||
});
|
||||
}
|
||||
|
||||
function parseSeg(arrayBuffer, imageIds) {
|
||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
cornerstone.metaData
|
||||
);
|
||||
}
|
||||
|
||||
function addSegMetadataToCornerstoneToolState(
|
||||
segMetadata,
|
||||
toolState,
|
||||
displaySetInstanceUid
|
||||
) {
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
|
||||
toolState
|
||||
);
|
||||
|
||||
const brushModule = cornerstoneTools.store.modules.brush;
|
||||
|
||||
for (let i = 0; i < segMetadata.length; i++) {
|
||||
brushModule.setters.metadata(displaySetInstanceUid, i, segMetadata[i]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSegmentationStorage(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
const arrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||
displaySet,
|
||||
studies
|
||||
);
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
const seriesInstanceUid = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
|
||||
const displaySets = getDisplaySetsBySeries(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid
|
||||
);
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same seriesInstanceUid. This is not supported yet...'
|
||||
);
|
||||
}
|
||||
|
||||
const referenceDisplaySet = displaySets[0];
|
||||
const imageIds = referenceDisplaySet.images.map(image => image.getImageId());
|
||||
const results = parseSeg(arrayBuffer, imageIds);
|
||||
|
||||
if (!results) {
|
||||
throw new Error('Fractional segmentations are not supported');
|
||||
}
|
||||
|
||||
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
|
||||
const { setters } = cornerstoneTools.getModule('segmentation');
|
||||
|
||||
setters.labelmap3DByFirstImageId(
|
||||
imageIds[0],
|
||||
labelmapBuffer,
|
||||
0, // TODO -> Can define a color LUT based on colors in the SEG later.
|
||||
segMetadata,
|
||||
imageIds.length,
|
||||
segmentsOnFrame
|
||||
);
|
||||
|
||||
const cachedStack = StackManager.findOrCreateStack(
|
||||
study,
|
||||
referenceDisplaySet
|
||||
);
|
||||
const stack = Object.assign({}, cachedStack);
|
||||
|
||||
stack.currentImageIdIndex = 0;
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
stack,
|
||||
};
|
||||
}
|
||||
|
||||
export default handleSegmentationStorage;
|
||||
@@ -1,14 +1,20 @@
|
||||
import React from 'react';
|
||||
import init from './init.js';
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
|
||||
const OHIFCornerstoneViewport = asyncComponent(() =>
|
||||
import(
|
||||
/* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'
|
||||
)
|
||||
);
|
||||
const Component = React.lazy(() => {
|
||||
return import('./OHIFCornerstoneViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -3,29 +3,9 @@ import { SimpleDialog } from '@ohif/ui';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
import merge from 'lodash.merge';
|
||||
import queryString from 'query-string';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If you call for an WADO-URI imageId and get no
|
||||
// metadata, try reformatting to WADO-RS imageId
|
||||
const qs = queryString.parse(imageId);
|
||||
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
|
||||
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
|
||||
qs.seriesUID
|
||||
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||
|
||||
return cornerstone.metaData.get(type, wadoRsImageId);
|
||||
}
|
||||
|
||||
// Add this fallback provider with a low priority so it is handled last
|
||||
cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} servicesManager
|
||||
@@ -58,16 +38,13 @@ export default function init({ servicesManager, configuration }) {
|
||||
};
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
const { StackManager } = OHIF.utils;
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
||||
|
||||
// ~~ Set our MetadataProvider
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
metadataProvider.get.bind(metadataProvider),
|
||||
9999
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
// ~~
|
||||
const defaultCsToolsConfig = csToolsConfig || {
|
||||
globalToolSyncEnabled: true,
|
||||
@@ -89,7 +66,6 @@ export default function init({ servicesManager, configuration }) {
|
||||
csTools.DragProbeTool,
|
||||
csTools.RectangleRoiTool,
|
||||
],
|
||||
segmentation: [csTools.BrushTool],
|
||||
other: [
|
||||
csTools.PanTool,
|
||||
csTools.ZoomTool,
|
||||
|
||||
@@ -2,6 +2,9 @@ import Hammer from 'hammerjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneMath from 'cornerstone-math';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
const { log } = OHIF;
|
||||
|
||||
export default function(configuration = {}) {
|
||||
// For debugging
|
||||
@@ -12,6 +15,13 @@ export default function(configuration = {}) {
|
||||
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
|
||||
cornerstoneTools.init(configuration);
|
||||
|
||||
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(
|
||||
(element, imageId, error) => {
|
||||
log.error(imageId);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
// Set the tool font and font size
|
||||
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
|
||||
const fontFamily =
|
||||
|
||||
+27
-19
@@ -1,6 +1,11 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
const SUPPORTED_TOOLS = ['Length', 'EllipticalRoi', 'RectangleRoi', 'ArrowAnnotate'];
|
||||
const SUPPORTED_TOOLS = [
|
||||
'Length',
|
||||
'EllipticalRoi',
|
||||
'RectangleRoi',
|
||||
'ArrowAnnotate',
|
||||
];
|
||||
|
||||
const measurementServiceMappingsFactory = measurementService => {
|
||||
/**
|
||||
@@ -17,17 +22,17 @@ const measurementServiceMappingsFactory = measurementService => {
|
||||
description,
|
||||
points,
|
||||
unit,
|
||||
sopInstanceUID,
|
||||
frameOfReferenceUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID,
|
||||
} = measurement;
|
||||
|
||||
return {
|
||||
toolName: definition,
|
||||
measurementData: {
|
||||
sopInstanceUid: sopInstanceUID,
|
||||
frameOfReferenceUid: frameOfReferenceUID,
|
||||
seriesInstanceUid: referenceSeriesUID,
|
||||
sopInstanceUid: SOPInstanceUID,
|
||||
frameOfReferenceUID: FrameOfReferenceUID,
|
||||
SeriesInstanceUID: referenceSeriesUID,
|
||||
unit,
|
||||
text: label,
|
||||
description,
|
||||
@@ -57,9 +62,9 @@ const measurementServiceMappingsFactory = measurementService => {
|
||||
}
|
||||
|
||||
const {
|
||||
sopInstanceUid,
|
||||
frameOfReferenceUid,
|
||||
seriesInstanceUid,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
} = _getAttributes(element);
|
||||
|
||||
const points = [];
|
||||
@@ -67,13 +72,16 @@ const measurementServiceMappingsFactory = measurementService => {
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
sopInstanceUID: sopInstanceUid,
|
||||
frameOfReferenceUID: frameOfReferenceUid,
|
||||
referenceSeriesUID: seriesInstanceUid,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
label: measurementData.text,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
area: measurementData.cachedStats && measurementData.cachedStats.area, /* TODO: Add concept names instead (descriptor) */
|
||||
area:
|
||||
measurementData.cachedStats &&
|
||||
measurementData.cachedStats
|
||||
.area /* TODO: Add concept names instead (descriptor) */,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points: _getPointsFromHandles(measurementData.handles),
|
||||
};
|
||||
@@ -82,13 +90,13 @@ const measurementServiceMappingsFactory = measurementService => {
|
||||
const _getAttributes = element => {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const imageId = enabledElement.image.imageId;
|
||||
const sopInstance = cornerstone.metaData.get('instance', imageId);
|
||||
const sopInstanceUid = sopInstance.sopInstanceUid;
|
||||
const frameOfReferenceUid = sopInstance.frameOfReferenceUID;
|
||||
const series = cornerstone.metaData.get('series', imageId);
|
||||
const seriesInstanceUid = series.seriesInstanceUid;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
|
||||
return { sopInstanceUid, frameOfReferenceUid, seriesInstanceUid };
|
||||
return {
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
};
|
||||
};
|
||||
|
||||
const _getValueTypeFromToolType = toolType => {
|
||||
|
||||
+11
-8
@@ -8,9 +8,9 @@ jest.mock('cornerstone-core', () => ({
|
||||
metaData: {
|
||||
...jest.requireActual('cornerstone-core').metaData,
|
||||
get: () => ({
|
||||
sopInstanceUid: "123",
|
||||
frameOfReferenceUID: "123",
|
||||
seriesInstanceUid: "123",
|
||||
SOPInstanceUID: '123',
|
||||
FrameOfReferenceUID: '123',
|
||||
SeriesInstanceUID: '123',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
@@ -42,8 +42,8 @@ describe('measurementServiceMappings.js', () => {
|
||||
measurementData: {
|
||||
_measurementServiceId: 1,
|
||||
sopInstanceUid: '123',
|
||||
frameOfReferenceUid: '123',
|
||||
seriesInstanceUid: '123',
|
||||
frameOfReferenceUID: '123',
|
||||
SeriesInstanceUID: '123',
|
||||
handles,
|
||||
text: 'Test',
|
||||
description: 'Test',
|
||||
@@ -52,8 +52,8 @@ describe('measurementServiceMappings.js', () => {
|
||||
};
|
||||
measurement = {
|
||||
id: 1,
|
||||
sopInstanceUID: '123',
|
||||
frameOfReferenceUID: '123',
|
||||
SOPInstanceUID: '123',
|
||||
FrameOfReferenceUID: '123',
|
||||
referenceSeriesUID: '123',
|
||||
label: 'Test',
|
||||
description: 'Test',
|
||||
@@ -66,7 +66,10 @@ describe('measurementServiceMappings.js', () => {
|
||||
|
||||
describe('toAnnotation()', () => {
|
||||
it('map measurement service format to annotation', async () => {
|
||||
const mappedMeasurement = await mappings.toAnnotation(measurement, definition);
|
||||
const mappedMeasurement = await mappings.toAnnotation(
|
||||
measurement,
|
||||
definition
|
||||
);
|
||||
expect(mappedMeasurement).toEqual(csToolsAnnotation);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.2...@ohif/extension-dicom-html@1.2.0) (2020-04-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.1...@ohif/extension-dicom-html@1.1.2) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.0...@ohif/extension-dicom-html@1.1.1) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "OHIF extension for rendering structured reports to HTML",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dcmjs": "^0.8.2",
|
||||
"dcmjs": "^0.12.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -11,26 +11,26 @@ const SOP_CLASS_UIDS = {
|
||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
||||
};
|
||||
|
||||
const sopClassUids = Object.values(SOP_CLASS_UIDS);
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
||||
// same SOP Class
|
||||
const OHIFDicomHtmlSopClassHandler = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUids,
|
||||
sopClassUIDs,
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
modality: 'SR',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* We use this component to leverage "Code Splitting"
|
||||
*
|
||||
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export default function asyncComponent(importComponent) {
|
||||
class AsyncComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
component: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Add dynamically loaded component to state
|
||||
const { default: component } = await importComponent();
|
||||
|
||||
this.setState({
|
||||
component: component,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const C = this.state.component;
|
||||
|
||||
// Render the loaded component, or null
|
||||
return C ? <C {...this.props} /> : null;
|
||||
}
|
||||
}
|
||||
|
||||
return AsyncComponent;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import React from 'react';
|
||||
import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js';
|
||||
|
||||
const OHIFDicomHtmlViewport = asyncComponent(() =>
|
||||
import(
|
||||
/* webpackChunkName: "OHIFDicomHtmlViewport" */ './OHIFDicomHtmlViewport.js'
|
||||
)
|
||||
);
|
||||
const Component = React.lazy(() => {
|
||||
return import('./OHIFDicomHtmlViewport');
|
||||
});
|
||||
|
||||
const OHIFDicomHtmlViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.8...@ohif/extension-dicom-microscopy@0.50.9) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.7...@ohif/extension-dicom-microscopy@0.50.8) (2020-03-31)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.6...@ohif/extension-dicom-microscopy@0.50.7) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.5...@ohif/extension-dicom-microscopy@0.50.6) (2019-09-26)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "0.50.6",
|
||||
"version": "0.50.9",
|
||||
"description": "OHIF extension for Dicom Microscopy",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -8,7 +8,7 @@ const SOP_CLASS_UIDS = {
|
||||
|
||||
const DicomMicroscopySopClassHandler = {
|
||||
id: 'DicomMicroscopySopClassHandlerPlugin',
|
||||
sopClassUids: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
|
||||
sopClassUIDs: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
@@ -16,12 +16,12 @@ const DicomMicroscopySopClassHandler = {
|
||||
|
||||
return {
|
||||
plugin: 'microscopy',
|
||||
modality: 'SM',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'SM',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
dicomWebClient,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,8 +25,8 @@ class DicomMicroscopyViewport extends Component {
|
||||
const dicomWebClient = displaySet.dicomWebClient;
|
||||
|
||||
const searchInstanceOptions = {
|
||||
studyInstanceUID: displaySet.studyInstanceUid,
|
||||
seriesInstanceUID: displaySet.seriesInstanceUid,
|
||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||
seriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
};
|
||||
|
||||
dicomWebClient
|
||||
@@ -35,17 +35,18 @@ class DicomMicroscopyViewport extends Component {
|
||||
const promises = [];
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
const sopInstanceUID = instances[i]['00080018']['Value'][0];
|
||||
|
||||
const retrieveInstanceOptions = {
|
||||
studyInstanceUID: displaySet.studyInstanceUid,
|
||||
seriesInstanceUID: displaySet.seriesInstanceUid,
|
||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||
seriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
};
|
||||
|
||||
const promise = dicomWebClient
|
||||
.retrieveInstanceMetadata(retrieveInstanceOptions)
|
||||
.then(metadata => {
|
||||
const imageType = metadata[0]['00080008']['Value'];
|
||||
if (imageType[2] === 'VOLUME') {
|
||||
const ImageType = metadata[0]['00080008']['Value'];
|
||||
if (ImageType[2] === 'VOLUME') {
|
||||
return metadata[0];
|
||||
}
|
||||
});
|
||||
@@ -56,13 +57,15 @@ class DicomMicroscopyViewport extends Component {
|
||||
.then(async metadata => {
|
||||
metadata = metadata.filter(m => m);
|
||||
|
||||
const { api } = await import(/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer');
|
||||
const { api } = await import(
|
||||
/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer'
|
||||
);
|
||||
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
|
||||
|
||||
this.viewer = new microscopyViewer({
|
||||
client: dicomWebClient,
|
||||
metadata,
|
||||
retrieveRendered: false
|
||||
retrieveRendered: false,
|
||||
});
|
||||
|
||||
this.viewer.render({ container });
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* We use this component to leverage "Code Splitting"
|
||||
*
|
||||
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export default function asyncComponent(importComponent) {
|
||||
class AsyncComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
component: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Add dynamically loaded component to state
|
||||
const { default: component } = await importComponent();
|
||||
|
||||
this.setState({
|
||||
component: component,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const C = this.state.component;
|
||||
|
||||
// Render the loaded component, or null
|
||||
return C ? <C {...this.props} /> : null;
|
||||
}
|
||||
}
|
||||
|
||||
return AsyncComponent;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import React from 'react';
|
||||
import DicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler.js';
|
||||
|
||||
const DicomMicroscopyViewport = asyncComponent(() =>
|
||||
import(
|
||||
/* webpackChunkName: "DicomMicroscopyViewport" */ './DicomMicroscopyViewport.js'
|
||||
)
|
||||
);
|
||||
const Component = React.lazy(() => {
|
||||
return import('./DicomMicroscopyViewport');
|
||||
});
|
||||
|
||||
const DicomMicroscopyViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const merge = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'OHIFExtDicomP10Downloader',
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
filename: pkg.main,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-p10-downloader@0.0.2...@ohif/extension-dicom-p10-downloader@0.1.0) (2020-04-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 0.0.2 (2020-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-p10-downloader
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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 +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-p10-downloader
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-p10-downloader",
|
||||
"version": "0.1.0",
|
||||
"description": "OHIF extension for downloading DICOM P10 files",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||
"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": "^2.6.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"jszip": "^3.2.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import {
|
||||
save,
|
||||
getDicomWebClientFromContext,
|
||||
getSOPInstanceReferenceFromActiveViewport,
|
||||
getSOPInstanceReferencesFromViewports,
|
||||
} from './utils';
|
||||
import _downloadAndZip from './downloadAndZip';
|
||||
|
||||
const {
|
||||
utils: {
|
||||
Queue,
|
||||
},
|
||||
} = OHIF;
|
||||
|
||||
export function getCommands(context) {
|
||||
const queue = new Queue(1);
|
||||
const actions = {
|
||||
/**
|
||||
* @example Running this command using Commands Manager
|
||||
* commandsManager.runCommand(
|
||||
* 'downloadAndZip',
|
||||
* {
|
||||
* listOfUIDs: [...],
|
||||
* options: {
|
||||
* progress(status) {
|
||||
* console.info('Progress:', (status.progress * 100).toFixed(2) + '%');
|
||||
* }
|
||||
* }
|
||||
* },
|
||||
* 'VIEWER'
|
||||
* );
|
||||
*/
|
||||
downloadAndZip({ servers, dicomWebClient, listOfUIDs, options }) {
|
||||
return save(
|
||||
_downloadAndZip(
|
||||
dicomWebClient || getDicomWebClientFromContext(context, servers),
|
||||
listOfUIDs,
|
||||
options
|
||||
),
|
||||
listOfUIDs
|
||||
);
|
||||
},
|
||||
downloadAndZipSeriesOnViewports({ servers, viewports, progress }) {
|
||||
const dicomWebClient = getDicomWebClientFromContext(context, servers);
|
||||
const listOfUIDs = getSOPInstanceReferencesFromViewports(viewports);
|
||||
return save(
|
||||
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
|
||||
listOfUIDs
|
||||
);
|
||||
},
|
||||
downloadAndZipSeriesOnActiveViewport({ servers, viewports, progress }) {
|
||||
const dicomWebClient = getDicomWebClientFromContext(context, servers);
|
||||
const listOfUIDs = getSOPInstanceReferenceFromActiveViewport(viewports);
|
||||
return save(
|
||||
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
|
||||
listOfUIDs
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
downloadAndZip: {
|
||||
commandFn: queue.bindSafe(actions.downloadAndZip, error),
|
||||
storeContexts: ['servers'],
|
||||
},
|
||||
downloadAndZipSeriesOnViewports: {
|
||||
commandFn: queue.bindSafe(actions.downloadAndZipSeriesOnViewports, error),
|
||||
storeContexts: ['servers', 'viewports'],
|
||||
options: { progress },
|
||||
},
|
||||
downloadAndZipSeriesOnActiveViewport: {
|
||||
commandFn: queue.bindSafe(
|
||||
actions.downloadAndZipSeriesOnActiveViewport,
|
||||
error
|
||||
),
|
||||
storeContexts: ['servers', 'viewports'],
|
||||
options: { progress },
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
function progress(status) {
|
||||
OHIF.log.info(
|
||||
'Download and Zip Progress:',
|
||||
(status.progress * 100.0).toFixed(2) + '%'
|
||||
);
|
||||
}
|
||||
|
||||
function error(e) {
|
||||
if (e.message === 'Queue limit reached') {
|
||||
OHIF.log.warn('A download is already in progress, please wait.');
|
||||
} else {
|
||||
OHIF.log.error(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { api } from 'dicomweb-client';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const {
|
||||
utils: {
|
||||
isDicomUid,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils: progressUtils,
|
||||
},
|
||||
} = OHIF;
|
||||
|
||||
/**
|
||||
* Public Methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* Download and Zip all DICOM P10 instances from specified DICOM Web Client
|
||||
* based on an hierarchical list of UIDs;
|
||||
*
|
||||
* @param {DICOMwebClient} dicomWebClient A DICOMwebClient instance through
|
||||
* which the referenced instances will be retrieved;
|
||||
* @param {Array} listOfUIDs The hierarchical list of UIDs from the instances
|
||||
* that should be retrieved:
|
||||
* A hierarchical list of UIDs is a regular JS Array where the type of the UID
|
||||
* (study, series, instance) is determined by its nasting lavel. For example:
|
||||
* @ The following list instructs the library to download all the instances
|
||||
* from both studies "A" and "B":
|
||||
*
|
||||
* ['studyUIDFromA', 'studyUIDFromB']
|
||||
*
|
||||
* @ In the previous example both UIDs are treated as STUDY UIDs because both
|
||||
* of them are listed in the same (top) level of the list. If, on the other
|
||||
* hand, only instances from series "I" and "J" from the study "B"
|
||||
* are to be downloaded, the expected hierarchical list would be:
|
||||
*
|
||||
* ['studyUIDFromA', ['studyUIDFromB', ['seriesUIDFromI', 'seriesUIDFromJ']]]
|
||||
*
|
||||
* @ Which, when prettified, reads like this:
|
||||
*
|
||||
* [
|
||||
* 'studyUIDFromA',
|
||||
* ['studyUIDFromB', [
|
||||
* 'seriesUIDFromI',
|
||||
* 'seriesUIDFromJ'
|
||||
* ]]
|
||||
* ]
|
||||
*
|
||||
* @ Furthermore, if only instances "X", "Y" and "Z" from series "J" need to
|
||||
* be downloaded (instead of all the instances from that series), the list
|
||||
* could be changed to:
|
||||
*
|
||||
* [
|
||||
* 'studyUIDFromA',
|
||||
* ['studyUIDFromB', [
|
||||
* 'seriesUIDFromI',
|
||||
* ['seriesUIDFromJ', [
|
||||
* 'instanceUIDFromX',
|
||||
* 'instanceUIDFromY',
|
||||
* 'instanceUIDFromZ'
|
||||
* ]]
|
||||
* ]]
|
||||
* ]
|
||||
*
|
||||
* Please refer to hierarchicalListUtils.js for more information and utilities;
|
||||
*
|
||||
* @param {Object} options A plain object with options;
|
||||
* @param {function} options.progress A callback to retrieve notifications
|
||||
* @returns {Promise} A promise that resolves to an URL from which the ZIP file
|
||||
* can be downloaded;
|
||||
*/
|
||||
|
||||
async function downloadAndZip(dicomWebClient, listOfUIDs, options) {
|
||||
if (dicomWebClient instanceof api.DICOMwebClient) {
|
||||
const settings = buildSettings(listOfUIDs, options);
|
||||
const { compression } = settings.tasks;
|
||||
// Register user-provided progress handler as a task list observer
|
||||
progressUtils.addObserver(settings.taskList, settings.options.progress);
|
||||
const buffers = await downloadAll(dicomWebClient, settings).catch(error => {
|
||||
// Reject promise from compression task on download failure
|
||||
compression.deferred.reject(error);
|
||||
throw error;
|
||||
});
|
||||
compression.deferred.resolve(zipAll(buffers, settings));
|
||||
const url = await compression.deferred.promise;
|
||||
return url;
|
||||
}
|
||||
throw new Error('A valid DICOM Web Client instance is expected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
async function zipAll(buffers, settings) {
|
||||
const zip = new JSZip();
|
||||
OHIF.log.info('Adding DICOM P10 files to archive:', buffers.length);
|
||||
buffers.forEach((buffer, i) => {
|
||||
const path = buildPath(buffer) || `${i}.dcm`;
|
||||
zip.file(path, buffer);
|
||||
});
|
||||
// Set compression task progress to 50%
|
||||
progressUtils.update(settings.tasks.compression.task, 0.5);
|
||||
const blob = await zip.generateAsync({ type: 'blob' });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
function buildSettings(listOfUIDs, options) {
|
||||
const taskList = progressUtils.createList();
|
||||
const compression = progressUtils.addDeferred(taskList);
|
||||
const downloads = [];
|
||||
|
||||
// Build downloads list
|
||||
hierarchicalListUtils.forEach(
|
||||
listOfUIDs,
|
||||
(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) => {
|
||||
if (isDicomUid(StudyInstanceUID)) {
|
||||
downloads.push({
|
||||
tracking: progressUtils.addDeferred(taskList),
|
||||
parameters: [StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Print tree of hierarchical references
|
||||
OHIF.log.info('Downloading DICOM P10 files for references:');
|
||||
OHIF.log.info(hierarchicalListUtils.print(listOfUIDs));
|
||||
|
||||
return {
|
||||
options: Object(options),
|
||||
taskList,
|
||||
tasks: {
|
||||
downloads,
|
||||
compression,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildPath(buffer) {
|
||||
let path;
|
||||
try {
|
||||
const byteArray = new Uint8Array(buffer);
|
||||
const dataSet = dicomParser.parseDicom(byteArray, {
|
||||
// Stop parsing after SeriesInstanceUID is found
|
||||
untilTag: 'x0020000e',
|
||||
});
|
||||
const StudyInstanceUID = dataSet.string('x0020000d');
|
||||
const SeriesInstanceUID = dataSet.string('x0020000e');
|
||||
const SOPInstanceUID = dataSet.string('x00080018');
|
||||
if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) {
|
||||
path = `${StudyInstanceUID}/${SeriesInstanceUID}/${SOPInstanceUID}.dcm`;
|
||||
}
|
||||
} catch (e) {
|
||||
OHIF.log.error('Error parsing downloaded DICOM P10 file...', e);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
async function downloadAll(dicomWebClient, settings) {
|
||||
const { downloads } = settings.tasks;
|
||||
|
||||
// Make sure at least one download was initiated
|
||||
if (downloads.length < 1) {
|
||||
throw new Error('No valid reference to be downloaded');
|
||||
}
|
||||
|
||||
const promises = downloads.map(item => {
|
||||
const {
|
||||
parameters,
|
||||
tracking: { deferred, task },
|
||||
} = item;
|
||||
deferred.resolve(download(task, dicomWebClient, ...parameters));
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
// Wait on created download promises
|
||||
return Promise.all(promises).then(results => {
|
||||
const buffers = [];
|
||||
// The "results" array may directly contain buffers (ArrayBuffer instances)
|
||||
// or arrays of buffers, depending on the type of downloads initiated on the
|
||||
// previous step (retrieveStudy, retrieveSeries or retrieveinstance). Ex:
|
||||
// results = [buf1, [buf2, buf3], buf4, [buf5], ...];
|
||||
results.forEach(
|
||||
function select(nesting, result) {
|
||||
if (result instanceof ArrayBuffer) {
|
||||
buffers.push(result);
|
||||
} else if (nesting && Array.isArray(result)) {
|
||||
// "nesting" argument is important to make sure only two levels
|
||||
// of arrays are visited. For example, "bufX" should not be visited:
|
||||
// [buf1, [buf2, buf3, [bufX]], buf4, [buf5], ...];
|
||||
result.forEach(select.bind(null, false));
|
||||
}
|
||||
}.bind(null, true)
|
||||
);
|
||||
return buffers;
|
||||
});
|
||||
}
|
||||
|
||||
async function download(
|
||||
task,
|
||||
dicomWebClient,
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID
|
||||
) {
|
||||
// Strict DICOM-formatted variable names COULDN'T be used here because the
|
||||
// DICOM Web client interface expects them in this specific format.
|
||||
// @TODO: Add support for download progress handler which will use the
|
||||
// currently not use "task" param
|
||||
if (!isDicomUid(studyInstanceUID)) {
|
||||
throw new Error('Download requires at least a "StudyInstanceUID" property');
|
||||
}
|
||||
if (!isDicomUid(seriesInstanceUID)) {
|
||||
// Download entire study
|
||||
return dicomWebClient.retrieveStudy({
|
||||
studyInstanceUID,
|
||||
});
|
||||
}
|
||||
if (!isDicomUid(sopInstanceUID)) {
|
||||
// Download entire series
|
||||
return dicomWebClient.retrieveSeries({
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
});
|
||||
}
|
||||
// Download specific instance
|
||||
return dicomWebClient.retrieveInstance({
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
export { downloadAndZip as default, downloadAndZip };
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getDicomWebClientFromConfig } from './utils';
|
||||
import { getCommands } from './commandsModule';
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
/**
|
||||
* Globals
|
||||
*/
|
||||
|
||||
const sharedContext = {
|
||||
dicomWebClient: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Extension
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'dicom-p10-downloader',
|
||||
|
||||
/**
|
||||
* LIFECYCLE HOOKS
|
||||
*/
|
||||
|
||||
preRegistration({ appConfig }) {
|
||||
const dicomWebClient = getDicomWebClientFromConfig(appConfig);
|
||||
if (dicomWebClient) {
|
||||
sharedContext.dicomWebClient = dicomWebClient;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* MODULE GETTERS
|
||||
*/
|
||||
|
||||
getCommandsModule() {
|
||||
return getCommands(sharedContext);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { api } from 'dicomweb-client';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
const {
|
||||
utils: { isDicomUid, resolveObjectPath, hierarchicalListUtils },
|
||||
DICOMWeb,
|
||||
} = OHIF;
|
||||
|
||||
function validDicomUid(subject) {
|
||||
if (isDicomUid(subject)) {
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
|
||||
function getActiveServerFromServersStore(store) {
|
||||
const servers = resolveObjectPath(store, 'servers');
|
||||
if (Array.isArray(servers) && servers.length > 0) {
|
||||
return servers.find(server => resolveObjectPath(server, 'active') === true);
|
||||
}
|
||||
}
|
||||
|
||||
function getDicomWebClientFromConfig(config) {
|
||||
const servers = resolveObjectPath(config, 'servers.dicomWeb');
|
||||
if (Array.isArray(servers) && servers.length > 0) {
|
||||
const server = servers[0];
|
||||
return new api.DICOMwebClient({
|
||||
url: server.wadoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getDicomWebClientFromContext(context, store) {
|
||||
const activeServer = getActiveServerFromServersStore(store);
|
||||
if (activeServer) {
|
||||
return new api.DICOMwebClient({
|
||||
url: activeServer.wadoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(activeServer),
|
||||
});
|
||||
} else if (context.dicomWebClient instanceof api.DICOMwebClient) {
|
||||
return context.dicomWebClient;
|
||||
}
|
||||
}
|
||||
|
||||
function getSOPInstanceReference(viewports, index) {
|
||||
if (index >= 0) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = Object(
|
||||
resolveObjectPath(viewports, `viewportSpecificData.${index}`)
|
||||
);
|
||||
return Object.freeze(
|
||||
hierarchicalListUtils.addToList(
|
||||
[],
|
||||
validDicomUid(StudyInstanceUID),
|
||||
validDicomUid(SeriesInstanceUID),
|
||||
validDicomUid(SOPInstanceUID)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getSOPInstanceReferenceFromActiveViewport(viewports) {
|
||||
return getSOPInstanceReference(
|
||||
viewports,
|
||||
resolveObjectPath(viewports, 'activeViewportIndex')
|
||||
);
|
||||
}
|
||||
|
||||
function getSOPInstanceReferencesFromViewports(viewports) {
|
||||
const list = [];
|
||||
const viewportSpecificData = resolveObjectPath(
|
||||
viewports,
|
||||
'viewportSpecificData'
|
||||
);
|
||||
Object.keys(viewportSpecificData).forEach(index => {
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = Object(
|
||||
viewportSpecificData[index]
|
||||
);
|
||||
hierarchicalListUtils.addToList(
|
||||
list,
|
||||
validDicomUid(StudyInstanceUID),
|
||||
validDicomUid(SeriesInstanceUID),
|
||||
validDicomUid(SOPInstanceUID)
|
||||
);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
function save(promise, listOfUIDs) {
|
||||
return Promise.resolve(promise)
|
||||
.then(url => {
|
||||
OHIF.log.info('Files successfully compressed:', url);
|
||||
const StudyInstanceUID = hierarchicalListUtils.getItem(listOfUIDs, 0);
|
||||
saveAs(url, `${StudyInstanceUID}.zip`);
|
||||
return url;
|
||||
})
|
||||
.catch(error => {
|
||||
OHIF.log.error('Failed to create Zip file...', error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
save,
|
||||
validDicomUid,
|
||||
getDicomWebClientFromConfig,
|
||||
getDicomWebClientFromContext,
|
||||
getSOPInstanceReferenceFromActiveViewport,
|
||||
getSOPInstanceReferencesFromViewports,
|
||||
};
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.2...@ohif/extension-dicom-pdf@1.0.3) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.1...@ohif/extension-dicom-pdf@1.0.2) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.0...@ohif/extension-dicom-pdf@1.0.1) (2019-12-16)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.3",
|
||||
"description": "OHIF extension for Dicom PDF",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -101,9 +101,9 @@ class DicomPDFViewport extends Component {
|
||||
let pdfByteArray = byteArray;
|
||||
|
||||
if (dataSet) {
|
||||
const sopClassUid = dataSet.string('x00080016');
|
||||
const SOPClassUID = dataSet.string('x00080016');
|
||||
|
||||
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
|
||||
if (SOPClassUID !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
|
||||
throw new Error('This is not a DICOM-encapsulated PDF');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,19 @@ const SOP_CLASS_UIDS = {
|
||||
const OHIFDicomPDFSopClassHandler = {
|
||||
id: 'OHIFDicomPDFSopClassHandlerPlugin',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUids: [SOP_CLASS_UIDS.ENCAPSULATED_PDF],
|
||||
sopClassUIDs: [SOP_CLASS_UIDS.ENCAPSULATED_PDF],
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'pdf',
|
||||
modality: 'DOC',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'DOC',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders: authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ class OHIFDicomPDFViewport extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { displaySet, studies } = this.props.viewportData;
|
||||
|
||||
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
|
||||
data => this.setState({ byteArray: new Uint8Array(data) }),
|
||||
error => {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* We use this component to leverage "Code Splitting"
|
||||
*
|
||||
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export default function asyncComponent(importComponent) {
|
||||
class AsyncComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
component: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Add dynamically loaded component to state
|
||||
const { default: component } = await importComponent();
|
||||
|
||||
this.setState({
|
||||
component: component,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const C = this.state.component;
|
||||
|
||||
// Render the loaded component, or null
|
||||
return C ? <C {...this.props} /> : null;
|
||||
}
|
||||
}
|
||||
|
||||
return AsyncComponent;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import React from 'react';
|
||||
import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js';
|
||||
|
||||
const ConnectedOHIFDicomPDFViewer = asyncComponent(() =>
|
||||
import(
|
||||
/* webpackChunkName: "ConnectedOHIFDicomPDFViewer" */ './ConnectedOHIFDicomPDFViewer'
|
||||
)
|
||||
);
|
||||
const Component = React.lazy(() => {
|
||||
return import('./ConnectedOHIFDicomPDFViewer');
|
||||
});
|
||||
|
||||
const ConnectedOHIFDicomPDFViewer = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
|
||||
@@ -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,19 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.0.2...@ohif/extension-dicom-rt@0.1.0) (2020-04-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 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.1.0",
|
||||
"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.12.2",
|
||||
"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,88 @@
|
||||
.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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-label span {
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: calc(100% - 40px); /* calc(100% - 50px); 20px = eye icon */
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.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 }}>
|
||||
<span>{label}</span>
|
||||
<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,77 @@
|
||||
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];
|
||||
if (
|
||||
/* Could be expanded to contain RTPLAN and RTDOSE information in the future */
|
||||
['RTSTRUCT'].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;
|
||||
}
|
||||
@@ -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,33 @@
|
||||
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',
|
||||
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,41 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.2...@ohif/extension-dicom-segmentation@0.2.0) (2020-04-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* segmentation not loading ([#1566](https://github.com/OHIF/Viewers/issues/1566)) ([4a7ce1c](https://github.com/OHIF/Viewers/commit/4a7ce1c09324d74c61048393e3a2427757e4001a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 0.1.0 (2020-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
|
||||
@@ -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,65 @@
|
||||
# @ohif/dicom-segmentation
|
||||
|
||||

|
||||
|
||||
This extension adds support to view and navigate the segments available in the DICOM SEG,
|
||||
allowing control over visibility of a given segment and providing metadata about the segment(s).
|
||||
|
||||
<!-- TODO: Simple image or GIF? -->
|
||||
|
||||
#### Index
|
||||
|
||||
Extension Id: `com.ohif.dicom-segmentation`
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
Tools can be configured through extension configuration using the tools key:
|
||||
|
||||
```js
|
||||
...
|
||||
cornerstoneExtensionConfig: {
|
||||
tools: {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
## Annotate Tools Configuration
|
||||
|
||||
*We currently support one property for annotation tools.*
|
||||
|
||||
### Hide handles
|
||||
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
|
||||
|
||||
```js
|
||||
...
|
||||
cornerstoneExtensionConfig: {
|
||||
hideHandles: true,
|
||||
},
|
||||
...
|
||||
|
||||
## Resources
|
||||
|
||||
### Repositories
|
||||
|
||||
- [cornerstonejs/react-cornerstone-viewport][react-viewport]
|
||||
- [cornerstonejs/cornerstoneTools][cornerstone-tools]
|
||||
- [cornerstonejs/cornerstone][cornerstone]
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[docs-commands]: https://www.com
|
||||
[docs-hotkeys]: https://www.com
|
||||
[docs-userprefs]: htt
|
||||
[react-viewport]: https://github.com/cornerstonejs/react-cornerstone-viewport
|
||||
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
|
||||
[cornerstone]: https://github.com/cornerstonejs/cornerstone
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.2.0",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"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.12.5",
|
||||
"dcmjs": "^0.12.2",
|
||||
"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,71 @@
|
||||
import { MODULE_TYPES, utils } from '@ohif/core';
|
||||
import loadSegmentation from './loadSegmentation';
|
||||
|
||||
// TODO: Should probably use dcmjs for this
|
||||
const SOP_CLASS_UIDS = {
|
||||
DICOM_SEG: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
};
|
||||
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
||||
// same SOP Class.
|
||||
const OHIFDicomSegSopClassHandler = {
|
||||
id: 'OHIFDicomSegSopClassHandler',
|
||||
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;
|
||||
|
||||
const segDisplaySet = {
|
||||
Modality: 'SEG',
|
||||
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,
|
||||
};
|
||||
|
||||
segDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||
return loadSegmentation(
|
||||
segDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
).catch(error => {
|
||||
segDisplaySet.isLoaded = false;
|
||||
throw new Error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return segDisplaySet;
|
||||
},
|
||||
};
|
||||
|
||||
export default OHIFDicomSegSopClassHandler;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
.dcmseg-brush-color-selector .selector-buttons 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;
|
||||
}
|
||||
|
||||
.dcmseg-brush-color-selector .selector-buttons button:hover,
|
||||
.dcmseg-brush-color-selector .selector-buttons button:active,
|
||||
.dcmseg-brush-color-selector .selector-buttons button.active {
|
||||
background-color: var(--active-color);
|
||||
border-color: var(--ui-border-color-active);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dcmseg-brush-color-selector .selector-buttons {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dcmseg-brush-color-selector .selector-buttons button:last-of-type {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dcmseg-brush-color-selector .selector-active-segment {
|
||||
border-radius: 100%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './BrushColorSelector.css';
|
||||
|
||||
const BrushColorSelector = ({ defaultColor, index, onNext, onPrev }) => (
|
||||
<div className="dcmseg-brush-color-selector">
|
||||
<div
|
||||
className="selector-active-segment"
|
||||
style={{ backgroundColor: defaultColor }}
|
||||
>
|
||||
{index}
|
||||
</div>
|
||||
<div className="selector-buttons">
|
||||
<button onClick={onPrev}>
|
||||
Previous
|
||||
</button>
|
||||
<button onClick={onNext}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
BrushColorSelector.propTypes = {
|
||||
defaultColor: PropTypes.string.isRequired,
|
||||
index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
onNext: PropTypes.func.isRequired,
|
||||
onPrev: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default BrushColorSelector;
|
||||
@@ -0,0 +1,4 @@
|
||||
.dcmseg-brush-radius label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Range } from '@ohif/ui';
|
||||
|
||||
import './BrushRadius.css';
|
||||
|
||||
const BrushRadius = ({ value, onChange, min, max, step }) => (
|
||||
<div className="dcmseg-brush-radius">
|
||||
<label htmlFor="dcmseg-brush-radius">Brush Radius</label>
|
||||
<Range
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onChange={onChange}
|
||||
id="dcmseg-brush-radius"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
BrushRadius.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
step: PropTypes.number,
|
||||
};
|
||||
|
||||
BrushRadius.defaultProps = {
|
||||
onChange: () => { },
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
export default BrushRadius;
|
||||
@@ -0,0 +1,83 @@
|
||||
.dcmseg-segment-item .segment-color-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-color {
|
||||
margin-top: 8px;
|
||||
border-radius: 100%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-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%;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item.selected .segment-actions {
|
||||
height: 35px;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-actions {
|
||||
margin-left: -1px;
|
||||
background-color: var(--ui-gray-darker);
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
visibility: hidden;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-actions .btnAction {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--default-color);
|
||||
cursor: pointer;
|
||||
line-height: 35px;
|
||||
height: 35px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-actions .btnAction:hover, .dcmseg-segment-item .segment-actions .btnAction:active {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-actions .btnAction i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-label span {
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: calc(100% - 40px); /* calc(100% - 50px); 20px = eye icon */
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-label .eye-icon {
|
||||
cursor: pointer;
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-label .eye-icon:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-label .eye-icon.--visible {
|
||||
color: var(--default-color);
|
||||
}
|
||||
Loaded 100 of 340 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user