Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d827c2a49 | ||
|
|
a8305772d0 | ||
|
|
eab115e70c | ||
|
|
c02b232b0c | ||
|
|
f01640d009 | ||
|
|
63fd65690c | ||
|
|
944afe4a3e | ||
|
|
b1678ce639 | ||
|
|
39937cca08 | ||
|
|
f7b8b6a41c | ||
|
|
79fe442293 | ||
|
|
875238e5b0 | ||
|
|
d2d3ca2828 | ||
|
|
cebea832a3 | ||
|
|
c4f84b1174 | ||
|
|
f9fe4232e7 | ||
|
|
42c22df1b6 | ||
|
|
19a8e71317 | ||
|
|
fa95bae6ef | ||
|
|
8a20a0526e | ||
|
|
5b20d8f323 | ||
|
|
9ea37a2201 | ||
|
|
2a66e175df | ||
|
|
77a421cdc0 | ||
|
|
7033886697 | ||
|
|
256563d7ee | ||
|
|
f7162ce617 | ||
|
|
027ef4f93c | ||
|
|
f09ebbe2e8 | ||
|
|
51b4e60161 | ||
|
|
08de987711 | ||
|
|
20e366ffbd | ||
|
|
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 |
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 -->
|
||||
+107
-10
@@ -123,14 +123,111 @@ 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?
|
||||
|
||||
> This is possible to do with the OHIF Viewer, but not as straightforward. Look
|
||||
> out for documentation on this subject in the near future.
|
||||
It's possible to supply all Study data via JSON format, in the event you do not have a DicomWeb endpoint.
|
||||
You can host all of the relevant files on any web accessible server (Amazon S3, Azure Blob Storage, Local file server etc.)
|
||||
|
||||
This JSON is supplied via the '?url=' query parameter.
|
||||
It should reference an endpoint that returns **application/json** formatted text.
|
||||
|
||||
If you do not have an API, you can simply return a text file containing the JSON from any web server.
|
||||
|
||||
|
||||
You tell the OHIF viewer to use JSON by appending the `'?url='` query to the `/Viewer` route:
|
||||
|
||||
eg. `https://my-test-ohif-server/viewer?url=https://my-json-server/study-uid.json`
|
||||
|
||||
The returned JSON object must contain a single root object with a 'studies' array.
|
||||
|
||||
|
||||
*Sample JSON format:*
|
||||
```JSON
|
||||
{
|
||||
"studies": [
|
||||
{
|
||||
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78",
|
||||
"StudyDescription": "BRAIN SELLA",
|
||||
"StudyDate": "20010108",
|
||||
"StudyTime": "120022",
|
||||
"PatientName": "MISTER^MR",
|
||||
"PatientId": "832040",
|
||||
"series": [
|
||||
{
|
||||
"SeriesDescription": "SAG T-1",
|
||||
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
|
||||
"SeriesNumber": 2,
|
||||
"SeriesDate": "20010108",
|
||||
"SeriesTime": "120318",
|
||||
"Modality": "MR",
|
||||
"instances": [
|
||||
{
|
||||
"metadata": {
|
||||
"Columns": 512,
|
||||
"Rows": 512,
|
||||
"InstanceNumber": 3,
|
||||
"AcquisitionNumber": 0,
|
||||
"PhotometricInterpretation": "MONOCHROME2",
|
||||
"BitsAllocated": 16,
|
||||
"BitsStored": 16,
|
||||
"PixelRepresentation": 1,
|
||||
"SamplesPerPixel": 1,
|
||||
"PixelSpacing": [0.390625, 0.390625],
|
||||
"HighBit": 15,
|
||||
"ImageOrientationPatient": [0,1,0,0,0,-1],
|
||||
"ImagePositionPatient": [11.600000,-92.500000, 98.099998],
|
||||
"FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470",
|
||||
"ImageType": ["ORIGINAL","PRIMARY","OTHER"],
|
||||
"Modality": "MR",
|
||||
"SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.124",
|
||||
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
|
||||
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78"
|
||||
},
|
||||
"url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.124.dcm"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
More info on this JSON format can be found here [Issue #1500](https://github.com/OHIF/Viewers/issues/1500)
|
||||
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route.
|
||||
2. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomweb:` in order for the cornerstone image loader to retrieve it correctly.
|
||||
eg. `https://image-server/my-image.dcm` ---> `dicomweb:https://image-server/my-image.dcm`
|
||||
3. The JSON format above is compatible with >= v3.7.8 of the application. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet.
|
||||
4. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500).
|
||||
|
||||
*CORS Issues (Cross-Origin Resource Sharing)*
|
||||
|
||||
If you host a JSON API or Images on a different domain from the the app itself, you will likely have CORS issues. This will also happen when testing from Localhost and reaching out to remote servers.
|
||||
Even if the domain is the same, different ports, subdomains or protocols (https vs http) will also cause CORS errors.
|
||||
You will to need add a configuration on each server hosting these assets to allow your App server origin.
|
||||
|
||||
For example:
|
||||
|
||||
Lets assume your application is hosted on `https://my-ohif-server.com`.
|
||||
|
||||
Your JSON API is hosted on `https://my-json-api.aws.com`
|
||||
|
||||
And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com`
|
||||
|
||||
When you first start your application, browsing to `https://my-ohif-server.com/viewer?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`.
|
||||
|
||||
Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this.
|
||||
|
||||
Next, you will likely get a similar CORS error, as the browser tries to go to `https://my-s3-bucket.aws.com`.
|
||||
You will need to go to the S3 bucket configuration, and add a CORS setting to allow origin = `https://my-ohif-server.com`.
|
||||
|
||||
Essentially, whenever the application connects to a remote resource, you will need to add the applications url to the allowed CORS Origins on that resource. Adding an origin similar to https://localhost:3000 will also allow for local testing.
|
||||
|
||||
...
|
||||
|
||||
### Securing Your Data
|
||||
|
||||
@@ -166,7 +263,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 +280,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,9 +1,9 @@
|
||||
# Module: SOP Class Handler
|
||||
|
||||
An extension can register a SOP Class Handler Module by defining a
|
||||
`getSopClassHandlerModule` method. The SOP Class Handler is a bit different from
|
||||
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
|
||||
own components. It instead defines:
|
||||
An extension can register a [SOP Class][sop-class-link] Handler Module by
|
||||
defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link]
|
||||
Handler is a bit different from the other modules, as it doesn't provide a `1:1`
|
||||
schema for UI or provide it's own components. It instead defines:
|
||||
|
||||
- `sopClassUIDs`: an array of string SOP Class UIDs that the
|
||||
`getDisplaySetFromSeries` method should be applied to.
|
||||
@@ -76,6 +76,13 @@ getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
};
|
||||
```
|
||||
|
||||
### More examples :
|
||||
|
||||
- [Dicom-HTML SOP][dicom-html-sop]
|
||||
- [Dicom-PDF SOP][dicom-pdf-sop]
|
||||
- [Dicom-Microscopy SOP][dicom-micro-sop]
|
||||
- [Dicom-Segmentation SOP][dicom-seg-sop]
|
||||
|
||||
## `@ohif/viewer` usage
|
||||
|
||||
We use the `sopClassHandlerModule`s in three different places:
|
||||
@@ -88,3 +95,11 @@ Each time, it is used to map study and series data to `displaySets`. It does
|
||||
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
|
||||
manager has the method `createDisplaySets` that takes an array of
|
||||
`sopClassHandlerModules`.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html
|
||||
[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12
|
||||
[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6
|
||||
[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7
|
||||
[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -75,7 +75,7 @@ items to our ["Active Development" GitHub Project Board][gh-board]. Items on
|
||||
this project board are either in active development by Core Team members, or
|
||||
queued up for development as in-progress items are completed.
|
||||
|
||||
> :pencil: Want to contribute but not sure where to start? Check out [Up for
|
||||
> 🖋 Want to contribute but not sure where to start? Check out [Up for
|
||||
> grabs][label-grabs] issues and our [Contributing
|
||||
> documentation][contributing-docs]
|
||||
|
||||
@@ -156,5 +156,5 @@ addressed and a Stable version is released.
|
||||
[ohif-prod]: https://viewer.ohif.org
|
||||
[gh-board]: https://github.com/OHIF/Viewers/projects/4
|
||||
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
|
||||
[contributing-docs]: https://docs.ohif.org/contributing/
|
||||
[contributing-docs]: ./development/contributing.md
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -62,7 +62,7 @@ able to fail or be removed. Related patterns that may reduce coupling include:
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[commands]: #/
|
||||
[commands]: ../extensions/modules/commands.md
|
||||
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
|
||||
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
|
||||
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Tips & Tricks](#/)
|
||||
- [Maintained Services](#/)
|
||||
- [Tips & Tricks](#tips--tricks)
|
||||
- [Maintained Services](#maintained-services)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -3,6 +3,107 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.8.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.8.0...@ohif/extension-cornerstone@2.8.1) (2020-06-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Infinite frame index change when quickly jumping between ([#1734](https://github.com/OHIF/Viewers/issues/1734)) ([a830577](https://github.com/OHIF/Viewers/commit/a8305772d0fbec506549ad7ea08504a8bc3a4f39)), closes [#1733](https://github.com/OHIF/Viewers/issues/1733)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.8.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.3...@ohif/extension-cornerstone@2.8.0) (2020-06-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.2...@ohif/extension-cornerstone@2.7.3) (2020-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.1...@ohif/extension-cornerstone@2.7.2) (2020-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.0...@ohif/extension-cornerstone@2.7.1) (2020-05-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.5.0",
|
||||
"version": "2.8.1",
|
||||
"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.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.10.1",
|
||||
"cornerstone-tools": "4.15.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "^0.12.3",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -51,6 +51,6 @@
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-cornerstone-viewport": "^2.3.6"
|
||||
"react-cornerstone-viewport": "2.3.9"
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
@@ -215,12 +215,28 @@ class OHIFCornerstoneViewport extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
const newImageHandler = ({ currentImageIdIndex, sopInstanceUid }) => {
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const { StudyInstanceUID } = displaySet;
|
||||
|
||||
if (currentImageIdIndex > 0) {
|
||||
this.props.onNewImage({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID: sopInstanceUid,
|
||||
frameIndex: currentImageIdIndex,
|
||||
activeViewportIndex: viewportIndex,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectedCornerstoneViewport
|
||||
viewportIndex={viewportIndex}
|
||||
imageIds={imageIds}
|
||||
imageIdIndex={currentImageIdIndex}
|
||||
onNewImage={newImageHandler}
|
||||
onNewImageDebounceTime={700}
|
||||
// ~~ Connected (From REDUX)
|
||||
// frameRate={frameRate}
|
||||
// isPlaying={false}
|
||||
|
||||
@@ -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 }) => {
|
||||
@@ -255,9 +258,35 @@ const commandsModule = ({ servicesManager }) => {
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
jumpToImage: ({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex
|
||||
}) => {
|
||||
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||
|
||||
const displaySet = study.findDisplaySet(ds => {
|
||||
return ds.images && 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,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>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -28,8 +34,17 @@ export default {
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getViewportModule() {
|
||||
return OHIFCornerstoneViewport;
|
||||
getViewportModule({ commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
const onNewImageHandler = jumpData => {
|
||||
commandsManager.runCommand('jumpToImage', jumpData);
|
||||
};
|
||||
return (
|
||||
<OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />
|
||||
);
|
||||
};
|
||||
|
||||
return ExtendedOHIFCornerstoneViewport;
|
||||
},
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.1...@ohif/extension-dicom-html@1.2.2) (2020-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.0...@ohif/extension-dicom-html@1.2.1) (2020-04-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.2",
|
||||
"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.10.1",
|
||||
"dcmjs": "^0.12.3",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import dcmjs from 'dcmjs';
|
||||
import TypedArrayProp from './TypedArrayProp';
|
||||
|
||||
import './DicomHtmlViewport.css';
|
||||
|
||||
@@ -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,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.9...@ohif/extension-dicom-microscopy@0.51.0) (2020-06-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "0.50.7",
|
||||
"version": "0.51.0",
|
||||
"description": "OHIF extension for Dicom Microscopy",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -34,11 +34,12 @@ class DicomMicroscopyViewport extends Component {
|
||||
.then(instances => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
const sopInstanceUid = instances[i]['00080018']['Value'][0];
|
||||
const sopInstanceUID = instances[i]['00080018']['Value'][0];
|
||||
|
||||
const retrieveInstanceOptions = {
|
||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||
seriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
sopInstanceUid,
|
||||
sopInstanceUID,
|
||||
};
|
||||
|
||||
const promise = dicomWebClient
|
||||
@@ -91,8 +92,8 @@ class DicomMicroscopyViewport extends Component {
|
||||
{this.state.error ? (
|
||||
<h2>{JSON.stringify(this.state.error)}</h2>
|
||||
) : (
|
||||
<div style={style} ref={this.container} />
|
||||
)}
|
||||
<div style={style} ref={this.container} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,14 @@
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"description": "OHIF extension for Dicom PDF",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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,71 @@
|
||||
# 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-rt@0.1.4...@ohif/extension-dicom-rt@0.2.0) (2020-06-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.3...@ohif/extension-dicom-rt@0.1.4) (2020-05-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Load default display set when no time metadata ([#1684](https://github.com/OHIF/Viewers/issues/1684)) ([f7b8b6a](https://github.com/OHIF/Viewers/commit/f7b8b6a41c4626084ef56b0fdf7363e914b143c4)), closes [#1683](https://github.com/OHIF/Viewers/issues/1683)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.2...@ohif/extension-dicom-rt@0.1.3) (2020-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.1...@ohif/extension-dicom-rt@0.1.2) (2020-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.0...@ohif/extension-dicom-rt@0.1.1) (2020-04-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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.2.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.15.1",
|
||||
"dcmjs": "^0.12.3",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core';
|
||||
import loadRTStruct from './loadRTStruct';
|
||||
|
||||
import id from './id';
|
||||
|
||||
// TODO: Should probably use dcmjs for this
|
||||
const SOP_CLASS_UIDS = {
|
||||
DICOM_RT_STRUCT: '1.2.840.10008.5.1.4.1.1.481.3',
|
||||
};
|
||||
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
const OHIFDicomRTStructSopClassHandler = {
|
||||
id: 'OHIFDicomRTStructSopClassHandler',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUIDs,
|
||||
getDisplaySetFromSeries: function (
|
||||
series,
|
||||
study,
|
||||
dicomWebClient,
|
||||
authorizationHeaders
|
||||
) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
const metadata = instance.getData().metadata;
|
||||
const {
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesDescription,
|
||||
FrameOfReferenceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = metadata;
|
||||
|
||||
// TODO -> GET REFERENCED FRAME OF REFERENCE SEQUENCE.
|
||||
|
||||
const rtStructDisplaySet = {
|
||||
Modality: 'RTSTRUCT',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
authorizationHeaders,
|
||||
metadata,
|
||||
isDerived: true,
|
||||
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||
labelmapIndex: null, // Assigned when loaded.
|
||||
isLoaded: false,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesDescription,
|
||||
};
|
||||
|
||||
if (!metadata.ReferencedSeriesSequence) {
|
||||
const ReferencedFrameOfReferenceSequence =
|
||||
metadata.ReferencedFrameOfReferenceSequence;
|
||||
|
||||
if (ReferencedFrameOfReferenceSequence) {
|
||||
// TODO -> @dannyrb Do we augment metadata or add a (potentially large? fallback list in filterDerivedDisplaySets )
|
||||
metadata.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
ReferencedFrameOfReferenceSequence
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||
return loadRTStruct(
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
).catch(error => {
|
||||
rtStructDisplaySet.isLoaded = false;
|
||||
throw new Error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return rtStructDisplaySet;
|
||||
},
|
||||
};
|
||||
|
||||
function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
ReferencedFrameOfReferenceSequence
|
||||
) {
|
||||
const ReferencedSeriesSequence = [];
|
||||
|
||||
_getSequenceAsArray(ReferencedFrameOfReferenceSequence).forEach(
|
||||
referencedFrameOfReference => {
|
||||
const { RTReferencedStudySequence } = referencedFrameOfReference;
|
||||
|
||||
_getSequenceAsArray(RTReferencedStudySequence).forEach(
|
||||
rtReferencedStudy => {
|
||||
const { RTReferencedSeriesSequence } = rtReferencedStudy;
|
||||
|
||||
_getSequenceAsArray(RTReferencedSeriesSequence).forEach(
|
||||
rtReferencedSeries => {
|
||||
const ReferencedInstanceSequence = [];
|
||||
const {
|
||||
ContourImageSequence,
|
||||
SeriesInstanceUID,
|
||||
} = rtReferencedSeries;
|
||||
|
||||
_getSequenceAsArray(ContourImageSequence).forEach(
|
||||
contourImage => {
|
||||
ReferencedInstanceSequence.push({
|
||||
ReferencedSOPInstanceUID:
|
||||
contourImage.ReferencedSOPInstanceUID,
|
||||
ReferencedSOPClassUID: contourImage.ReferencedSOPClassUID,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const referencedSeries = {
|
||||
SeriesInstanceUID,
|
||||
ReferencedInstanceSequence,
|
||||
};
|
||||
|
||||
ReferencedSeriesSequence.push(referencedSeries);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return ReferencedSeriesSequence;
|
||||
}
|
||||
|
||||
function _getSequenceAsArray(sequence) {
|
||||
return Array.isArray(sequence) ? sequence : [sequence];
|
||||
}
|
||||
|
||||
function _getReferencedFrameOfReferenceSequence(instance) {
|
||||
const referencedFrameOfReferenceSequenceRaw = instance['30060010'];
|
||||
|
||||
const referencedFrameOfReferenceSequence = [];
|
||||
|
||||
if (
|
||||
!referencedFrameOfReferenceSequenceRaw ||
|
||||
!referencedFrameOfReferenceSequenceRaw.Value
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
referencedFrameOfReferenceSequenceRaw.Value.forEach(
|
||||
referencedFrameOfReferenceRaw => {
|
||||
const frameOfReferenceUID = DICOMWeb.getString(
|
||||
referencedFrameOfReferenceRaw['00200052']
|
||||
);
|
||||
|
||||
const referencedFrameOfReference = { frameOfReferenceUID };
|
||||
const RTReferencedStudySequenceRaw =
|
||||
referencedFrameOfReferenceRaw['30060012'];
|
||||
|
||||
referencedFrameOfReferenceSequence.push(referencedFrameOfReference);
|
||||
|
||||
if (RTReferencedStudySequenceRaw && RTReferencedStudySequenceRaw.Value) {
|
||||
referencedFrameOfReference.rtReferencedStudySequence = [];
|
||||
|
||||
const { rtReferencedStudySequence } = referencedFrameOfReference;
|
||||
|
||||
RTReferencedStudySequenceRaw.Value.forEach(rtReferencedStudyRaw => {
|
||||
const referencedSopClassUID = DICOMWeb.getString(
|
||||
rtReferencedStudyRaw['00081150']
|
||||
);
|
||||
|
||||
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||
rtReferencedStudyRaw['00081155']
|
||||
);
|
||||
|
||||
const rtReferencedStudy = {
|
||||
referencedSopClassUID,
|
||||
referencedSOPInstanceUID,
|
||||
rtReferencedSeriesSequence: [],
|
||||
};
|
||||
|
||||
rtReferencedStudySequence.push(rtReferencedStudy);
|
||||
|
||||
const { rtReferencedSeriesSequence } = rtReferencedStudy;
|
||||
|
||||
const rtReferencedSeriesSequenceRaw =
|
||||
rtReferencedStudyRaw['30060014'];
|
||||
|
||||
rtReferencedSeriesSequenceRaw.Value.forEach(rtReferencedSeriesRaw => {
|
||||
const seriesInstanceUID = DICOMWeb.getString(
|
||||
rtReferencedSeriesRaw['0020000E']
|
||||
);
|
||||
|
||||
const rtReferencedSeries = {
|
||||
seriesInstanceUID,
|
||||
contourImageSequence: [],
|
||||
};
|
||||
|
||||
rtReferencedSeriesSequence.push(rtReferencedSeries);
|
||||
|
||||
const { contourImageSequence } = rtReferencedSeries;
|
||||
|
||||
const contourImageSequenceRaw = rtReferencedSeriesRaw['30060016'];
|
||||
|
||||
contourImageSequenceRaw.Value.forEach(contourImageRaw => {
|
||||
const referencedSOPClassUID = DICOMWeb.getString(
|
||||
contourImageRaw['00081150']
|
||||
);
|
||||
|
||||
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||
contourImageRaw['00081155']
|
||||
);
|
||||
|
||||
const referencedFrameNumber = DICOMWeb.getString(
|
||||
contourImageRaw['00081160']
|
||||
);
|
||||
const referencedSegmentNumber = DICOMWeb.getString(
|
||||
contourImageRaw['0062000B']
|
||||
);
|
||||
|
||||
const contourImage = {
|
||||
referencedSOPClassUID,
|
||||
referencedSOPInstanceUID,
|
||||
referencedFrameNumber,
|
||||
referencedSegmentNumber,
|
||||
};
|
||||
|
||||
contourImageSequence.push(contourImage);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return referencedFrameOfReferenceSequence;
|
||||
}
|
||||
|
||||
export default OHIFDicomRTStructSopClassHandler;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
|
||||
const LoadingIndicator = ({ height = "200px", width = "200px", expand = false }) => {
|
||||
return (
|
||||
<div style={expand ? { height: "100%", width: "100%" } : {}}>
|
||||
<svg style={{ margin: 'auto', background: 'none', display: 'block', shapeRendering: 'auto' }} width={width} height={height} viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(30 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(60 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(90 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(120 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(150 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(180 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(210 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(240 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(270 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(300 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(330 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingIndicator;
|
||||
@@ -0,0 +1,44 @@
|
||||
.dcmrt-panel-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-primary-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dcmrt-panel-section .header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--ui-gray-dark);
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.dcmrt-panel-section .header .eye-icon,
|
||||
.dcmrt-panel-section .header .angle-double-down,
|
||||
.dcmrt-panel-section .header .angle-double-up {
|
||||
cursor: pointer;
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.dcmrt-panel-section .header .eye-icon:hover,
|
||||
.dcmrt-panel-section .header .angle-double-down:hover,
|
||||
.dcmrt-panel-section .header .angle-double-up:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.dcmrt-panel-section .header .eye-icon.expanded {
|
||||
color: var(--default-color);
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.icons .angle-double-down, .icons .angle-double-up {
|
||||
margin-left: 15px;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
import './PanelSection.css';
|
||||
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||
|
||||
const PanelSection = ({
|
||||
title,
|
||||
children,
|
||||
visible = false,
|
||||
expanded = false,
|
||||
loading = false,
|
||||
hideVisibleButton = false,
|
||||
onVisibilityChange = () => { },
|
||||
onExpandChange = () => { }
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(expanded);
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(visible);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dcmrt-panel-section"
|
||||
style={{
|
||||
marginBottom: isExpanded ? 0 : 2,
|
||||
height: isExpanded ? '100%' : 'unset'
|
||||
}}
|
||||
>
|
||||
<div className="header">
|
||||
<div>{title}</div>
|
||||
<div className="icons">
|
||||
{!hideVisibleButton && (
|
||||
<Icon
|
||||
className={`eye-icon ${isVisible && 'expanded'}`}
|
||||
name={isVisible ? "eye" : "eye-closed"}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={() => {
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
onVisibilityChange(newVisibility);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Icon
|
||||
className={`angle-double-${isExpanded ? 'down' : 'up'} ${isExpanded && 'expanded'}`}
|
||||
name={`angle-double-${isExpanded ? 'down' : 'up'}`}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={() => {
|
||||
const newExpandValue = !isExpanded;
|
||||
setIsExpanded(newExpandValue);
|
||||
onExpandChange(newExpandValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{loading && isExpanded && <LoadingIndicator expand height="70px" width="70px" />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PanelSection.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||
visible: PropTypes.bool,
|
||||
expanded: PropTypes.bool,
|
||||
onVisibilityChange: PropTypes.func
|
||||
};
|
||||
|
||||
PanelSection.defaultProps = {
|
||||
visible: false,
|
||||
expanded: false,
|
||||
onVisibilityChange: () => { }
|
||||
};
|
||||
|
||||
export default PanelSection;
|
||||
@@ -0,0 +1,26 @@
|
||||
.dcmrt-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--primary-background-color);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header .cog-icon {
|
||||
align-self: flex-end;
|
||||
color: var(--default-color);
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header .cog-icon:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
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,
|
||||
activeContexts = [],
|
||||
contexts = {}
|
||||
}) => {
|
||||
const isVTK = () => activeContexts.includes(contexts.VTK);
|
||||
const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE);
|
||||
|
||||
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);
|
||||
|
||||
if (isCornerstone()) {
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
const element = enabledElements[activeIndex].element;
|
||||
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||
|
||||
if (!toolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = toolState.data[0].imageIds;
|
||||
|
||||
const 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.sets && 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={newVisibility => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
loadedSet.ROIContours.forEach(({ ROINumber }) => {
|
||||
module.setters.toggleROIContour(loadedSet.SeriesInstanceUID, ROINumber);
|
||||
});
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||
setState(state => ({ ...state, sets }));
|
||||
refreshViewport();
|
||||
}}
|
||||
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,105 @@
|
||||
import React, { useState, useEffect } 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);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(visible);
|
||||
}, [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,84 @@
|
||||
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, api }) {
|
||||
const ExtendedRTPanel = props => {
|
||||
const { activeContexts } = api.hooks.useAppContext();
|
||||
|
||||
const contourItemClickHandler = contourData => {
|
||||
commandsManager.runCommand('jumpToImage', contourData);
|
||||
};
|
||||
|
||||
return (
|
||||
<RTPanel
|
||||
{...props}
|
||||
onContourItemClick={contourItemClickHandler}
|
||||
activeContexts={activeContexts}
|
||||
contexts={api.contexts}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 dcmjs from 'dcmjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
||||
import TOOL_NAMES from './utils/toolNames';
|
||||
|
||||
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
|
||||
const globalImageIdSpecificToolStateManager =
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
const { DicomLoaderService } = OHIF.utils;
|
||||
|
||||
export default async function loadRTStruct(
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
) {
|
||||
const rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||
|
||||
// Set here is loading is asynchronous.
|
||||
// If this function throws its set back to false.
|
||||
rtStructDisplaySet.isLoaded = true;
|
||||
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
||||
|
||||
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||
rtStructDisplaySet,
|
||||
studies
|
||||
);
|
||||
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
|
||||
const rtStructDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
|
||||
rtStructDataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
|
||||
dicomData.meta
|
||||
);
|
||||
|
||||
// global cornerstone tools state to attach measurements to.
|
||||
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||
|
||||
const {
|
||||
StructureSetROISequence,
|
||||
ROIContourSequence,
|
||||
RTROIObservationsSequence,
|
||||
StructureSetLabel,
|
||||
} = rtStructDataset;
|
||||
|
||||
// Define our structure set entry and add it to the rtstruct module state.
|
||||
const structureSet = {
|
||||
StructureSetLabel,
|
||||
SeriesInstanceUID: rtStructDataset.SeriesInstanceUID,
|
||||
ROIContours: [],
|
||||
referencedSeriesSequence:
|
||||
rtStructDisplaySet.metadata.ReferencedSeriesSequence,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
rtStructModule.setters.structureSet(structureSet);
|
||||
|
||||
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||
|
||||
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||
|
||||
const ROIContour = ROIContourSequence[i];
|
||||
const { ReferencedROINumber, ContourSequence } = ROIContour;
|
||||
|
||||
if (!ContourSequence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_setROIContourMetadata(
|
||||
structureSet,
|
||||
StructureSetROISequence,
|
||||
RTROIObservationsSequence,
|
||||
ROIContour
|
||||
);
|
||||
|
||||
for (let c = 0; c < ContourSequence.length; c++) {
|
||||
const {
|
||||
ContourImageSequence,
|
||||
ContourData,
|
||||
NumberOfContourPoints,
|
||||
ContourGeometricType,
|
||||
} = ContourSequence[c];
|
||||
|
||||
if (ContourGeometricType !== 'CLOSED_PLANAR') {
|
||||
// TODO: Do we want to visualise types other than closed planar?
|
||||
// We could easily do open planar.
|
||||
continue;
|
||||
}
|
||||
|
||||
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
|
||||
toolState,
|
||||
imageId,
|
||||
rtStructDisplayToolName
|
||||
);
|
||||
|
||||
const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
|
||||
const points = [];
|
||||
|
||||
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
|
||||
points.push({
|
||||
x: ContourData[p],
|
||||
y: ContourData[p + 1],
|
||||
z: ContourData[p + 2],
|
||||
});
|
||||
}
|
||||
|
||||
transformPointsToImagePlane(points, imagePlane);
|
||||
|
||||
const measurementData = {
|
||||
handles: {
|
||||
points,
|
||||
},
|
||||
structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
|
||||
ROINumber: ReferencedROINumber,
|
||||
};
|
||||
|
||||
imageIdSpecificToolData.push(measurementData);
|
||||
}
|
||||
}
|
||||
|
||||
_setToolEnabledIfNotEnabled(rtStructDisplayToolName);
|
||||
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||
*
|
||||
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||
* or other components that could rely on loaded rtstructs that
|
||||
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||
* and update itself with the new structs.
|
||||
*
|
||||
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||
* updated after the structs are loaded) that React its not aware of its changes
|
||||
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||
*
|
||||
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
const event = new CustomEvent('extensiondicomrtrtloaded');
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function _setROIContourMetadata(
|
||||
structureSet,
|
||||
StructureSetROISequence,
|
||||
RTROIObservationsSequence,
|
||||
ROIContour
|
||||
) {
|
||||
const StructureSetROI = StructureSetROISequence.find(
|
||||
structureSetROI =>
|
||||
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||
);
|
||||
|
||||
const ROIContourData = {
|
||||
ROINumber: StructureSetROI.ROINumber,
|
||||
ROIName: StructureSetROI.ROIName,
|
||||
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
|
||||
ROIDescription: StructureSetROI.ROIDescription,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
_setROIContourDataColor(ROIContour, ROIContourData);
|
||||
|
||||
if (RTROIObservationsSequence) {
|
||||
// If present, add additional RTROIObservations metadata.
|
||||
_setROIContourRTROIObservations(
|
||||
ROIContourData,
|
||||
RTROIObservationsSequence,
|
||||
ROIContour.ReferencedROINumber
|
||||
);
|
||||
}
|
||||
|
||||
structureSet.ROIContours.push(ROIContourData);
|
||||
}
|
||||
|
||||
function _setROIContourDataColor(ROIContour, ROIContourData) {
|
||||
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
|
||||
|
||||
if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
|
||||
// If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
|
||||
ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
|
||||
}
|
||||
|
||||
if (ROIDisplayColor) {
|
||||
ROIContourData.colorArray = [...ROIDisplayColor];
|
||||
} else {
|
||||
//Choose a color from the cornerstoneTools colorLUT
|
||||
// We sample from the default color LUT here (i.e. 0), as we have nothing else to go on.
|
||||
const { getters } = cornerstoneTools.getModule('segmentation');
|
||||
const color = getters.colorForSegmentIndexColorLUT(
|
||||
0,
|
||||
ROIContourData.ROINumber
|
||||
);
|
||||
|
||||
ROIContourData.colorArray = [...color];
|
||||
}
|
||||
}
|
||||
|
||||
function _setROIContourRTROIObservations(
|
||||
ROIContourData,
|
||||
RTROIObservationsSequence,
|
||||
ROINumber
|
||||
) {
|
||||
const RTROIObservations = RTROIObservationsSequence.find(
|
||||
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
|
||||
);
|
||||
|
||||
if (RTROIObservations) {
|
||||
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
|
||||
const {
|
||||
ObservationNumber,
|
||||
ROIObservationDescription,
|
||||
RTROIInterpretedType,
|
||||
ROIInterpreter,
|
||||
} = RTROIObservations;
|
||||
|
||||
ROIContourData.RTROIObservations = {
|
||||
ObservationNumber,
|
||||
ROIObservationDescription,
|
||||
RTROIInterpretedType,
|
||||
ROIInterpreter,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function _setToolEnabledIfNotEnabled(toolName) {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
const { element } = enabledElement;
|
||||
const tool = cornerstoneTools.getToolForElement(element, toolName);
|
||||
|
||||
if (tool.mode !== 'enabled') {
|
||||
// If not already active or passive, set passive so contours render.
|
||||
cornerstoneTools.setToolEnabled(toolName);
|
||||
}
|
||||
|
||||
cornerstone.updateImage(element);
|
||||
});
|
||||
}
|
||||
|
||||
function _getOrCreateImageIdSpecificToolData(toolState, imageId, toolName) {
|
||||
if (toolState.hasOwnProperty(imageId) === false) {
|
||||
toolState[imageId] = {};
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[imageId];
|
||||
|
||||
// If we don't have tool state for this type of tool, add an empty object
|
||||
if (imageIdToolState.hasOwnProperty(toolName) === false) {
|
||||
imageIdToolState[toolName] = {
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
return imageIdToolState[toolName].data;
|
||||
}
|
||||
|
||||
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
||||
imageIdSopInstanceUidPairsEntry =>
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry.imageId;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = study.displaySets.filter(set => {
|
||||
return set.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||
);
|
||||
// TODO -> We could make check the instance list and see if any match?
|
||||
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
||||
// ^ Will that even happen?
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySets[0];
|
||||
|
||||
return referencedDisplaySet.images.map(image => {
|
||||
return {
|
||||
imageId: image.getImageId(),
|
||||
sopInstanceUID: image.getSOPInstanceUID(),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
||||
|
||||
import TOOL_NAMES from '../utils/toolNames';
|
||||
|
||||
// Cornerstone 3rd party dev kit imports
|
||||
const draw = importInternal('drawing/draw');
|
||||
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
||||
const getNewContext = importInternal('drawing/getNewContext');
|
||||
const BaseTool = importInternal('base/BaseTool');
|
||||
|
||||
/**
|
||||
* @class RTStructDisplayTool - Renders RTSTRUCT data in a read only manner (i.e. as an overlay).
|
||||
* @extends cornerstoneTools.BaseTool
|
||||
*/
|
||||
export default class RTStructDisplayTool extends BaseTool {
|
||||
constructor(props = {}) {
|
||||
const defaultProps = {
|
||||
mixins: ['enabledOrDisabledBinaryTool'],
|
||||
name: TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL,
|
||||
};
|
||||
|
||||
const initialProps = Object.assign(defaultProps, props);
|
||||
|
||||
super(initialProps);
|
||||
|
||||
this._rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||
}
|
||||
|
||||
renderToolData(evt) {
|
||||
const eventData = evt.detail;
|
||||
const rtstructModule = this._rtStructModule;
|
||||
|
||||
const toolState = getToolState(evt.currentTarget, this.name);
|
||||
|
||||
if (!toolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lineWidth, opacity } = rtstructModule.configuration;
|
||||
|
||||
// We have tool data for this element - iterate over each one and draw it
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
|
||||
for (let i = 0; i < toolState.data.length; i++) {
|
||||
const data = toolState.data[i];
|
||||
|
||||
const structureSet = rtstructModule.getters.structureSet(
|
||||
data.structureSetSeriesInstanceUid
|
||||
);
|
||||
|
||||
// Don't render if entire StructureSet is hidden.
|
||||
if (structureSet.visible === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ROIContourData = rtstructModule.getters.ROIContour(
|
||||
data.structureSetSeriesInstanceUid,
|
||||
data.ROINumber
|
||||
);
|
||||
|
||||
// Don't render if ROIContour is hidden.
|
||||
if (ROIContourData.visible === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const points = data.handles.points;
|
||||
|
||||
if (!points.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const colorArray = ROIContourData.colorArray;
|
||||
const color = `rgba(${colorArray[0]},${colorArray[1]},${
|
||||
colorArray[2]
|
||||
},${opacity})`;
|
||||
|
||||
lineWidth;
|
||||
|
||||
draw(context, context => {
|
||||
drawJoinedLines(
|
||||
context,
|
||||
eventData.element,
|
||||
points[points.length - 1],
|
||||
points,
|
||||
{
|
||||
color,
|
||||
lineWidth,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import TOOL_NAMES from '../../../utils/toolNames';
|
||||
|
||||
const globalImageIdSpecificToolStateManager =
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
|
||||
/**
|
||||
* getImageIdOfCenterFrameOfROIContour - Returns the imageId nearest to the center of the
|
||||
* volumes extent in the stack's Z direction which contains one of the ROIContour's Contours.
|
||||
* @param {string} structureSetSeriesInstanceUid The SeriesInstanceUID of the structure set.
|
||||
* @param {number} ROINumber The ROINumber of the region.
|
||||
* @param {string} imageIdsInStack The stack of imageIds.
|
||||
*
|
||||
* @returns The imageId
|
||||
*/
|
||||
export default function getImageIdOfCenterFrameOfROIContour(
|
||||
structureSetSeriesInstanceUid,
|
||||
ROINumber,
|
||||
imageIdsInStack
|
||||
) {
|
||||
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||
const imageIdIndicies = [];
|
||||
|
||||
for (let i = 0; i < imageIdsInStack.length; i++) {
|
||||
const imageId = imageIdsInStack[i];
|
||||
|
||||
const imageIdSpecificToolState = toolState[imageId];
|
||||
|
||||
if (
|
||||
!imageIdSpecificToolState ||
|
||||
!imageIdSpecificToolState[rtStructDisplayToolName] ||
|
||||
!imageIdSpecificToolState[rtStructDisplayToolName].data
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const toolData = imageIdSpecificToolState[rtStructDisplayToolName].data;
|
||||
|
||||
if (
|
||||
_toolDataContainsROIContour(
|
||||
toolData,
|
||||
structureSetSeriesInstanceUid,
|
||||
ROINumber
|
||||
)
|
||||
) {
|
||||
imageIdIndicies.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
const centerImageIdIndex =
|
||||
imageIdIndicies[Math.floor(imageIdIndicies.length / 2)];
|
||||
|
||||
return imageIdsInStack[centerImageIdIndex];
|
||||
}
|
||||
|
||||
function _toolDataContainsROIContour(
|
||||
toolData,
|
||||
structureSetSeriesInstanceUid,
|
||||
ROINumber
|
||||
) {
|
||||
return !!toolData.some(
|
||||
toolDataI =>
|
||||
toolDataI.structureSetSeriesInstanceUid ===
|
||||
structureSetSeriesInstanceUid && toolDataI.ROINumber === ROINumber
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* structureSetReferencesSeriesInstanceUid - Returns true if the structure set
|
||||
* references the given SeriesInstanceUID.
|
||||
* @param {*} StructureSet
|
||||
* @param {*} SeriesInstanceUID
|
||||
*/
|
||||
export default function structureSetReferencesSeriesInstanceUid(
|
||||
StructureSet,
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const { referencedSeriesSequence } = StructureSet;
|
||||
return referencedSeriesSequence.some(
|
||||
referencedSeries =>
|
||||
referencedSeries.SeriesInstanceUID ===
|
||||
SeriesInstanceUID
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import getImageIdOfCenterFrameOfROIContour from './lib/getImageIdOfCenterFrameOfROIContour';
|
||||
import structureSetReferencesSeriesInstanceUid from './lib/structureSetReferencesSeriesInstanceUid';
|
||||
|
||||
// We should put this as a helper somewhere as we are using it in mutliple places.
|
||||
function refreshViewport() {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
lineWidth: 3,
|
||||
opacity: 1.0,
|
||||
};
|
||||
|
||||
const state = {
|
||||
StructureSets: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a structure set to the module.
|
||||
* @param {Object} structureSetData The structure set data.
|
||||
*/
|
||||
function setStructureSet(structureSetData) {
|
||||
state.StructureSets.push(structureSetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the StructureSet with the given SeriesInstanceUID.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
*
|
||||
* @returns {Object} The StructureSet.
|
||||
*/
|
||||
function getStructureSet(SeriesInstanceUID) {
|
||||
return state.StructureSets.find(
|
||||
structureSet => structureSet.SeriesInstanceUID === SeriesInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ROI Contour with the given ROINumber on the StructureSet defined by the given
|
||||
* SeriesInstanceUID.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour to fetch.
|
||||
*/
|
||||
function getROIContour(SeriesInstanceUID, ROINumber) {
|
||||
const structureSet = getStructureSet(SeriesInstanceUID);
|
||||
|
||||
if (!structureSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
return structureSet.ROIContours.find(
|
||||
ROIContour => ROIContour.ROINumber === ROINumber
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the StructureSet.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
*/
|
||||
function setHideStructureSet(SeriesInstanceUID) {
|
||||
_setStructureSetVisible(SeriesInstanceUID, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the StructureSet.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
*/
|
||||
function setShowStructureSet(SeriesInstanceUID) {
|
||||
_setStructureSetVisible(SeriesInstanceUID, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the StructureSet.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||
*/
|
||||
function _setStructureSetVisible(SeriesInstanceUID, visible = true) {
|
||||
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||
|
||||
if (StructureSet) {
|
||||
StructureSet.visible = visible;
|
||||
|
||||
refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the StructureSet.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
*/
|
||||
function setToggleStructureSet(SeriesInstanceUID) {
|
||||
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||
|
||||
if (StructureSet) {
|
||||
StructureSet.visible = !StructureSet.visible;
|
||||
|
||||
refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the ROIContour.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||
*/
|
||||
function setHideROIContour(SeriesInstanceUID, ROINumber) {
|
||||
_setROIContourVisible(SeriesInstanceUID, ROINumber, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the ROIContour.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||
*/
|
||||
function setShowROIContour(SeriesInstanceUID, ROINumber) {
|
||||
_setROIContourVisible(SeriesInstanceUID, ROINumber, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the ROIContour.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||
*/
|
||||
function _setROIContourVisible(SeriesInstanceUID, ROINumber, visible = true) {
|
||||
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||
|
||||
if (ROIContour) {
|
||||
ROIContour.visible = visible;
|
||||
|
||||
refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the ROIContour.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||
*/
|
||||
function setToggleROIContour(SeriesInstanceUID, ROINumber) {
|
||||
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||
|
||||
if (ROIContour) {
|
||||
ROIContour.visible = !ROIContour.visible;
|
||||
|
||||
refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of StructureSets which reference the given SeriesInstanceUID.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID to check.
|
||||
*/
|
||||
function getStructuresSetsWhichReferenceSeriesInstanceUid(
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const { StructureSets } = state;
|
||||
return StructureSets.filter(StructureSet =>
|
||||
structureSetReferencesSeriesInstanceUid(
|
||||
StructureSet,
|
||||
SeriesInstanceUID
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
getters: {
|
||||
structureSet: getStructureSet,
|
||||
ROIContour: getROIContour,
|
||||
structuresSetsWhichReferenceSeriesInstanceUid: getStructuresSetsWhichReferenceSeriesInstanceUid,
|
||||
imageIdOfCenterFrameOfROIContour: getImageIdOfCenterFrameOfROIContour,
|
||||
},
|
||||
setters: {
|
||||
structureSet: setStructureSet,
|
||||
hideROIContour: setHideROIContour,
|
||||
showROIContour: setShowROIContour,
|
||||
toggleROIContour: setToggleROIContour,
|
||||
hideStructureSet: setHideStructureSet,
|
||||
showStructureSet: setShowStructureSet,
|
||||
toggleStructureSet: setToggleStructureSet,
|
||||
},
|
||||
state,
|
||||
configuration,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const TOOL_NAMES = {
|
||||
RTSTRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||
};
|
||||
|
||||
export default TOOL_NAMES;
|
||||
@@ -0,0 +1,91 @@
|
||||
export default function transformPointsToImagePlane(points, imagePlane) {
|
||||
// See Equation C.7.6.2.1-1 of the DICOM standard
|
||||
|
||||
const {
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
rowPixelSpacing: deltaI,
|
||||
columnPixelSpacing: deltaJ,
|
||||
imagePositionPatient,
|
||||
} = imagePlane;
|
||||
|
||||
const X = [rowCosines[0], rowCosines[1], rowCosines[2]];
|
||||
const Y = [columnCosines[0], columnCosines[1], columnCosines[2]];
|
||||
const S = [
|
||||
imagePositionPatient[0],
|
||||
imagePositionPatient[1],
|
||||
imagePositionPatient[2],
|
||||
];
|
||||
|
||||
// 9 sets of simulataneous equations to choose from, choose which set to solve
|
||||
// Based on the largest component of each direction cosine.
|
||||
// This avoids NaNs or floating point errors caused by dividing by very small numbers and ensures a safe mapping
|
||||
// when mapping between planes that are close to orthogonal.
|
||||
|
||||
let ix = 0;
|
||||
let iy = 0;
|
||||
let largestDirectionCosineMagnitude = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
// Find the element with the largest magnitude in each direction cosine vector.
|
||||
for (let i = 0; i < X.length; i++) {
|
||||
if (Math.abs(X[i]) > largestDirectionCosineMagnitude.x) {
|
||||
ix = i;
|
||||
largestDirectionCosineMagnitude.x = Math.abs(X[i]);
|
||||
}
|
||||
if (Math.abs(Y[i]) > largestDirectionCosineMagnitude.y) {
|
||||
iy = i;
|
||||
largestDirectionCosineMagnitude.y = Math.abs(Y[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const ci = {
|
||||
// Index of max elements in X and Y
|
||||
ix,
|
||||
iy,
|
||||
};
|
||||
|
||||
// Sanity Check
|
||||
const directionCosineMagnitude = {
|
||||
x: Math.pow(X[0], 2) + Math.pow(X[1], 2) + Math.pow(X[2], 2),
|
||||
y: Math.pow(Y[0], 2) + Math.pow(Y[1], 2) + Math.pow(Y[2], 2),
|
||||
};
|
||||
|
||||
if (directionCosineMagnitude.x < 0.99 || directionCosineMagnitude.y < 0.99) {
|
||||
throw Error(
|
||||
`Direction cosines do not sum to 1 in quadrature. There is likely a mistake in the DICOM metadata.` +
|
||||
`directionCosineMagnitudes: ${directionCosineMagnitude.x}, ${directionCosineMagnitude.y}`
|
||||
);
|
||||
}
|
||||
|
||||
// Fill in elements that won't change between points
|
||||
const c = [undefined, Y[ci.ix], X[ci.ix], undefined, X[ci.iy], Y[ci.iy]];
|
||||
|
||||
for (let pointI = 0; pointI < points.length; pointI++) {
|
||||
// Subtract imagePositionPatient from the coordinate
|
||||
const r = [
|
||||
points[pointI].x - S[0],
|
||||
points[pointI].y - S[1],
|
||||
points[pointI].z - S[2],
|
||||
];
|
||||
|
||||
// Set the variable terms in c.
|
||||
c[0] = r[ci.ix];
|
||||
c[3] = r[ci.iy];
|
||||
|
||||
// General case: Solves the two choosen simulataneous equations to go from the patient coordinate system to the imagePlane.
|
||||
const i =
|
||||
(c[0] - (c[1] * c[3]) / c[5]) /
|
||||
(c[2] * deltaI * (1 - (c[1] * c[4]) / (c[2] * c[5])));
|
||||
const j = (c[3] - c[4] * i * deltaI) / (c[5] * deltaJ);
|
||||
|
||||
// NOTE: Add (0.5, 0.5) to the coordinate, as PCS reference frame is with respect to the centre of the first pixel.
|
||||
points[pointI].x = i + 0.5;
|
||||
points[pointI].y = j + 0.5;
|
||||
delete points[pointI].z;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -3,6 +3,104 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.6...@ohif/extension-dicom-segmentation@0.3.0) (2020-06-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.5...@ohif/extension-dicom-segmentation@0.2.6) (2020-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.4...@ohif/extension-dicom-segmentation@0.2.5) (2020-05-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.3...@ohif/extension-dicom-segmentation@0.2.4) (2020-05-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.2...@ohif/extension-dicom-segmentation@0.2.3) (2020-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.1...@ohif/extension-dicom-segmentation@0.2.2) (2020-04-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.0...@ohif/extension-dicom-segmentation@0.2.1) (2020-04-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Dropdown not updating when selecting segment ([#1675](https://github.com/OHIF/Viewers/issues/1675)) ([20e366f](https://github.com/OHIF/Viewers/commit/20e366ffbd76781f48dc99c9f59e24b255b5e5df)), closes [#1669](https://github.com/OHIF/Viewers/issues/1669)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -30,14 +30,15 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"dcmjs": "^0.6.1",
|
||||
"cornerstone-tools": "4.15.1",
|
||||
"dcmjs": "^0.12.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
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;
|
||||
@@ -12,14 +12,6 @@
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-label {
|
||||
overflow: hidden;
|
||||
max-width: calc(100% - 50px);
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-info {
|
||||
display: inline-block;
|
||||
margin-top: 9px;
|
||||
@@ -62,3 +54,30 @@
|
||||
.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);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TableListItem, Icon } from '@ohif/ui';
|
||||
|
||||
@@ -17,48 +17,68 @@ ColoredCircle.propTypes = {
|
||||
color: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
const SegmentItem = ({ index, label, onClick, itemClass, color }) => (
|
||||
<div className="dcmseg-segment-item">
|
||||
<TableListItem
|
||||
key={index}
|
||||
itemKey={index}
|
||||
itemIndex={index}
|
||||
itemClass={itemClass}
|
||||
itemMeta={<ColoredCircle color={color} />}
|
||||
itemMetaClass="segment-color-section"
|
||||
onItemClick={onClick}
|
||||
>
|
||||
<div>
|
||||
<div className="segment-label" style={{ marginBottom: 4 }}>
|
||||
{label}
|
||||
</div>
|
||||
{false && <div className="segment-info">{'...'}</div>}
|
||||
{false && (
|
||||
<div className="segment-actions">
|
||||
<button
|
||||
className="btnAction"
|
||||
onClick={() => console.log('Relabelling...')}
|
||||
>
|
||||
<span style={{ marginRight: '4px' }}>
|
||||
<Icon name="edit" width="14px" height="14px" />
|
||||
</span>
|
||||
const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true, onVisibilityChange }) => {
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(visible);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div className="dcmseg-segment-item">
|
||||
<TableListItem
|
||||
key={index}
|
||||
itemKey={index}
|
||||
itemIndex={index}
|
||||
itemClass={itemClass}
|
||||
itemMeta={<ColoredCircle color={color} />}
|
||||
itemMetaClass="segment-color-section"
|
||||
onItemClick={onClick}
|
||||
>
|
||||
<div>
|
||||
<div className="segment-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="segment-info">{'...'}</div>}
|
||||
{false && (
|
||||
<div className="segment-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>
|
||||
<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>
|
||||
);
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableListItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentItem.propTypes = {
|
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
|
||||
+394
-176
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
@@ -28,63 +28,91 @@ const refreshViewport = () => {
|
||||
/**
|
||||
* SegmentationPanel component
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Array} props.studies
|
||||
* @param {Array} props.viewports - viewportSpecificData
|
||||
* @param {number} props.activeIndex - activeViewportIndex
|
||||
* @param {Array} props.studies - Studies data
|
||||
* @param {Array} props.viewports - Viewports data (viewportSpecificData)
|
||||
* @param {number} props.activeIndex - Active viewport index
|
||||
* @param {boolean} props.isOpen - Boolean that indicates if the panel is expanded
|
||||
* @param {Function} props.onSegmentItemClick - Segment click handler
|
||||
* @param {Function} props.onSegmentVisibilityChange - Segment visibiliy change handler
|
||||
* @param {Function} props.onConfigurationChange - Configuration change handler
|
||||
* @param {Function} props.activeContexts - List of active application contexts
|
||||
* @param {Function} props.contexts - List of available application contexts
|
||||
* @returns component
|
||||
*/
|
||||
const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
const SegmentationPanel = ({
|
||||
studies,
|
||||
viewports,
|
||||
activeIndex,
|
||||
isOpen,
|
||||
onSegmentItemClick,
|
||||
onSegmentVisibilityChange,
|
||||
onConfigurationChange,
|
||||
onDisplaySetLoadFailure,
|
||||
onSelectedSegmentationChange,
|
||||
activeContexts = [],
|
||||
contexts = {},
|
||||
}) => {
|
||||
const isVTK = () => activeContexts.includes(contexts.VTK);
|
||||
const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE);
|
||||
|
||||
/*
|
||||
* TODO: wrap get/set interactions with the cornerstoneTools
|
||||
* store with context to make these kind of things less blurry.
|
||||
*/
|
||||
const segmentationModule = cornerstoneTools.getModule('segmentation');
|
||||
const { configuration } = segmentationModule;
|
||||
const { configuration } = cornerstoneTools.getModule('segmentation');
|
||||
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
|
||||
|
||||
const [brushRadius, setBrushRadius] = useState(DEFAULT_BRUSH_RADIUS);
|
||||
|
||||
/* TODO: We shouldn't hardcode this color, in the future the SEG may set the colorLUT to whatever it wants. */
|
||||
const [brushColor, setBrushColor] = useState('rgba(221, 85, 85, 1)');
|
||||
const [selectedSegment, setSelectedSegment] = useState();
|
||||
const [showSegSettings, setShowSegSettings] = useState(false);
|
||||
const [selectedSegmentation, setSelectedSegmentation] = useState();
|
||||
|
||||
const viewport = viewports[activeIndex];
|
||||
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
} = viewport;
|
||||
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
const firstImageId = studyMetadata.getFirstImageId(displaySetInstanceUID);
|
||||
|
||||
/* CornerstoneTools */
|
||||
const [brushStackState, setBrushStackState] = useState(
|
||||
segmentationModule.state.series[firstImageId]
|
||||
);
|
||||
/*
|
||||
* TODO: We shouldn't hardcode brushColor color, in the future
|
||||
* the SEG may set the colorLUT to whatever it wants.
|
||||
*/
|
||||
const [state, setState] = useState({
|
||||
brushRadius: DEFAULT_BRUSH_RADIUS,
|
||||
brushColor:
|
||||
'rgba(221, 85, 85, 1)',
|
||||
selectedSegment: null,
|
||||
selectedSegmentation: null,
|
||||
showSegmentationSettings: false,
|
||||
brushStackState: null,
|
||||
labelmapList: [],
|
||||
segmentList: [],
|
||||
cachedSegmentsProperties: [],
|
||||
isLoading: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setShowSegSettings(showSegSettings && !isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setBrushStackState(segmentationModule.state.series[firstImageId]);
|
||||
}, [studies, viewports, activeIndex, firstImageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (brushStackState) {
|
||||
setSelectedSegmentation(brushStackState.activeLabelmapIndex);
|
||||
}
|
||||
|
||||
const labelmapModifiedHandler = event => {
|
||||
log.warn('labelmap modified', event);
|
||||
setBrushStackState(segmentationModule.state.series[firstImageId]);
|
||||
log.warn('Segmentation Panel: labelmap modified', event);
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const firstImageId = studyMetadata.getFirstImageId(
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
updateState('brushStackState', module.state.series[firstImageId]);
|
||||
};
|
||||
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app that depends on segs to be loaded.
|
||||
*
|
||||
* Currently we are using a non-ideal implementation through a custom event to notify the segmentation panel
|
||||
* or other components that could rely on loaded segmentations that
|
||||
* the segments were loaded so that e.g. when the user opens the panel
|
||||
* before the segments are fully loaded, the panel can subscribe to this custom event
|
||||
* and update itself with the new segments.
|
||||
*
|
||||
* This limitation is due to the fact that the cs segmentation module is an object (which will be
|
||||
* updated after the segments are loaded) that React its not aware of its changes
|
||||
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||
*
|
||||
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||
* allows us to easily watch the module or the segmentations loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
document.addEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations);
|
||||
|
||||
/*
|
||||
* These are specific to each element;
|
||||
* Need to iterate cornerstone-tools tracked enabled elements?
|
||||
@@ -98,6 +126,7 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations);
|
||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||
enabledElement.removeEventListener(
|
||||
'cornerstonetoolslabelmapmodified',
|
||||
@@ -105,144 +134,317 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
)
|
||||
);
|
||||
};
|
||||
});
|
||||
}, [activeIndex, viewports]);
|
||||
|
||||
if (!brushStackState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelmap3D =
|
||||
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||
|
||||
/*
|
||||
* 2. UseEffect to update state? or to a least trigger a re-render
|
||||
* 4. Toggle visibility of labelmap?
|
||||
* 5. Toggle visibility of seg?
|
||||
*
|
||||
* If the port is cornerstone, just need to call a re-render.
|
||||
* If the port is vtkjs, its a bit more tricky as we now need to create a new
|
||||
*/
|
||||
|
||||
const getLabelmapList = () => {
|
||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
const refreshSegmentations = useCallback(() => {
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const firstImageId = studyMetadata.getFirstImageId(
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
const brushStackState = module.state.series[firstImageId];
|
||||
if (brushStackState) {
|
||||
const labelmap3D =
|
||||
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||
const labelmapList = getLabelmapList(
|
||||
brushStackState,
|
||||
firstImageId,
|
||||
activeViewport
|
||||
);
|
||||
const segmentList = getSegmentList(
|
||||
labelmap3D,
|
||||
firstImageId,
|
||||
brushStackState
|
||||
);
|
||||
setState(state => ({
|
||||
...state,
|
||||
brushStackState,
|
||||
selectedSegmentation: brushStackState.activeLabelmapIndex,
|
||||
labelmapList,
|
||||
segmentList,
|
||||
}));
|
||||
} else {
|
||||
setState(state => ({
|
||||
...state,
|
||||
labelmapList: [],
|
||||
segmentList: [],
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
viewports,
|
||||
activeIndex,
|
||||
state.isLoading
|
||||
]);
|
||||
|
||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
||||
useEffect(() => {
|
||||
refreshSegmentations();
|
||||
}, [viewports, activeIndex, state.selectedSegmentation, activeContexts, state.isLoading]);
|
||||
|
||||
/* Map to display representation */
|
||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||
const isActiveLabelmap =
|
||||
labelmapIndex === brushStackState.activeLabelmapIndex;
|
||||
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||
const displayTime = date.format('h:mm:ss a');
|
||||
const displayDescription = displaySet.SeriesDescription;
|
||||
/* Handle open/closed panel behaviour */
|
||||
useEffect(() => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
showSegmentationSettings: state.showSegmentationSettings && !isOpen,
|
||||
}));
|
||||
}, [isOpen]);
|
||||
|
||||
return {
|
||||
value: labelmapIndex,
|
||||
title: displayDescription,
|
||||
description: displayDate,
|
||||
onClick: async () => {
|
||||
const activatedLabelmapIndex = await _setActiveLabelmap(
|
||||
viewport,
|
||||
studies,
|
||||
displaySet,
|
||||
firstImageId,
|
||||
brushStackState.activeLabelmapIndex
|
||||
);
|
||||
setSelectedSegmentation(activatedLabelmapIndex);
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
const getLabelmapList = useCallback(
|
||||
(brushStackState, firstImageId, activeViewport) => {
|
||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||
activeViewport.StudyInstanceUID,
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const labelmapList = getLabelmapList();
|
||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
||||
|
||||
const segmentList = [];
|
||||
/* Map to display representation */
|
||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||
const isActiveLabelmap =
|
||||
labelmapIndex === brushStackState.activeLabelmapIndex;
|
||||
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||
const displayTime = date.format('h:mm:ss a');
|
||||
const displayDescription = displaySet.SeriesDescription;
|
||||
|
||||
if (labelmap3D) {
|
||||
/*
|
||||
* Newly created segments have no `meta`
|
||||
* So we instead build a list of all segment indexes in use
|
||||
* Then find any associated metadata
|
||||
*/
|
||||
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
|
||||
.reduce((acc, labelmap2D) => {
|
||||
if (labelmap2D) {
|
||||
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
||||
return {
|
||||
value: labelmapIndex,
|
||||
title: displayDescription,
|
||||
description: displayDate,
|
||||
onClick: async () => {
|
||||
const activatedLabelmapIndex = await _setActiveLabelmap(
|
||||
activeViewport,
|
||||
studies,
|
||||
displaySet,
|
||||
firstImageId,
|
||||
brushStackState.activeLabelmapIndex,
|
||||
() => onSelectedSegmentationChange(),
|
||||
onDisplaySetLoadFailure
|
||||
);
|
||||
updateState('selectedSegmentation', activatedLabelmapIndex);
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[studies]
|
||||
);
|
||||
|
||||
for (let i = 0; i < segmentIndexes.length; i++) {
|
||||
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
||||
acc.push(segmentIndexes[i]);
|
||||
const getSegmentList = useCallback(
|
||||
(labelmap3D, firstImageId, brushStackState) => {
|
||||
/*
|
||||
* Newly created segments have no `meta`
|
||||
* So we instead build a list of all segment indexes in use
|
||||
* Then find any associated metadata
|
||||
*/
|
||||
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
|
||||
.reduce((acc, labelmap2D) => {
|
||||
if (labelmap2D) {
|
||||
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
||||
|
||||
for (let i = 0; i < segmentIndexes.length; i++) {
|
||||
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
||||
acc.push(segmentIndexes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const colorLutTable =
|
||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
|
||||
|
||||
const segmentList = [];
|
||||
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
|
||||
const segmentIndex = uniqueSegmentIndexes[i];
|
||||
|
||||
const color = colorLutTable[segmentIndex];
|
||||
let segmentLabel = '(unlabeled)';
|
||||
let segmentNumber = segmentIndex;
|
||||
|
||||
/* Meta */
|
||||
if (hasLabelmapMeta) {
|
||||
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
|
||||
|
||||
if (segmentMeta) {
|
||||
segmentNumber = segmentMeta.SegmentNumber;
|
||||
segmentLabel = segmentMeta.SegmentLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => a - b);
|
||||
const sameSegment = state.selectedSegment === segmentNumber;
|
||||
const setCurrentSelectedSegment = () => {
|
||||
_setActiveSegment(
|
||||
firstImageId,
|
||||
segmentNumber,
|
||||
labelmap3D.activeSegmentIndex
|
||||
);
|
||||
updateState('selectedSegment', sameSegment ? null : segmentNumber);
|
||||
|
||||
const colorLutTable =
|
||||
segmentationModule.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
|
||||
const validIndexList = [];
|
||||
labelmap3D.labelmaps2D.forEach((labelMap2D, index) => {
|
||||
if (labelMap2D.segmentsOnLabelmap.includes(segmentNumber)) {
|
||||
validIndexList.push(index);
|
||||
}
|
||||
});
|
||||
const avg = array => array.reduce((a, b) => a + b) / array.length;
|
||||
const average = avg(validIndexList);
|
||||
const closest = validIndexList.reduce((prev, curr) => {
|
||||
return Math.abs(curr - average) < Math.abs(prev - average)
|
||||
? curr
|
||||
: prev;
|
||||
});
|
||||
|
||||
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
|
||||
const segmentIndex = uniqueSegmentIndexes[i];
|
||||
if (isCornerstone()) {
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
const element = enabledElements[activeIndex].element;
|
||||
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||
|
||||
const color = colorLutTable[segmentIndex];
|
||||
let segmentLabel = '(unlabeled)';
|
||||
let segmentNumber = segmentIndex;
|
||||
if (!toolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Meta */
|
||||
if (hasLabelmapMeta) {
|
||||
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
|
||||
const imageIds = toolState.data[0].imageIds;
|
||||
const imageId = imageIds[closest];
|
||||
const frameIndex = imageIds.indexOf(imageId);
|
||||
|
||||
if (segmentMeta) {
|
||||
segmentNumber = segmentMeta.SegmentNumber;
|
||||
segmentLabel = segmentMeta.SegmentLabel;
|
||||
const SOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
const StudyInstanceUID = cornerstone.metaData.get(
|
||||
'StudyInstanceUID',
|
||||
imageId
|
||||
);
|
||||
|
||||
onSegmentItemClick({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex: activeIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (isVTK()) {
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const allDisplaySets = studyMetadata.getDisplaySets();
|
||||
const currentDisplaySet = allDisplaySets.find(
|
||||
displaySet =>
|
||||
displaySet.displaySetInstanceUID ===
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const frame = labelmap3D.labelmaps2D[closest];
|
||||
|
||||
onSegmentItemClick({
|
||||
studies,
|
||||
StudyInstanceUID: currentDisplaySet.StudyInstanceUID,
|
||||
displaySetInstanceUID: currentDisplaySet.displaySetInstanceUID,
|
||||
SOPClassUID: viewports[activeIndex].sopClassUIDs[0],
|
||||
SOPInstanceUID: currentDisplaySet.SOPInstanceUID,
|
||||
segmentNumber,
|
||||
frameIndex: closest,
|
||||
frame,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isSegmentVisible = () => {
|
||||
return !labelmap3D.segmentsHidden[segmentIndex];
|
||||
};
|
||||
|
||||
const toggleSegmentVisibility = () => {
|
||||
const segmentsHidden = labelmap3D.segmentsHidden;
|
||||
segmentsHidden[segmentIndex] = !segmentsHidden[segmentIndex];
|
||||
return !segmentsHidden[segmentIndex];
|
||||
};
|
||||
|
||||
const cachedSegmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||
let visible = isSegmentVisible();
|
||||
if (cachedSegmentProperties && cachedSegmentProperties.visible !== visible) {
|
||||
toggleSegmentVisibility();
|
||||
}
|
||||
|
||||
segmentList.push(
|
||||
<SegmentItem
|
||||
key={segmentNumber}
|
||||
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
||||
onClick={() => setCurrentSelectedSegment()}
|
||||
label={segmentLabel}
|
||||
index={segmentNumber}
|
||||
color={color}
|
||||
visible={visible}
|
||||
onVisibilityChange={newVisibility => {
|
||||
if (isCornerstone()) {
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
const element = enabledElements[activeIndex].element;
|
||||
module.setters.toggleSegmentVisibility(
|
||||
element,
|
||||
segmentNumber,
|
||||
brushStackState.activeLabelmapIndex
|
||||
);
|
||||
}
|
||||
|
||||
if (isVTK()) {
|
||||
onSegmentVisibilityChange(segmentNumber, newVisibility);
|
||||
}
|
||||
|
||||
updateCachedSegmentsProperties(segmentNumber, { visible: newVisibility });
|
||||
refreshViewport();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sameSegment = selectedSegment === segmentNumber;
|
||||
const setCurrentSelectedSegment = () => {
|
||||
_setActiveSegment(
|
||||
firstImageId,
|
||||
segmentNumber,
|
||||
labelmap3D.activeSegmentIndex
|
||||
);
|
||||
setSelectedSegment(sameSegment ? null : segmentNumber);
|
||||
};
|
||||
return segmentList;
|
||||
|
||||
segmentList.push(
|
||||
<SegmentItem
|
||||
key={segmentNumber}
|
||||
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
||||
onClick={setCurrentSelectedSegment}
|
||||
label={segmentLabel}
|
||||
index={segmentNumber}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
}
|
||||
/*
|
||||
* Let's iterate over segmentIndexes ^ above
|
||||
* If meta has a match, use it to show info
|
||||
* If now, add "no-meta" class
|
||||
* Show default name
|
||||
*/
|
||||
},
|
||||
[activeIndex, onSegmentItemClick, state.selectedSegment, state.isLoading]
|
||||
);
|
||||
|
||||
/*
|
||||
* Let's iterate over segmentIndexes ^ above
|
||||
* If meta has a match, use it to show info
|
||||
* If now, add "no-meta" class
|
||||
* Show default name
|
||||
*/
|
||||
}
|
||||
const updateCachedSegmentsProperties = (segmentNumber, properties) => {
|
||||
const segmentsProperties = state.cachedSegmentsProperties;
|
||||
const segmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||
|
||||
segmentsProperties[segmentNumber] =
|
||||
segmentProperties ?
|
||||
{ ...segmentProperties, ...properties } :
|
||||
properties;
|
||||
|
||||
updateState('cachedSegmentsProperties', segmentsProperties);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateState('cachedSegmentsProperties', []);
|
||||
}, [activeContexts]);
|
||||
|
||||
const updateState = (field, value) => {
|
||||
setState(state => ({ ...state, [field]: value }));
|
||||
};
|
||||
|
||||
const updateBrushSize = evt => {
|
||||
const updatedRadius = Number(evt.target.value);
|
||||
|
||||
if (updatedRadius !== brushRadius) {
|
||||
setBrushRadius(updatedRadius);
|
||||
segmentationModule.setters.radius(updatedRadius);
|
||||
updateState('brushRadius', updatedRadius);
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
module.setters.radius(updatedRadius);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -251,36 +453,35 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
if (labelmap3D.activeSegmentIndex > 1) {
|
||||
labelmap3D.activeSegmentIndex--;
|
||||
}
|
||||
setSelectedSegment(labelmap3D.activeSegmentIndex);
|
||||
updateState('selectedSegment', labelmap3D.activeSegmentIndex);
|
||||
updateActiveSegmentColor();
|
||||
};
|
||||
|
||||
const incrementSegment = event => {
|
||||
event.preventDefault();
|
||||
labelmap3D.activeSegmentIndex++;
|
||||
setSelectedSegment(labelmap3D.activeSegmentIndex);
|
||||
updateState('selectedSegment', labelmap3D.activeSegmentIndex);
|
||||
updateActiveSegmentColor();
|
||||
};
|
||||
|
||||
const updateActiveSegmentColor = () => {
|
||||
const color = getActiveSegmentColor();
|
||||
setBrushColor(color);
|
||||
updateState('brushColor', color);
|
||||
};
|
||||
|
||||
const getActiveSegmentColor = () => {
|
||||
if (!brushStackState) {
|
||||
if (!state.brushStackState) {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
|
||||
const colorLutTable =
|
||||
segmentationModule.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const colorLutTable = module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
const color = colorLutTable[labelmap3D.activeSegmentIndex];
|
||||
|
||||
return `rgba(${color.join(',')})`;
|
||||
};
|
||||
|
||||
const updateConfiguration = newConfiguration => {
|
||||
/* Supported configuration */
|
||||
configuration.renderFill = newConfiguration.renderFill;
|
||||
configuration.renderOutline = newConfiguration.renderOutline;
|
||||
configuration.shouldRenderInactiveLabelmaps =
|
||||
@@ -290,14 +491,17 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
configuration.outlineWidth = newConfiguration.outlineWidth;
|
||||
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
|
||||
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
|
||||
onConfigurationChange(newConfiguration);
|
||||
refreshViewport();
|
||||
};
|
||||
|
||||
if (showSegSettings) {
|
||||
const disabledConfigurationFields = ['outlineAlpha', 'shouldRenderInactiveLabelmaps'];
|
||||
if (state.showSegmentationSettings) {
|
||||
return (
|
||||
<SegmentationSettings
|
||||
disabledFields={isVTK() ? disabledConfigurationFields : []}
|
||||
configuration={configuration}
|
||||
onBack={() => setShowSegSettings(false)}
|
||||
onBack={() => updateState('showSegmentationSettings', false)}
|
||||
onChange={updateConfiguration}
|
||||
/>
|
||||
);
|
||||
@@ -309,13 +513,13 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
name="cog"
|
||||
width="25px"
|
||||
height="25px"
|
||||
onClick={() => setShowSegSettings(true)}
|
||||
onClick={() => updateState('showSegmentationSettings', true)}
|
||||
/>
|
||||
{false && (
|
||||
<form className="selector-form">
|
||||
<BrushColorSelector
|
||||
defaultColor={brushColor}
|
||||
index={labelmap3D.activeSegmentIndex}
|
||||
index={state.selectedSegment}
|
||||
onNext={incrementSegment}
|
||||
onPrev={decrementSegment}
|
||||
/>
|
||||
@@ -330,18 +534,18 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
<h3>Segmentations</h3>
|
||||
<div className="segmentations">
|
||||
<SegmentationSelect
|
||||
value={
|
||||
labelmapList.find(i => i.value === selectedSegmentation) || null
|
||||
}
|
||||
value={state.labelmapList.find(
|
||||
i => i.value === state.selectedSegmentation
|
||||
)}
|
||||
formatOptionLabel={SegmentationItem}
|
||||
options={labelmapList}
|
||||
options={state.labelmapList}
|
||||
/>
|
||||
</div>
|
||||
<ScrollableArea>
|
||||
<TableList
|
||||
customHeader={<SegmentsHeader count={segmentList.length} />}
|
||||
customHeader={<SegmentsHeader count={state.segmentList.length} />}
|
||||
>
|
||||
{segmentList}
|
||||
{state.segmentList}
|
||||
</TableList>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
@@ -373,6 +577,7 @@ SegmentationPanel.propTypes = {
|
||||
}),
|
||||
activeIndex: PropTypes.number.isRequired,
|
||||
studies: PropTypes.array.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
SegmentationPanel.defaultProps = {};
|
||||
|
||||
@@ -416,26 +621,39 @@ const _setActiveLabelmap = async (
|
||||
studies,
|
||||
displaySet,
|
||||
firstImageId,
|
||||
activeLabelmapIndex
|
||||
activeLabelmapIndex,
|
||||
callback = () => { },
|
||||
onDisplaySetLoadFailure
|
||||
) => {
|
||||
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
||||
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
||||
return;
|
||||
return displaySet.labelmapIndex;
|
||||
}
|
||||
|
||||
if (!displaySet.isLoaded) {
|
||||
// What props does this expect `viewportSpecificData` to have?
|
||||
// TODO: Should this return the `labelmapIndex`?
|
||||
await displaySet.load(viewportSpecificData, studies);
|
||||
|
||||
const loadPromise = displaySet.load(viewportSpecificData, studies);
|
||||
|
||||
loadPromise.catch(error => {
|
||||
onDisplaySetLoadFailure(error);
|
||||
|
||||
// Return old index.
|
||||
return activeLabelmapIndex;
|
||||
});
|
||||
|
||||
await loadPromise;
|
||||
}
|
||||
|
||||
const { state } = cornerstoneTools.getModule('segmentation');
|
||||
const brushStackState = state.series[firstImageId];
|
||||
|
||||
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
|
||||
|
||||
refreshViewport();
|
||||
|
||||
callback();
|
||||
|
||||
return displaySet.labelmapIndex;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ const defaultColor = computedstyle.getPropertyValue('--default-color');
|
||||
const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
|
||||
|
||||
const segmentationSelectStyles = {
|
||||
singleValue: (base, state) => ({
|
||||
...base,
|
||||
width: '100%'
|
||||
}),
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
cursor: 'pointer',
|
||||
|
||||
+26
-22
@@ -4,7 +4,7 @@ import { Range } from '@ohif/ui';
|
||||
|
||||
import './SegmentationSettings.css';
|
||||
|
||||
const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
||||
const SegmentationSettings = ({ configuration, onBack, onChange, disabledFields = [] }) => {
|
||||
const [state, setState] = useState({
|
||||
renderFill: configuration.renderFill,
|
||||
renderOutline: configuration.renderOutline,
|
||||
@@ -70,28 +70,32 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
||||
/>
|
||||
{state.renderOutline && (
|
||||
<>
|
||||
<CustomRange
|
||||
value={state.outlineAlpha * 100}
|
||||
label="Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
|
||||
/>
|
||||
<CustomRange
|
||||
value={state.outlineWidth}
|
||||
label="Width"
|
||||
showValue
|
||||
step={1}
|
||||
min={0}
|
||||
max={5}
|
||||
onChange={event => save('outlineWidth', parseInt(event.target.value))}
|
||||
/>
|
||||
{!disabledFields.includes('outlineAlpha') && (
|
||||
<CustomRange
|
||||
value={state.outlineAlpha * 100}
|
||||
label="Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
{!disabledFields.includes('outlineWidth') && (
|
||||
<CustomRange
|
||||
value={state.outlineWidth}
|
||||
label="Width"
|
||||
showValue
|
||||
step={1}
|
||||
min={0}
|
||||
max={5}
|
||||
onChange={event => save('outlineWidth', parseInt(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{(state.renderFill || state.renderOutline) && (
|
||||
{(state.renderFill || state.renderOutline) && !disabledFields.includes('shouldRenderInactiveLabelmaps') && (
|
||||
<div
|
||||
className="settings-group"
|
||||
style={{ marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0 }}
|
||||
@@ -103,7 +107,7 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
||||
/>
|
||||
{state.shouldRenderInactiveLabelmaps && (
|
||||
<>
|
||||
{state.renderFill && (
|
||||
{state.renderFill && !disabledFields.includes('fillAlphaInactive') && (
|
||||
<CustomRange
|
||||
label="Fill Opacity"
|
||||
showPercentage
|
||||
@@ -114,7 +118,7 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
||||
onChange={event => save('fillAlphaInactive', toFloat(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
{state.renderOutline && (
|
||||
{state.renderOutline && !disabledFields.includes('outlineAlphaInactive') && (
|
||||
<CustomRange
|
||||
label="Outline Opacity"
|
||||
showPercentage
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
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);
|
||||
|
||||
export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
||||
// same SOP Class.
|
||||
return {
|
||||
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);
|
||||
};
|
||||
|
||||
return segDisplaySet;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import init from './init.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import panelModule from './panelModule.js';
|
||||
import sopClassHandlerModule from './OHIFDicomSegSopClassHandler.js';
|
||||
import getSopClassHandlerModule from './getOHIFDicomSegSopClassHandler.js';
|
||||
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -21,10 +23,97 @@ export default {
|
||||
getToolbarModule({ servicesManager }) {
|
||||
return toolbarModule;
|
||||
},
|
||||
getPanelModule({ servicesManager }) {
|
||||
return panelModule;
|
||||
},
|
||||
getSopClassHandlerModule({ servicesManager }) {
|
||||
return sopClassHandlerModule;
|
||||
getPanelModule({ commandsManager, api, servicesManager }) {
|
||||
const { UINotificationService } = servicesManager;
|
||||
|
||||
const ExtendedSegmentationPanel = props => {
|
||||
const { activeContexts } = api.hooks.useAppContext();
|
||||
|
||||
const onDisplaySetLoadFailureHandler = error => {
|
||||
UINotificationService.show({
|
||||
title: 'DICOM Segmentation Loader',
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
autoClose: false,
|
||||
});
|
||||
};
|
||||
|
||||
const segmentItemClickHandler = data => {
|
||||
commandsManager.runCommand('jumpToImage', data);
|
||||
commandsManager.runCommand('jumpToSlice', data);
|
||||
};
|
||||
|
||||
const onSegmentVisibilityChangeHandler = (segmentNumber, visible) => {
|
||||
commandsManager.runCommand('setSegmentConfiguration', {
|
||||
segmentNumber,
|
||||
visible,
|
||||
});
|
||||
};
|
||||
|
||||
const onConfigurationChangeHandler = configuration => {
|
||||
commandsManager.runCommand('setSegmentationConfiguration', {
|
||||
globalOpacity: configuration.fillAlpha,
|
||||
outlineThickness: configuration.outlineWidth,
|
||||
renderOutline: configuration.renderOutline,
|
||||
visible: configuration.renderFill,
|
||||
});
|
||||
};
|
||||
|
||||
const onSelectedSegmentationChangeHandler = () => {
|
||||
commandsManager.runCommand('requestNewSegmentation');
|
||||
};
|
||||
|
||||
return (
|
||||
<SegmentationPanel
|
||||
{...props}
|
||||
activeContexts={activeContexts}
|
||||
contexts={api.contexts}
|
||||
onSegmentItemClick={segmentItemClickHandler}
|
||||
onSegmentVisibilityChange={onSegmentVisibilityChangeHandler}
|
||||
onConfigurationChange={onConfigurationChangeHandler}
|
||||
onSelectedSegmentationChange={onSelectedSegmentationChangeHandler}
|
||||
onDisplaySetLoadFailure={onDisplaySetLoadFailureHandler}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'Segmentations',
|
||||
target: 'segmentation-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 (series.Modality === 'SEG') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'segmentation-panel',
|
||||
component: ExtendedSegmentationPanel,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER'],
|
||||
};
|
||||
},
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import dcmjs from 'dcmjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
@@ -34,28 +34,58 @@ export default async function loadSegmentation(
|
||||
referencedDisplaySet.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const results = _parseSeg(segArrayBuffer, imageIds);
|
||||
return new Promise((resolve, reject) => {
|
||||
let results;
|
||||
|
||||
if (!results) {
|
||||
throw new Error('Fractional segmentations are not yet supported');
|
||||
}
|
||||
try {
|
||||
results = _parseSeg(segArrayBuffer, imageIds);
|
||||
} catch (error) {
|
||||
segDisplaySet.isLoaded = false;
|
||||
reject(error);
|
||||
}
|
||||
|
||||
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
|
||||
const { setters } = cornerstoneTools.getModule('segmentation');
|
||||
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
|
||||
const { setters } = cornerstoneTools.getModule('segmentation');
|
||||
|
||||
// TODO: Could define a color LUT based on colors in the SEG.
|
||||
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
|
||||
// TODO: Could define a color LUT based on colors in the SEG.
|
||||
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
|
||||
const colorLUTIndex = _makeColorLUTAndGetIndex(segMetadata);
|
||||
|
||||
setters.labelmap3DByFirstImageId(
|
||||
imageIds[0],
|
||||
labelmapBuffer,
|
||||
labelmapIndex,
|
||||
segMetadata,
|
||||
imageIds.length,
|
||||
segmentsOnFrame
|
||||
);
|
||||
setters.labelmap3DByFirstImageId(
|
||||
imageIds[0],
|
||||
labelmapBuffer,
|
||||
labelmapIndex,
|
||||
segMetadata,
|
||||
imageIds.length,
|
||||
segmentsOnFrame,
|
||||
colorLUTIndex
|
||||
);
|
||||
|
||||
segDisplaySet.labelmapIndex = labelmapIndex;
|
||||
segDisplaySet.labelmapIndex = labelmapIndex;
|
||||
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app that depends on segs to be loaded.
|
||||
*
|
||||
* Currently we are using a non-ideal implementation through a custom event to notify the segmentation panel
|
||||
* or other components that could rely on loaded segmentations that
|
||||
* the segments were loaded so that e.g. when the user opens the panel
|
||||
* before the segments are fully loaded, the panel can subscribe to this custom event
|
||||
* and update itself with the new segments.
|
||||
*
|
||||
* This limitation is due to the fact that the cs segmentation module is an object (which will be
|
||||
* updated after the segments are loaded) that React its not aware of its changes
|
||||
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||
*
|
||||
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||
* allows us to easily watch the module or the segmentations loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
console.log('Segmentation loaded.');
|
||||
const event = new CustomEvent('extensiondicomsegmentationsegloaded');
|
||||
document.dispatchEvent(event);
|
||||
|
||||
resolve(labelmapIndex);
|
||||
});
|
||||
}
|
||||
|
||||
function _getNextLabelmapIndex(firstImageId) {
|
||||
@@ -79,6 +109,69 @@ function _getNextLabelmapIndex(firstImageId) {
|
||||
return labelmapIndex;
|
||||
}
|
||||
|
||||
function _makeColorLUTAndGetIndex(segMetadata) {
|
||||
const { setters, state } = cornerstoneTools.getModule('segmentation');
|
||||
const { colorLutTables } = state;
|
||||
const colorLUTIndex = _getNextColorLUTIndex();
|
||||
|
||||
const { data } = segMetadata;
|
||||
|
||||
if (
|
||||
!data.some(
|
||||
segment =>
|
||||
segment &&
|
||||
(segment.ROIDisplayColor || segment.RecommendedDisplayCIELabValue)
|
||||
)
|
||||
) {
|
||||
// Use default cornerstoneTools colorLUT.
|
||||
return 0;
|
||||
}
|
||||
|
||||
const colorLUT = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const segment = data[i];
|
||||
if (!segment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { ROIDisplayColor, RecommendedDisplayCIELabValue } = segment;
|
||||
|
||||
if (RecommendedDisplayCIELabValue) {
|
||||
const rgb = dcmjs.data.Colors.dicomlab2RGB(
|
||||
RecommendedDisplayCIELabValue
|
||||
).map(x => Math.round(x * 255));
|
||||
|
||||
colorLUT[i] = [...rgb, 255];
|
||||
} else if (ROIDisplayColor) {
|
||||
colorLUT[i] = [...ROIDisplayColor, 255];
|
||||
} else {
|
||||
colorLUT[i] = [...colorLutTables[0][i]];
|
||||
}
|
||||
}
|
||||
|
||||
colorLUT.shift();
|
||||
setters.colorLUT(colorLUTIndex, colorLUT);
|
||||
|
||||
return colorLUTIndex;
|
||||
}
|
||||
|
||||
function _getNextColorLUTIndex() {
|
||||
const { state } = cornerstoneTools.getModule('segmentation');
|
||||
const { colorLutTables } = state;
|
||||
|
||||
let colorLUTIndex = colorLutTables.length;
|
||||
|
||||
for (let i = 0; i < colorLutTables.length; i++) {
|
||||
if (!colorLutTables[i]) {
|
||||
colorLUTIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return colorLUTIndex;
|
||||
}
|
||||
|
||||
function _parseSeg(arrayBuffer, imageIds) {
|
||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||
imageIds,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
||||
|
||||
export default {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'Segmentations',
|
||||
target: 'segmentation-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 (series.Modality === 'SEG') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'segmentation-panel',
|
||||
component: SegmentationPanel,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER'],
|
||||
};
|
||||
@@ -3,6 +3,218 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.6...@ohif/extension-vtk@1.6.0) (2020-06-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.5...@ohif/extension-vtk@1.5.6) (2020-05-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.4...@ohif/extension-vtk@1.5.5) (2020-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.3...@ohif/extension-vtk@1.5.4) (2020-05-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.2...@ohif/extension-vtk@1.5.3) (2020-05-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.1...@ohif/extension-vtk@1.5.2) (2020-05-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.0...@ohif/extension-vtk@1.5.1) (2020-04-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.4.1...@ohif/extension-vtk@1.5.0) (2020-04-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.4.0...@ohif/extension-vtk@1.4.1) (2020-04-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.14...@ohif/extension-vtk@1.4.0) (2020-04-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.13...@ohif/extension-vtk@1.3.14) (2020-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.12...@ohif/extension-vtk@1.3.13) (2020-04-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.11...@ohif/extension-vtk@1.3.12) (2020-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.10...@ohif/extension-vtk@1.3.11) (2020-04-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Revert "refactor: Reduce bundle size ([#1575](https://github.com/OHIF/Viewers/issues/1575))" ([#1622](https://github.com/OHIF/Viewers/issues/1622)) ([d21af3f](https://github.com/OHIF/Viewers/commit/d21af3f133492fa31492413b8782936c9ff18b44))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.9...@ohif/extension-vtk@1.3.10) (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.8...@ohif/extension-vtk@1.3.9) (2020-04-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.7...@ohif/extension-vtk@1.3.8) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.6...@ohif/extension-vtk@1.3.7) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.5...@ohif/extension-vtk@1.3.6) (2020-04-01)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.4...@ohif/extension-vtk@1.3.5) (2020-03-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.3...@ohif/extension-vtk@1.3.4) (2020-03-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.2...@ohif/extension-vtk@1.3.3) (2020-03-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.1...@ohif/extension-vtk@1.3.2) (2020-03-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.0...@ohif/extension-vtk@1.3.1) (2020-03-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.24...@ohif/extension-vtk@1.3.0) (2020-03-13)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.3.0",
|
||||
"version": "1.6.0",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -33,9 +33,9 @@
|
||||
"@ohif/i18n": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.10.1",
|
||||
"cornerstone-tools": "4.15.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "^0.12.2",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
@@ -50,13 +50,13 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-vtkjs-viewport": "^0.8.3"
|
||||
"react-vtkjs-viewport": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.6.0",
|
||||
"@ohif/ui": "^1.4.0",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"@ohif/core": "^2.9.6",
|
||||
"@ohif/ui": "^1.5.0",
|
||||
"cornerstone-tools": "4.15.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"gh-pages": "^2.0.1",
|
||||
"i18next": "^17.0.3",
|
||||
|
||||
@@ -32,16 +32,16 @@ class LoadingIndicator extends PureComponent {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div className="indicatorContents">
|
||||
<p>
|
||||
{this.props.t('Reformatting')}...
|
||||
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
||||
{percComplete}
|
||||
</p>
|
||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div className="indicatorContents">
|
||||
<p>
|
||||
{this.props.t('Loading...')}
|
||||
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
||||
{percComplete}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class OHIFVTKViewport extends Component {
|
||||
state = {
|
||||
volumes: null,
|
||||
paintFilterLabelMapImageData: null,
|
||||
paintFilterBackgroundImageData: null,
|
||||
paintFilterBackgroundImageData: null
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@@ -69,7 +69,7 @@ class OHIFVTKViewport extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onScroll: () => {},
|
||||
onScroll: () => { },
|
||||
};
|
||||
|
||||
static id = 'OHIFVTKViewport';
|
||||
@@ -156,6 +156,10 @@ class OHIFVTKViewport extends Component {
|
||||
const { activeLabelmapIndex } = brushStackState;
|
||||
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
|
||||
|
||||
this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map(isHidden => {
|
||||
return { visible: !isHidden };
|
||||
});
|
||||
|
||||
const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`;
|
||||
|
||||
if (labelmapCache[vtkLabelmapID]) {
|
||||
@@ -339,13 +343,13 @@ class OHIFVTKViewport extends Component {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
@@ -405,10 +409,6 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||
|
||||
const visible = configuration.renderFill || configuration.renderOutline;
|
||||
const opacity = configuration.fillAlpha;
|
||||
const outlineThickness = configuration.outlineThickness;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={style}>
|
||||
@@ -428,10 +428,14 @@ class OHIFVTKViewport extends Component {
|
||||
dataDetails={this.state.dataDetails}
|
||||
labelmapRenderingOptions={{
|
||||
colorLUT: this.state.labelmapColorLUT,
|
||||
globalOpacity: opacity,
|
||||
visible,
|
||||
outlineThickness,
|
||||
renderOutline: true,
|
||||
globalOpacity: configuration.fillAlpha,
|
||||
visible: configuration.renderFill,
|
||||
outlineThickness: configuration.outlineWidth,
|
||||
renderOutline: configuration.renderOutline,
|
||||
segmentsDefaultProperties: this.segmentsDefaultProperties,
|
||||
onNewSegmentationRequested: () => {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
}}
|
||||
onScroll={this.props.onScroll}
|
||||
/>
|
||||
|
||||
@@ -5,10 +5,13 @@ import {
|
||||
vtkInteractorStyleMPRRotate,
|
||||
vtkSVGCrosshairsWidget,
|
||||
} from 'react-vtkjs-viewport';
|
||||
|
||||
import { getImageData } from 'react-vtkjs-viewport';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import setMPRLayout from './utils/setMPRLayout.js';
|
||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
||||
import OHIFVTKViewport from './OHIFVTKViewport';
|
||||
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
||||
|
||||
const { BlendMode } = Constants;
|
||||
|
||||
@@ -34,7 +37,6 @@ const commandsModule = ({ commandsManager }) => {
|
||||
}
|
||||
|
||||
const displaySet = viewportSpecificData[activeViewportIndex];
|
||||
|
||||
let api;
|
||||
if (!api) {
|
||||
try {
|
||||
@@ -99,6 +101,20 @@ const commandsModule = ({ commandsManager }) => {
|
||||
});
|
||||
}
|
||||
|
||||
const _convertModelToWorldSpace = (position, vtkImageData) => {
|
||||
const indexToWorld = vtkImageData.getIndexToWorld();
|
||||
const pos = vec3.create();
|
||||
|
||||
position[0] += 0.5; /* Move to the centre of the voxel. */
|
||||
position[1] += 0.5; /* Move to the centre of the voxel. */
|
||||
position[2] += 0.5; /* Move to the centre of the voxel. */
|
||||
|
||||
vec3.set(pos, position[0], position[1], position[2]);
|
||||
vec3.transformMat4(pos, pos, indexToWorld);
|
||||
|
||||
return pos;
|
||||
};
|
||||
|
||||
const actions = {
|
||||
getVtkApis: ({ index }) => {
|
||||
return apis[index];
|
||||
@@ -124,6 +140,119 @@ const commandsModule = ({ commandsManager }) => {
|
||||
|
||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||
},
|
||||
requestNewSegmentation: async ({ viewports }) => {
|
||||
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||
let api = apis[viewportIndex];
|
||||
|
||||
if (!api) {
|
||||
api = await _getActiveViewportVTKApi(viewports);
|
||||
apis[viewportIndex] = api;
|
||||
}
|
||||
|
||||
api.requestNewSegmentation();
|
||||
api.updateImage();
|
||||
});
|
||||
await Promise.all(promises);
|
||||
},
|
||||
jumpToSlice: async ({
|
||||
viewports,
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
segmentNumber,
|
||||
frameIndex,
|
||||
frame,
|
||||
done = () => { }
|
||||
}) => {
|
||||
let api = apis[viewports.activeViewportIndex];
|
||||
|
||||
if (!api) {
|
||||
api = await _getActiveViewportVTKApi(viewports);
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
}
|
||||
|
||||
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
);
|
||||
|
||||
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||
|
||||
let pixelIndex = 0;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let count = 0;
|
||||
|
||||
const rows = imageDataObject.dimensions[1];
|
||||
const cols = imageDataObject.dimensions[0];
|
||||
|
||||
for (let j = 0; j < rows; j++) {
|
||||
for (let i = 0; i < cols; i++) {
|
||||
// [i, j] =
|
||||
const pixel = frame.pixelData[pixelIndex];
|
||||
if (pixel === segmentNumber) {
|
||||
x += i;
|
||||
y += j;
|
||||
count++;
|
||||
}
|
||||
pixelIndex++;
|
||||
}
|
||||
}
|
||||
x /= count;
|
||||
y /= count;
|
||||
|
||||
const position = [x, y, frameIndex];
|
||||
const worldPos = _convertModelToWorldSpace(position, imageDataObject.vtkImageData);
|
||||
|
||||
api.svgWidgets.crosshairsWidget.moveCrosshairs(worldPos, apis);
|
||||
done();
|
||||
},
|
||||
setSegmentationConfiguration: async ({
|
||||
viewports,
|
||||
globalOpacity,
|
||||
visible,
|
||||
renderOutline,
|
||||
outlineThickness,
|
||||
}) => {
|
||||
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||
let api = apis[viewportIndex];
|
||||
|
||||
if (!api) {
|
||||
api = await _getActiveViewportVTKApi(viewports);
|
||||
apis[viewportIndex] = api;
|
||||
}
|
||||
|
||||
api.setGlobalOpacity(globalOpacity);
|
||||
api.setVisibility(visible);
|
||||
api.setOutlineThickness(outlineThickness);
|
||||
api.setOutlineRendering(renderOutline);
|
||||
api.updateImage();
|
||||
});
|
||||
await Promise.all(promises);
|
||||
},
|
||||
setSegmentConfiguration: async ({ viewports, visible, segmentNumber }) => {
|
||||
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||
let api = apis[viewportIndex];
|
||||
|
||||
if (!api) {
|
||||
api = await _getActiveViewportVTKApi(viewports);
|
||||
apis[viewportIndex] = api;
|
||||
}
|
||||
|
||||
api.setSegmentVisibility(segmentNumber, visible);
|
||||
api.updateImage();
|
||||
});
|
||||
await Promise.all(promises);
|
||||
},
|
||||
enableRotateTool: () => {
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||
@@ -280,6 +409,26 @@ const commandsModule = ({ commandsManager }) => {
|
||||
window.vtkActions = actions;
|
||||
|
||||
const definitions = {
|
||||
requestNewSegmentation: {
|
||||
commandFn: actions.requestNewSegmentation,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
jumpToSlice: {
|
||||
commandFn: actions.jumpToSlice,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
setSegmentationConfiguration: {
|
||||
commandFn: actions.setSegmentationConfiguration,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
setSegmentConfiguration: {
|
||||
commandFn: actions.setSegmentConfiguration,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
axial: {
|
||||
commandFn: actions.axial,
|
||||
storeContexts: ['viewports'],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
@@ -16,7 +17,8 @@ const vtkExtension = {
|
||||
id: 'vtk',
|
||||
|
||||
getViewportModule({ commandsManager }) {
|
||||
return withCommandsManager(OHIFVTKViewport, commandsManager);
|
||||
const ExtendedVTKViewport = props => <OHIFVTKViewport {...props} />;
|
||||
return withCommandsManager(ExtendedVTKViewport, commandsManager);
|
||||
},
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
|
||||
@@ -3,6 +3,236 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.9.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.5...@ohif/core@2.9.6) (2020-05-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Load default display set when no time metadata ([#1684](https://github.com/OHIF/Viewers/issues/1684)) ([f7b8b6a](https://github.com/OHIF/Viewers/commit/f7b8b6a41c4626084ef56b0fdf7363e914b143c4)), closes [#1683](https://github.com/OHIF/Viewers/issues/1683)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.4...@ohif/core@2.9.5) (2020-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.3...@ohif/core@2.9.4) (2020-05-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.2...@ohif/core@2.9.3) (2020-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.1...@ohif/core@2.9.2) (2020-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use bit-appropriate array for palette lookup tables ([#1698](https://github.com/OHIF/Viewers/issues/1698)) ([7033886](https://github.com/OHIF/Viewers/commit/70338866978a76fa026c18d7c3c05257c5ece762))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.0...@ohif/core@2.9.1) (2020-04-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.8.1...@ohif/core@2.9.0) (2020-04-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.8.0...@ohif/core@2.8.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.8.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.7.1...@ohif/core@2.8.0) (2020-04-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.7.0...@ohif/core@2.7.1) (2020-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* whiteLabeling should support component creation by passing React to defined fn ([#1659](https://github.com/OHIF/Viewers/issues/1659)) ([2093a00](https://github.com/OHIF/Viewers/commit/2093a0036584b2cc698c8f06fe62b334523b1029))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.11...@ohif/core@2.7.0) (2020-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* set the authorization header for DICOMWeb requests if provided in query string ([#1646](https://github.com/OHIF/Viewers/issues/1646)) ([450c80b](https://github.com/OHIF/Viewers/commit/450c80b9d5f172be8b5713b422370360325a0afc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.11](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.10...@ohif/core@2.6.11) (2020-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.10](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.9...@ohif/core@2.6.10) (2020-04-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Revert "refactor: Reduce bundle size ([#1575](https://github.com/OHIF/Viewers/issues/1575))" ([#1622](https://github.com/OHIF/Viewers/issues/1622)) ([d21af3f](https://github.com/OHIF/Viewers/commit/d21af3f133492fa31492413b8782936c9ff18b44))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.8...@ohif/core@2.6.9) (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.6.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.7...@ohif/core@2.6.8) (2020-04-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.6...@ohif/core@2.6.7) (2020-04-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix multiframe images ([#1595](https://github.com/OHIF/Viewers/issues/1595)) ([9e0bd52](https://github.com/OHIF/Viewers/commit/9e0bd52c6a86648eb6673344a8555ad787043e5c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.5...@ohif/core@2.6.6) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.4...@ohif/core@2.6.5) (2020-04-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* segmentation not loading ([#1566](https://github.com/OHIF/Viewers/issues/1566)) ([4a7ce1c](https://github.com/OHIF/Viewers/commit/4a7ce1c09324d74c61048393e3a2427757e4001a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.3...@ohif/core@2.6.4) (2020-03-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add support for single entries in SequenceOfUltrasoundRegions. M… ([#1559](https://github.com/OHIF/Viewers/issues/1559)) ([c1a0d3c](https://github.com/OHIF/Viewers/commit/c1a0d3c662d143b62dfbf1c01f6ce394af3756ca))
|
||||
* disable autoFreeze of immer, even in dev mode ([#1560](https://github.com/OHIF/Viewers/issues/1560)) ([d604eba](https://github.com/OHIF/Viewers/commit/d604ebaffd93f688eadd0081e402f27074dd226b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.2...@ohif/core@2.6.3) (2020-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Ensure we take into account pixel spacing fields properly ([#1555](https://github.com/OHIF/Viewers/issues/1555)) ([77ab0ad](https://github.com/OHIF/Viewers/commit/77ab0ad9a14a135b5560741fc1600704df08c141))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.1...@ohif/core@2.6.2) (2020-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* OverlayPlane module usage for ArrayBuffer, BulkDataURI, and InlineBinary cases, as well as PaletteColor LUTs for ArrayBuffer (i.e. local drag/drop) case ([#1546](https://github.com/OHIF/Viewers/issues/1546)) ([404d52f](https://github.com/OHIF/Viewers/commit/404d52fe5c0442dd13e4d407bb0687d72fa5f32c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.0...@ohif/core@2.6.1) (2020-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid-wasteful-renders ([#1544](https://github.com/OHIF/Viewers/issues/1544)) ([e41d339](https://github.com/OHIF/Viewers/commit/e41d339f5faef6b93700bc860f37f29f32ad5ed6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.3...@ohif/core@2.6.0) (2020-03-13)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.6.0",
|
||||
"version": "2.9.6",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -32,15 +32,16 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"cornerstone-tools": "4.15.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dicom-parser": "^1.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "^0.8.3",
|
||||
"dicomweb-client": "^0.5.2",
|
||||
"dcmjs": "^0.12.2",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.merge": "^4.6.1",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import dcmjs from 'dcmjs';
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
import DICOMWeb from '../DICOMWeb';
|
||||
import parseDicomStructuredReport from './parseDicomStructuredReport';
|
||||
import parseMeasurementsData from './parseMeasurementsData';
|
||||
import getAllDisplaySets from './utils/getAllDisplaySets';
|
||||
import errorHandler from '../errorHandler';
|
||||
|
||||
const VERSION_NAME = 'dcmjs-0.0';
|
||||
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
||||
@@ -21,6 +22,7 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
||||
const config = {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
@@ -71,6 +73,7 @@ const stowSRFromMeasurements = async (measurements, serverUrl) => {
|
||||
const config = {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import dcmjs from 'dcmjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
import log from '../log';
|
||||
|
||||
Loaded 100 of 236 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user