Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cf72d1757 | ||
|
|
bb1cdbda3a | ||
|
|
fab122dd8f | ||
|
|
42803b0ae8 | ||
|
|
5a78da8ff6 | ||
|
|
0ef88e88eb | ||
|
|
a7b5f1847a | ||
|
|
5cb6e63b88 | ||
|
|
48eb4a9ec3 | ||
|
|
d9231fd431 | ||
|
|
e5b578bd8e | ||
|
|
f38979037d | ||
|
|
45ca88f5b8 | ||
|
|
a1dee16a3d | ||
|
|
6a37a2a661 | ||
|
|
a8caf4bba4 | ||
|
|
29dcc82e28 | ||
|
|
dbf0501031 | ||
|
|
ef28fa2ac1 | ||
|
|
779a7e0976 | ||
|
|
7f3ca1fc23 | ||
|
|
59ab68b4ed | ||
|
|
248bc0a64a | ||
|
|
5fdace1432 | ||
|
|
71b6b454a3 | ||
|
|
3b02a06f5e | ||
|
|
ebdcde1c4d | ||
|
|
829ac2b410 | ||
|
|
09745a0349 | ||
|
|
cd73dd3027 | ||
|
|
ca511edeff | ||
|
|
bb5f30ce2a | ||
|
|
87d5c68e71 | ||
|
|
0e87ab37c2 | ||
|
|
756b005fe1 | ||
|
|
3d86b5f876 | ||
|
|
2a647fd618 | ||
|
|
f8fc31bcac | ||
|
|
b7ab88b0f1 | ||
|
|
57a30d7c34 | ||
|
|
cd90665b0e | ||
|
|
98c657231e | ||
|
|
14da247318 | ||
|
|
6a77d6c778 | ||
|
|
8f1785446b | ||
|
|
41225eefc0 | ||
|
|
7772fee21a | ||
|
|
d870ac914a | ||
|
|
451f7eab92 | ||
|
|
029f8af4c7 | ||
|
|
b1b4c8c861 | ||
|
|
cdde2af224 | ||
|
|
4a2922dda0 | ||
|
|
b6b5a8414f | ||
|
|
e7cc735c03 | ||
|
|
6121bdcf63 | ||
|
|
da70079675 | ||
|
|
9089875ca7 | ||
|
|
b4627ecfa3 | ||
|
|
4ede8a525a | ||
|
|
2c23818c86 | ||
|
|
fa4b03d62d | ||
|
|
698e900b85 | ||
|
|
b7d47383c5 | ||
|
|
6123741765 | ||
|
|
ab25be3520 | ||
|
|
950a54c704 | ||
|
|
4293c073f0 | ||
|
|
62a2cd5ff3 | ||
|
|
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 |
No files matched your search
@@ -11,6 +11,7 @@ node -v
|
||||
|
||||
# Install build deps and all monorepo package dependencies. Yarn Workspaces
|
||||
# should also symlink all projects appropriately
|
||||
yarn run lerna:restore
|
||||
yarn install --no-ignore-optional --pure-lockfile
|
||||
|
||||
# Build && Move PWA Output
|
||||
@@ -21,6 +22,19 @@ mv platform/viewer/dist/* .netlify/www/pwa -v
|
||||
# Build && Move script output
|
||||
# yarn run build:package
|
||||
|
||||
# Build && Move Docz Output
|
||||
# Using local yarn install to prevent Gatsby from needing to access
|
||||
# node_modules above the platform/ui folder
|
||||
cd platform/ui
|
||||
yarn install
|
||||
yarn run build
|
||||
cd ../..
|
||||
mkdir -p ./.netlify/www/ui
|
||||
mv platform/ui/.docz/dist/* .netlify/www/ui -v
|
||||
|
||||
# Cache all of the node_module dependencies in
|
||||
# extensions, modules, and platform packages
|
||||
yarn run lerna:cache
|
||||
echo 'Nothing left to see here. Go home, folks.'
|
||||
|
||||
# Build using react-scripts
|
||||
|
||||
@@ -122,7 +122,7 @@ window.config = {
|
||||
```
|
||||
|
||||
- Install the viewer:
|
||||
`window.OHIFStandaloneViewer.installViewer(window.config);`
|
||||
`window.OHIFViewer.installViewer(window.config);`
|
||||
|
||||
This exact setup is demonstrated in this
|
||||
[CodeSandbox](https://codesandbox.io/s/viewer-script-tag-tprch) and in our
|
||||
|
||||
@@ -127,10 +127,107 @@ appropriate headers. You can find an example of this setup in our
|
||||
|
||||
#### 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
|
||||
|
||||
|
||||
@@ -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
|
||||
-->
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,186 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.9.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.9.3...@ohif/extension-cornerstone@2.9.4) (2020-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.9.2...@ohif/extension-cornerstone@2.9.3) (2020-09-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.9.1...@ohif/extension-cornerstone@2.9.2) (2020-08-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Updated react-cornerstone-viewport to version 4.0.2 ([#2001](https://github.com/OHIF/Viewers/issues/2001)) ([59ab68b](https://github.com/OHIF/Viewers/commit/59ab68b4edbfec010170c4429c300a53f688bf94))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.9.0...@ohif/extension-cornerstone@2.9.1) (2020-08-20)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **stackPrefetch:** Added stackPrefetch config with 20 max concurrent requests ([#2000](https://github.com/OHIF/Viewers/issues/2000)) ([3b02a06](https://github.com/OHIF/Viewers/commit/3b02a06f5e250660edde4862ce44147db3dc3ab9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.9.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.8.5...@ohif/extension-cornerstone@2.9.0) (2020-08-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Update react-vtkjs-viewport usage to use requestPool ([#1984](https://github.com/OHIF/Viewers/issues/1984)) ([bb5f30c](https://github.com/OHIF/Viewers/commit/bb5f30ce2a0192d2e021beaaadfff22fd38e17b9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.8.4...@ohif/extension-cornerstone@2.8.5) (2020-08-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.8.3...@ohif/extension-cornerstone@2.8.4) (2020-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 - Put guards in all places that a cornerstone re-render ([#1899](https://github.com/OHIF/Viewers/issues/1899)) ([451f7ea](https://github.com/OHIF/Viewers/commit/451f7eab9258e7a193eb362e0926b13aedc4b3c9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.8.2...@ohif/extension-cornerstone@2.8.3) (2020-07-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.8.1...@ohif/extension-cornerstone@2.8.2) (2020-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Disable seg panel when data for seg unavailable ([#1732](https://github.com/OHIF/Viewers/issues/1732)) ([698e900](https://github.com/OHIF/Viewers/commit/698e900b85121d3c2a46747c443ef69fb7a8c95b)), closes [#1728](https://github.com/OHIF/Viewers/issues/1728)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.5.1",
|
||||
"version": "2.9.4",
|
||||
"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.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.16.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": "4.0.2"
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -7,6 +7,17 @@ import { getEnabledElement } from './state';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
const scroll = cornerstoneTools.import('util/scroll');
|
||||
|
||||
const { studyMetadataManager } = OHIF.utils;
|
||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||
|
||||
const refreshCornerstoneViewports = () => {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
if (enabledElement.image) {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const commandsModule = ({ servicesManager }) => {
|
||||
const actions = {
|
||||
rotateViewport: ({ viewports, rotation }) => {
|
||||
@@ -177,10 +188,7 @@ const commandsModule = ({ servicesManager }) => {
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
|
||||
// Update images in all active viewports
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
refreshCornerstoneViewports();
|
||||
},
|
||||
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
|
||||
const nearbyTool = {};
|
||||
@@ -255,9 +263,38 @@ 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)
|
||||
);
|
||||
|
||||
refreshCornerstoneViewports();
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
jumpToImage: {
|
||||
commandFn: actions.jumpToImage,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getNearbyToolData: {
|
||||
commandFn: actions.getNearbyToolData,
|
||||
storeContexts: [],
|
||||
|
||||
@@ -3,6 +3,7 @@ import init from './init.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
import { version } from '../package.json';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import('./OHIFCornerstoneViewport');
|
||||
@@ -24,6 +25,7 @@ export default {
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'cornerstone',
|
||||
version,
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -34,8 +36,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 =
|
||||
@@ -24,4 +34,11 @@ export default function(configuration = {}) {
|
||||
cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)');
|
||||
|
||||
cornerstoneTools.store.state.touchProximity = 40;
|
||||
|
||||
// Configure stack prefetch
|
||||
cornerstoneTools.stackPrefetch.setConfiguration({
|
||||
maxImagesToPrefetch: Infinity,
|
||||
preserveExistingPool: false,
|
||||
maxSimultaneousRequests: 20,
|
||||
});
|
||||
}
|
||||
@@ -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,68 @@
|
||||
# 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.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.1.4...@ohif/extension-debugging@0.1.5) (2020-09-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.1.3...@ohif/extension-debugging@0.1.4) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 0.1.3 (2020-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-p10-downloader@0.1.1...@ohif/extension-dicom-p10-downloader@0.1.2) (2020-06-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-p10-downloader
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-p10-downloader@0.1.0...@ohif/extension-dicom-p10-downloader@0.1.1) (2020-06-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-p10-downloader
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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,41 @@
|
||||
{
|
||||
"name": "@ohif/extension-debugging",
|
||||
"version": "0.1.5",
|
||||
"description": "OHIF extension for debugging.",
|
||||
"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",
|
||||
"detect-browser": "5.1.1",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"jszip": "^3.2.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.debugReportModalHeader {
|
||||
padding: 10px 0 10px;
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.debug-report-modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import React from 'react';
|
||||
import { detect } from 'detect-browser';
|
||||
import './DebugReportModal.css';
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
|
||||
const DubugReportModal = ({
|
||||
viewports,
|
||||
studies,
|
||||
servers,
|
||||
extensionManager,
|
||||
mailTo,
|
||||
}) => {
|
||||
const mailToFunction = () => {
|
||||
const StudyInstanceUID = Object.keys(studies.studyData)[0];
|
||||
|
||||
const subject = encodeURI(`Issue with Study: ${StudyInstanceUID}`);
|
||||
|
||||
let body = `Enter the description of your problem here: \n\n\n`;
|
||||
|
||||
body += `============= SESSION INFO =============\n\n`;
|
||||
|
||||
// App version
|
||||
|
||||
body += '== App ==\n';
|
||||
body += `version\t${window.version}\n\n`;
|
||||
|
||||
// Extensions Versions
|
||||
|
||||
body += '== Extensions Versions ==\n';
|
||||
|
||||
const { registeredExtensionVesions } = extensionManager;
|
||||
|
||||
Object.keys(registeredExtensionVesions).forEach(extensionId => {
|
||||
const version = registeredExtensionVesions[extensionId];
|
||||
|
||||
body += `${extensionId}\t${version}\n`;
|
||||
});
|
||||
|
||||
body += '\n';
|
||||
|
||||
// Browser Info
|
||||
|
||||
const browser = detect();
|
||||
|
||||
const { name, os, type, version } = browser;
|
||||
|
||||
body += '== Browser Info ==\n';
|
||||
body += `name\t ${name}\n`;
|
||||
body += `os\t ${os}\n`;
|
||||
body += `type\t ${type}\n`;
|
||||
body += `version\t ${version}\n\n`;
|
||||
|
||||
// Study URL
|
||||
body += '== URL ==\n';
|
||||
body += `URL\t ${window.location.href}\n\n`;
|
||||
|
||||
// Layout
|
||||
|
||||
const { numRows, numColumns, viewportSpecificData } = viewports;
|
||||
|
||||
body += '== Viewport Layout ==\n';
|
||||
body += `Rows\t${numRows}\n`;
|
||||
body += `Columns\t${numColumns}\n\n`;
|
||||
|
||||
body += '== Viewports ==\n';
|
||||
|
||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||
const vsd = viewportSpecificData[viewportIndex];
|
||||
|
||||
const [row, column] = _viewportIndexToViewportPosition(
|
||||
viewportIndex,
|
||||
numColumns
|
||||
);
|
||||
|
||||
body += `[${row},${column}]\t${vsd.SeriesInstanceUID}\n`;
|
||||
});
|
||||
|
||||
// TODO Text dump of rest of stuff.
|
||||
|
||||
body = encodeURI(body);
|
||||
|
||||
window.location.href = `mailto:${mailTo}?subject=${subject}&body=${body}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="debug-report-modal-container">
|
||||
{mailTo ? (
|
||||
<div>
|
||||
<ToolbarButton
|
||||
label={'Send Bug Report'}
|
||||
onClick={mailToFunction}
|
||||
icon={'envelope-square'}
|
||||
isActive={false}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<table>
|
||||
{getAppVersion()}
|
||||
{getExtensionVersions(extensionManager)}
|
||||
{getBrowserInfo()}
|
||||
{getCurrentStudyUrl()}
|
||||
{getLayout(viewports)}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getAppVersion = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<tr>
|
||||
<th className="debugReportModalHeader">App</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{window.version}</td>
|
||||
</tr>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getCurrentStudyUrl = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<tr>
|
||||
<th className="debugReportModalHeader">URL</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>URL</td>
|
||||
<td>{window.location.href}</td>
|
||||
</tr>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getExtensionVersions = extensionManager => {
|
||||
const { registeredExtensionVesions } = extensionManager;
|
||||
|
||||
const lineItems = Object.keys(registeredExtensionVesions).map(extensionId => {
|
||||
const version = registeredExtensionVesions[extensionId];
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{extensionId}</td>
|
||||
<td>{version}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<th className="debugReportModalHeader">Extensions</th>
|
||||
{lineItems}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getLayout = viewports => {
|
||||
const { numRows, numColumns } = viewports;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<tr>
|
||||
<th className="debugReportModalHeader">Viewports</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Layout</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rows</td>
|
||||
<td>{numRows}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Columns</td>
|
||||
<td>{numColumns}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SeriesInstanceUIDs</th>
|
||||
</tr>
|
||||
{getSeriesInstanceUIDsPerRow(viewports)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getBrowserInfo = () => {
|
||||
const browser = detect();
|
||||
|
||||
const { name, os, type, version } = browser;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<tr>
|
||||
<th className="debugReportModalHeader">Platform</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>name</td>
|
||||
<td>{name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>os</td>
|
||||
<td>{os}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>{type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>version</td>
|
||||
<td>{version}</td>
|
||||
</tr>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getSeriesInstanceUIDsPerRow = viewports => {
|
||||
const { viewportSpecificData, numColumns } = viewports;
|
||||
|
||||
// NOTE viewportSpecificData is actually an object with numerical keys.
|
||||
return Object.keys(viewportSpecificData).map(viewportIndex => {
|
||||
const vsd = viewportSpecificData[viewportIndex];
|
||||
|
||||
const [row, column] = _viewportIndexToViewportPosition(
|
||||
viewportIndex,
|
||||
numColumns
|
||||
);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{`[${row},${column}]`}</td>
|
||||
<td>{vsd.SeriesInstanceUID}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const _viewportIndexToViewportPosition = (viewportIndex, numColumns) => {
|
||||
const row = Math.floor(viewportIndex / numColumns);
|
||||
const column = viewportIndex % numColumns;
|
||||
|
||||
return [row, column];
|
||||
};
|
||||
|
||||
export default DubugReportModal;
|
||||
@@ -0,0 +1,173 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import {
|
||||
save,
|
||||
upload,
|
||||
getDicomWebClientFromContext,
|
||||
getStudyInstanceUIDFromStudies,
|
||||
getSOPInstanceReferenceFromActiveViewport,
|
||||
getSOPInstanceReferencesFromViewports,
|
||||
} from './utils';
|
||||
import _downloadAndZip, { downloadInstances } from './downloadAndZip';
|
||||
import DebugReportModal from './DebugReportModal';
|
||||
import React from 'react';
|
||||
|
||||
import state from './state';
|
||||
|
||||
const {
|
||||
utils: { Queue },
|
||||
} = OHIF;
|
||||
|
||||
export function getCommands(context, servicesManager, extensionManager) {
|
||||
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
|
||||
);
|
||||
},
|
||||
downloadAndZipStudy({ servers, studies, progress }) {
|
||||
const dicomWebClient = getDicomWebClientFromContext(context, servers);
|
||||
const listOfUIDs = getStudyInstanceUIDFromStudies(studies);
|
||||
return save(
|
||||
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
|
||||
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
|
||||
);
|
||||
},
|
||||
downloadAndUploadStudy({ servers, studies, progress, serverConfig }) {
|
||||
const dicomWebClient = getDicomWebClientFromContext(context, servers);
|
||||
const listOfUIDs = getStudyInstanceUIDFromStudies(studies);
|
||||
return upload(
|
||||
downloadInstances(dicomWebClient, listOfUIDs, { progress }),
|
||||
/**
|
||||
* serverConfig is an object with the values used to create a new
|
||||
* instance of DICOMwebClient.
|
||||
*
|
||||
* Basic Structure:
|
||||
*
|
||||
* const config = {
|
||||
* url,
|
||||
* headers,
|
||||
* errorInterceptor
|
||||
* }
|
||||
*
|
||||
* const dicomWeb = new api.DICOMwebClient(config);
|
||||
*/
|
||||
serverConfig
|
||||
);
|
||||
},
|
||||
openDebugInfoModal({ viewports, studies, servers }) {
|
||||
const { UIModalService } = servicesManager.services;
|
||||
|
||||
const WrappedDebugReportModal = function() {
|
||||
return (
|
||||
<DebugReportModal
|
||||
viewports={viewports}
|
||||
studies={studies}
|
||||
servers={servers}
|
||||
extensionManager={extensionManager}
|
||||
mailTo={state.mailTo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
UIModalService.show({
|
||||
content: WrappedDebugReportModal,
|
||||
title: `Debugging Information`,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
openDebugInfoModal: {
|
||||
commandFn: actions.openDebugInfoModal,
|
||||
storeContexts: ['viewports', 'servers', 'studies'],
|
||||
},
|
||||
downloadAndZip: {
|
||||
commandFn: queue.bindSafe(actions.downloadAndZip, error),
|
||||
storeContexts: ['servers'],
|
||||
},
|
||||
downloadAndZipStudy: {
|
||||
commandFn: queue.bindSafe(actions.downloadAndZipStudy, error),
|
||||
storeContexts: ['servers', 'studies'],
|
||||
options: { progress },
|
||||
},
|
||||
downloadAndZipSeriesOnViewports: {
|
||||
commandFn: queue.bindSafe(actions.downloadAndZipSeriesOnViewports, error),
|
||||
storeContexts: ['servers', 'viewports'],
|
||||
options: { progress },
|
||||
},
|
||||
downloadAndZipSeriesOnActiveViewport: {
|
||||
commandFn: queue.bindSafe(
|
||||
actions.downloadAndZipSeriesOnActiveViewport,
|
||||
error
|
||||
),
|
||||
storeContexts: ['servers', 'viewports'],
|
||||
options: { progress },
|
||||
},
|
||||
downloadAndUploadStudy: {
|
||||
commandFn: queue.bindSafe(actions.downloadAndUploadStudy, error),
|
||||
storeContexts: ['servers', 'studies'],
|
||||
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,259 @@
|
||||
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;
|
||||
const buffers = await downloadBuffers(settings, dicomWebClient);
|
||||
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');
|
||||
}
|
||||
|
||||
async function downloadInstances(dicomWebClient, listOfUIDs, options) {
|
||||
if (dicomWebClient instanceof api.DICOMwebClient) {
|
||||
const settings = buildSettings(listOfUIDs, options);
|
||||
const buffers = await downloadBuffers(settings, dicomWebClient);
|
||||
return buffers;
|
||||
}
|
||||
throw new Error('A valid DICOM Web Client instance is expected');
|
||||
}
|
||||
|
||||
async function downloadBuffers(settings, dicomWebClient) {
|
||||
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;
|
||||
});
|
||||
return buffers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, downloadInstances };
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getDicomWebClientFromConfig } from './utils';
|
||||
import { getCommands } from './commandsModule';
|
||||
import { version } from '../package.json';
|
||||
import toolbarModule from './toolbarModule';
|
||||
import state from './state';
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
/**
|
||||
* Globals
|
||||
*/
|
||||
|
||||
const sharedContext = {
|
||||
dicomWebClient: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Extension
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'dicom-p10-downloader',
|
||||
version,
|
||||
|
||||
/**
|
||||
* LIFECYCLE HOOKS
|
||||
*/
|
||||
|
||||
preRegistration({ appConfig, configuration }) {
|
||||
const dicomWebClient = getDicomWebClientFromConfig(appConfig);
|
||||
if (dicomWebClient) {
|
||||
sharedContext.dicomWebClient = dicomWebClient;
|
||||
}
|
||||
|
||||
if (configuration && configuration.mailTo) {
|
||||
state.mailTo = configuration.mailTo;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* MODULE GETTERS
|
||||
*/
|
||||
|
||||
getCommandsModule({ servicesManager, extensionManager }) {
|
||||
return getCommands(sharedContext, servicesManager, extensionManager);
|
||||
},
|
||||
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
const state = { mailTo: undefined };
|
||||
|
||||
export default state;
|
||||
@@ -0,0 +1,20 @@
|
||||
const TOOLBAR_BUTTON_TYPES = {
|
||||
COMMAND: 'command',
|
||||
};
|
||||
|
||||
const definitions = [
|
||||
{
|
||||
id: 'Debug Info',
|
||||
label: 'Debug Info',
|
||||
icon: 'cog',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'openDebugInfoModal',
|
||||
context: 'VIEWER',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'VIEWER',
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
function upload(promise, serverConfig) {
|
||||
return Promise.resolve(promise)
|
||||
.then(async instances => {
|
||||
const instancesAmount = instances.length;
|
||||
OHIF.log.info(`Uploading study to ${serverConfig.url}`);
|
||||
OHIF.log.info(
|
||||
`${instancesAmount} instances are being uploaded. Don't close your browser.`
|
||||
);
|
||||
|
||||
try {
|
||||
const dicomWeb = new api.DICOMwebClient(serverConfig);
|
||||
let progress = 0;
|
||||
|
||||
const getProgress = () => {
|
||||
return ((progress * 100) / instancesAmount).toFixed();
|
||||
};
|
||||
|
||||
for (const instance of instances) {
|
||||
const options = {
|
||||
datasets: [instance],
|
||||
};
|
||||
|
||||
await dicomWeb.storeInstances(options);
|
||||
|
||||
progress++;
|
||||
|
||||
OHIF.log.info(`Progress: ${getProgress()}%`);
|
||||
}
|
||||
|
||||
OHIF.log.info('Successfully uploaded!');
|
||||
} catch (error) {
|
||||
OHIF.log.error(`Failed to upload: ${error}`);
|
||||
}
|
||||
|
||||
return instances;
|
||||
})
|
||||
.catch(error => {
|
||||
OHIF.log.error(`Failed to upload: ${error}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function getStudyInstanceUIDFromStudies(studies) {
|
||||
return Object.keys(Object(Object(studies).studyData)).slice(0, 1);
|
||||
}
|
||||
|
||||
export {
|
||||
save,
|
||||
upload,
|
||||
validDicomUid,
|
||||
getDicomWebClientFromConfig,
|
||||
getDicomWebClientFromContext,
|
||||
getStudyInstanceUIDFromStudies,
|
||||
getSOPInstanceReferenceFromActiveViewport,
|
||||
getSOPInstanceReferencesFromViewports,
|
||||
};
|
||||
@@ -3,6 +3,87 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.7...@ohif/extension-dicom-html@1.2.8) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.6...@ohif/extension-dicom-html@1.2.7) (2020-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.5...@ohif/extension-dicom-html@1.2.6) (2020-09-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.4...@ohif/extension-dicom-html@1.2.5) (2020-08-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.3...@ohif/extension-dicom-html@1.2.4) (2020-07-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.2...@ohif/extension-dicom-html@1.2.3) (2020-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Disable seg panel when data for seg unavailable ([#1732](https://github.com/OHIF/Viewers/issues/1732)) ([698e900](https://github.com/OHIF/Viewers/commit/698e900b85121d3c2a46747c443ef69fb7a8c95b)), closes [#1728](https://github.com/OHIF/Viewers/issues/1728)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.8",
|
||||
"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.16.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';
|
||||
|
||||
@@ -22,6 +22,14 @@ const OHIFDicomHtmlSopClassHandler = {
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
const metadata = instance.getData().metadata;
|
||||
const {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
} = metadata;
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
Modality: 'SR',
|
||||
@@ -31,6 +39,11 @@ const OHIFDicomHtmlSopClassHandler = {
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
SeriesDescription,
|
||||
metadata,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesNumber,
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js';
|
||||
import { version } from '../package.json';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import('./OHIFDicomHtmlViewport');
|
||||
@@ -18,6 +19,7 @@ export default {
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'html',
|
||||
version,
|
||||
|
||||
getViewportModule() {
|
||||
return OHIFDicomHtmlViewport;
|
||||
|
||||
@@ -3,6 +3,41 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.51.2...@ohif/extension-dicom-microscopy@0.51.3) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.51.1...@ohif/extension-dicom-microscopy@0.51.2) (2020-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.51.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.51.0...@ohif/extension-dicom-microscopy@0.51.1) (2020-08-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "0.50.9",
|
||||
"version": "0.51.3",
|
||||
"description": "OHIF extension for Dicom Microscopy",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -12,6 +12,14 @@ const DicomMicroscopySopClassHandler = {
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
const metadata = instance.getData().metadata;
|
||||
const {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
ContentDate,
|
||||
ContentTime,
|
||||
} = metadata;
|
||||
|
||||
// Note: We are passing the dicomweb client into each viewport!
|
||||
|
||||
return {
|
||||
@@ -22,6 +30,11 @@ const DicomMicroscopySopClassHandler = {
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
SeriesDescription,
|
||||
SeriesDate: ContentDate, // Map ContentDate/Time to SeriesTime for series list sorting.
|
||||
SeriesTime: ContentTime,
|
||||
SeriesNumber,
|
||||
metadata,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -92,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,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import DicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler.js';
|
||||
import { version } from '../package.json';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import('./DicomMicroscopyViewport');
|
||||
@@ -18,6 +19,7 @@ export default {
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'microscopy',
|
||||
version,
|
||||
|
||||
getViewportModule() {
|
||||
return DicomMicroscopyViewport;
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.5...@ohif/extension-dicom-pdf@1.0.6) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.4...@ohif/extension-dicom-pdf@1.0.5) (2020-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.3...@ohif/extension-dicom-pdf@1.0.4) (2020-08-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.6",
|
||||
"description": "OHIF extension for Dicom PDF",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -12,6 +12,14 @@ const OHIFDicomPDFSopClassHandler = {
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
const metadata = instance.getData().metadata;
|
||||
const {
|
||||
ContentDate,
|
||||
ContentTime,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
} = metadata;
|
||||
|
||||
return {
|
||||
plugin: 'pdf',
|
||||
Modality: 'DOC',
|
||||
@@ -21,6 +29,11 @@ const OHIFDicomPDFSopClassHandler = {
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
SeriesDescription,
|
||||
SeriesDate: ContentDate, // Map ContentDate/Time to SeriesTime for series list sorting.
|
||||
SeriesTime: ContentTime,
|
||||
SeriesNumber,
|
||||
metadata,
|
||||
authorizationHeaders: authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js';
|
||||
import { version } from '../package.json';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import('./ConnectedOHIFDicomPDFViewer');
|
||||
@@ -18,6 +19,7 @@ export default {
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'pdf',
|
||||
version,
|
||||
getViewportModule() {
|
||||
return ConnectedOHIFDicomPDFViewer;
|
||||
},
|
||||
|
||||
@@ -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,206 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.4.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.5...@ohif/extension-dicom-rt@0.4.6) (2020-09-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.4...@ohif/extension-dicom-rt@0.4.5) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.3...@ohif/extension-dicom-rt@0.4.4) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.2...@ohif/extension-dicom-rt@0.4.3) (2020-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.1...@ohif/extension-dicom-rt@0.4.2) (2020-09-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.0...@ohif/extension-dicom-rt@0.4.1) (2020-08-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.3.0...@ohif/extension-dicom-rt@0.4.0) (2020-08-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Update react-vtkjs-viewport usage to use requestPool ([#1984](https://github.com/OHIF/Viewers/issues/1984)) ([bb5f30c](https://github.com/OHIF/Viewers/commit/bb5f30ce2a0192d2e021beaaadfff22fd38e17b9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.2.7...@ohif/extension-dicom-rt@0.3.0) (2020-08-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Add support for POINT and OPEN_PLANAR for RT ([0e87ab3](https://github.com/OHIF/Viewers/commit/0e87ab37c29fcf9af74bbcefca854c6e6b8707bc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.2.6...@ohif/extension-dicom-rt@0.2.7) (2020-08-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.2.5...@ohif/extension-dicom-rt@0.2.6) (2020-08-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.2.4...@ohif/extension-dicom-rt@0.2.5) (2020-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 - Put guards in all places that a cornerstone re-render ([#1899](https://github.com/OHIF/Viewers/issues/1899)) ([451f7ea](https://github.com/OHIF/Viewers/commit/451f7eab9258e7a193eb362e0926b13aedc4b3c9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.2.3...@ohif/extension-dicom-rt@0.2.4) (2020-07-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.2.2...@ohif/extension-dicom-rt@0.2.3) (2020-07-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.2.1...@ohif/extension-dicom-rt@0.2.2) (2020-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix RT Panel hide/show and Fix looping load errors ([#1877](https://github.com/OHIF/Viewers/issues/1877)) ([e7cc735](https://github.com/OHIF/Viewers/commit/e7cc735c03d02eeb0d3af4ba02c15ed4f81bbec2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.2.0...@ohif/extension-dicom-rt@0.2.1) (2020-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Disable seg panel when data for seg unavailable ([#1732](https://github.com/OHIF/Viewers/issues/1732)) ([698e900](https://github.com/OHIF/Viewers/commit/698e900b85121d3c2a46747c443ef69fb7a8c95b)), closes [#1728](https://github.com/OHIF/Viewers/issues/1728)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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,44 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-rt",
|
||||
"version": "0.4.6",
|
||||
"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.20.1",
|
||||
"dcmjs": "0.16.3",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-tooltip": "4.2.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core';
|
||||
import loadRTStruct from './loadRTStruct';
|
||||
import getSourceDisplaySet from './getSourceDisplaySet';
|
||||
|
||||
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,
|
||||
SeriesNumber,
|
||||
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,
|
||||
SeriesNumber,
|
||||
SeriesDescription,
|
||||
metadata,
|
||||
};
|
||||
|
||||
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.getSourceDisplaySet = function(studies) {
|
||||
return getSourceDisplaySet(studies, rtStructDisplaySet);
|
||||
};
|
||||
|
||||
rtStructDisplaySet.load = function(referencedDisplaySet, studies) {
|
||||
return loadRTStruct(
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
).catch(error => {
|
||||
rtStructDisplaySet.isLoaded = false;
|
||||
rtStructDisplaySet.loadError = true;
|
||||
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,335 @@
|
||||
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';
|
||||
import TOOL_NAMES from '../../utils/toolNames';
|
||||
|
||||
const { RTSTRUCT_DISPLAY_TOOL } = TOOL_NAMES;
|
||||
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
const refreshViewport = () => {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
if (enabledElement.image) {
|
||||
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, isSupported },
|
||||
loadedSet
|
||||
) => {
|
||||
let interpretedType = '';
|
||||
if (RTROIObservations && RTROIObservations.RTROIInterpretedType) {
|
||||
interpretedType = `(${RTROIObservations.RTROIInterpretedType})`;
|
||||
}
|
||||
|
||||
const isSameContour = selectedContour && selectedContour === ROINumber;
|
||||
return (
|
||||
<StructureSetItem
|
||||
key={ROINumber}
|
||||
selected={isSameContour}
|
||||
isDisabled={!isSupported}
|
||||
onClick={() => {
|
||||
setSelectedContour(isSameContour ? null : ROINumber);
|
||||
|
||||
if (isCornerstone()) {
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
const element = enabledElements[activeIndex].element;
|
||||
const stackToolState = cornerstoneTools.getToolState(
|
||||
element,
|
||||
'stack'
|
||||
);
|
||||
|
||||
if (!stackToolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = stackToolState.data[0].imageIds;
|
||||
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
|
||||
state.selectedSet.SeriesInstanceUID,
|
||||
ROINumber,
|
||||
imageIds
|
||||
);
|
||||
|
||||
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
|
||||
const imageIdSpecificToolState = toolState[imageId];
|
||||
|
||||
const rtstructData =
|
||||
imageIdSpecificToolState[RTSTRUCT_DISPLAY_TOOL];
|
||||
|
||||
const specificData = rtstructData.data.find(
|
||||
rtData => rtData.ROINumber === ROINumber
|
||||
);
|
||||
|
||||
specificData.highlight = true;
|
||||
|
||||
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 && loadedSet.visible}
|
||||
hideVisibleButton={!isLoaded}
|
||||
expanded={
|
||||
isLoaded &&
|
||||
loadedSet.SeriesInstanceUID ===
|
||||
state.selectedSet.SeriesInstanceUID
|
||||
}
|
||||
onVisibilityChange={newVisibility => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
|
||||
if (newVisibility) {
|
||||
module.setters.showStructureSet(loadedSet.SeriesInstanceUID);
|
||||
} else {
|
||||
module.setters.hideStructureSet(loadedSet.SeriesInstanceUID);
|
||||
}
|
||||
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,93 @@
|
||||
.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 .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);
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item.isDisabled .item-color-section {
|
||||
background-color: #e29e4a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item.isDisabled .item-label {
|
||||
color: var(--text-disabled-color);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TableListItem, Icon, Tooltip, OverlayTrigger } from '@ohif/ui';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
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,
|
||||
isDisabled,
|
||||
onClick,
|
||||
itemClass,
|
||||
color,
|
||||
visible = true,
|
||||
onVisibilityChange,
|
||||
selected = false,
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(visible);
|
||||
}, [visible]);
|
||||
|
||||
let dcmrtClassNames = `dcmrt-structure-set-item`;
|
||||
|
||||
if (selected) {
|
||||
dcmrtClassNames += ' selected';
|
||||
}
|
||||
|
||||
if (isDisabled) {
|
||||
dcmrtClassNames += ' isDisabled';
|
||||
}
|
||||
|
||||
const warningIcon = (
|
||||
<span>
|
||||
<Icon name="exclamation-triangle" />
|
||||
</span>
|
||||
);
|
||||
|
||||
const tableListItem = (
|
||||
<TableListItem
|
||||
key={index}
|
||||
itemKey={index}
|
||||
itemIndex={index}
|
||||
itemClass={itemClass}
|
||||
itemMeta={isDisabled ? warningIcon : <ColoredCircle color={color} />}
|
||||
itemMetaClass="item-color-section"
|
||||
onItemClick={() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="item-label" style={{ marginBottom: 4 }}>
|
||||
<a data-tip data-for={`StructureHover${index}`}>
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
<ReactTooltip
|
||||
id={`StructureHover${index}`}
|
||||
delayShow={250}
|
||||
place="right"
|
||||
border={true}
|
||||
type="light"
|
||||
>
|
||||
<span>{label}</span>
|
||||
</ReactTooltip>
|
||||
{!isDisabled && (
|
||||
<Icon
|
||||
className={`eye-icon ${isVisible && '--visible'}`}
|
||||
name={isVisible ? 'eye' : 'eye-closed'}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={dcmrtClassNames}>
|
||||
<React.Fragment>
|
||||
{isDisabled ? (
|
||||
<OverlayTrigger
|
||||
key={index}
|
||||
placement="left"
|
||||
overlay={
|
||||
<Tooltip
|
||||
placement="left"
|
||||
className="in tooltip-warning"
|
||||
id="tooltip-left"
|
||||
>
|
||||
<div className="warningTitle">Unsupported Region</div>
|
||||
<div className="warningContent">
|
||||
Contour type currently unsupported.
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div>{tableListItem}</div>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<React.Fragment>{tableListItem} </React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</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,38 @@
|
||||
export default function getSourceDisplaySet(studies, rtStructDisplaySet) {
|
||||
const referencedDisplaySet = _getReferencedDisplaySet(
|
||||
rtStructDisplaySet,
|
||||
studies
|
||||
);
|
||||
|
||||
rtStructDisplaySet.load(referencedDisplaySet, studies);
|
||||
|
||||
return referencedDisplaySet;
|
||||
}
|
||||
|
||||
const _getReferencedDisplaySet = (rtStructDisplaySet, studies) => {
|
||||
let allDisplaySets = [];
|
||||
|
||||
studies.forEach(study => {
|
||||
allDisplaySets = allDisplaySets.concat(study.displaySets);
|
||||
});
|
||||
|
||||
const otherDisplaySets = allDisplaySets.filter(
|
||||
ds => ds.displaySetInstanceUID !== rtStructDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const ReferencedSeriesSequence = Array.isArray(
|
||||
rtStructDisplaySet.metadata.ReferencedSeriesSequence
|
||||
)
|
||||
? rtStructDisplaySet.metadata.ReferencedSeriesSequence
|
||||
: [rtStructDisplaySet.metadata.ReferencedSeriesSequence];
|
||||
|
||||
const referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
|
||||
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const referencedDisplaySet = otherDisplaySets.find(ds =>
|
||||
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
|
||||
);
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
const id = 'rt';
|
||||
|
||||
export default id;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import init from './init.js';
|
||||
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
|
||||
import id from './id.js';
|
||||
import RTPanel from './components/RTPanel/RTPanel';
|
||||
import { version } from '../package.json';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id,
|
||||
version,
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getPanelModule({ commandsManager, servicesManager, 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,326 @@
|
||||
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;
|
||||
}
|
||||
|
||||
const isSupported = false;
|
||||
|
||||
const ContourSequenceArray = _toArray(ContourSequence);
|
||||
|
||||
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
||||
const {
|
||||
ContourImageSequence,
|
||||
ContourData,
|
||||
NumberOfContourPoints,
|
||||
ContourGeometricType,
|
||||
} = ContourSequenceArray[c];
|
||||
|
||||
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
|
||||
toolState,
|
||||
imageId,
|
||||
rtStructDisplayToolName
|
||||
);
|
||||
|
||||
const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
|
||||
const points = [];
|
||||
let measurementData;
|
||||
|
||||
switch (ContourGeometricType) {
|
||||
case 'CLOSED_PLANAR':
|
||||
case 'OPEN_PLANAR':
|
||||
case 'POINT':
|
||||
isSupported = true;
|
||||
|
||||
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);
|
||||
|
||||
measurementData = {
|
||||
handles: {
|
||||
points,
|
||||
},
|
||||
type: ContourGeometricType,
|
||||
structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
|
||||
ROINumber: ReferencedROINumber,
|
||||
};
|
||||
|
||||
imageIdSpecificToolData.push(measurementData);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_setROIContourMetadata(
|
||||
structureSet,
|
||||
StructureSetROISequence,
|
||||
RTROIObservationsSequence,
|
||||
ROIContour,
|
||||
isSupported
|
||||
);
|
||||
}
|
||||
|
||||
_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,
|
||||
isSupported
|
||||
) {
|
||||
const StructureSetROI = StructureSetROISequence.find(
|
||||
structureSetROI =>
|
||||
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||
);
|
||||
|
||||
const ROIContourData = {
|
||||
ROINumber: StructureSetROI.ROINumber,
|
||||
ROIName: StructureSetROI.ROIName,
|
||||
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
|
||||
ROIDescription: StructureSetROI.ROIDescription,
|
||||
isSupported,
|
||||
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, image } = 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);
|
||||
}
|
||||
|
||||
if (image) {
|
||||
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(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function _toArray(objOrArray) {
|
||||
return Array.isArray(objOrArray) ? objOrArray : [objOrArray];
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
||||
import TOOL_NAMES from '../utils/toolNames';
|
||||
import drawCanvasCrosshairs from '../utils/drawCanvasCrosshairs';
|
||||
|
||||
// Cornerstone 3rd party dev kit imports
|
||||
const draw = importInternal('drawing/draw');
|
||||
const drawCircle = importInternal('drawing/drawCircle');
|
||||
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,
|
||||
highlightOpacity,
|
||||
} = rtstructModule.configuration;
|
||||
|
||||
// We have tool data for this element - iterate over each one and draw it
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
|
||||
let crossHairCenter;
|
||||
|
||||
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})`;
|
||||
|
||||
let highlight = data.highlight;
|
||||
const options = { color, lineWidth };
|
||||
|
||||
if (highlight) {
|
||||
crossHairCenter = { x: 0, y: 0 };
|
||||
|
||||
points.forEach(point => {
|
||||
crossHairCenter.x += point.x;
|
||||
crossHairCenter.y += point.y;
|
||||
});
|
||||
|
||||
crossHairCenter.x /= points.length;
|
||||
crossHairCenter.y /= points.length;
|
||||
|
||||
// TODO: Disabling hightlight for now, it'd be good to bring it back
|
||||
// when we have a good way of doing this for SEG.
|
||||
|
||||
// options.fillStyle = color = `rgba(${colorArray[0]},${colorArray[1]},${
|
||||
// colorArray[2]
|
||||
// },${highlightOpacity})`;
|
||||
|
||||
// Draw highlight lines.
|
||||
|
||||
delete data.highlight; // Don't highlight on next render.
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case 'CLOSED_PLANAR':
|
||||
this._renderClosedPlanar(context, eventData.element, points, options);
|
||||
break;
|
||||
case 'POINT':
|
||||
this._renderPoint(context, eventData.element, points, options);
|
||||
break;
|
||||
case 'OPEN_PLANAR':
|
||||
this._renderOpenPlanar(context, eventData.element, points, options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (crossHairCenter) {
|
||||
drawCanvasCrosshairs(eventData, crossHairCenter, {
|
||||
color: toolColors.getActiveColor(),
|
||||
lineWidth: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_renderClosedPlanar(context, element, points, options) {
|
||||
draw(context, context => {
|
||||
drawJoinedLines(
|
||||
context,
|
||||
element,
|
||||
points[points.length - 1],
|
||||
points,
|
||||
options
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_renderPoint(context, element, points, options) {
|
||||
draw(context, context => {
|
||||
drawCircle(context, element, points[0], 3, options);
|
||||
});
|
||||
}
|
||||
|
||||
_renderOpenPlanar(context, element, points, options) {
|
||||
draw(context, context => {
|
||||
drawJoinedLines(context, element, points[0], points, options);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
if (enabledElement.image) {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
lineWidth: 3,
|
||||
opacity: 0.75,
|
||||
highlightOpacity: 0.5,
|
||||
};
|
||||
|
||||
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.ROIContours.forEach(ROIContour => {
|
||||
ROIContour.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,56 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
const { importInternal } = cornerstoneTools;
|
||||
const draw = importInternal('drawing/draw');
|
||||
const drawLine = importInternal('drawing/drawLine');
|
||||
const getNewContext = importInternal('drawing/getNewContext');
|
||||
|
||||
export default function _drawCanvasCrosshairs(eventData, center, options) {
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
const { element } = eventData;
|
||||
|
||||
const centerCanvas = cornerstone.pixelToCanvas(element, center);
|
||||
|
||||
const { clientWidth: width, clientHeight: height } = element;
|
||||
|
||||
const offset = 10;
|
||||
|
||||
draw(context, context => {
|
||||
drawLine(
|
||||
context,
|
||||
element,
|
||||
{ x: centerCanvas.x + offset, y: centerCanvas.y },
|
||||
{ x: width, y: centerCanvas.y },
|
||||
options,
|
||||
'canvas'
|
||||
);
|
||||
|
||||
drawLine(
|
||||
context,
|
||||
element,
|
||||
{ x: centerCanvas.x - offset, y: centerCanvas.y },
|
||||
{ x: 0, y: centerCanvas.y },
|
||||
options,
|
||||
'canvas'
|
||||
);
|
||||
|
||||
drawLine(
|
||||
context,
|
||||
element,
|
||||
{ x: centerCanvas.x, y: centerCanvas.y + offset },
|
||||
{ x: centerCanvas.x, y: height },
|
||||
options,
|
||||
'canvas'
|
||||
);
|
||||
|
||||
drawLine(
|
||||
context,
|
||||
element,
|
||||
{ x: centerCanvas.x, y: centerCanvas.y - offset },
|
||||
{ x: centerCanvas.x, y: 0 },
|
||||
options,
|
||||
'canvas'
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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,190 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.4.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.5...@ohif/extension-dicom-segmentation@0.4.6) (2020-09-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.4...@ohif/extension-dicom-segmentation@0.4.5) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.3...@ohif/extension-dicom-segmentation@0.4.4) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.2...@ohif/extension-dicom-segmentation@0.4.3) (2020-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.1...@ohif/extension-dicom-segmentation@0.4.2) (2020-09-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.0...@ohif/extension-dicom-segmentation@0.4.1) (2020-08-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.3.4...@ohif/extension-dicom-segmentation@0.4.0) (2020-08-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Update react-vtkjs-viewport usage to use requestPool ([#1984](https://github.com/OHIF/Viewers/issues/1984)) ([bb5f30c](https://github.com/OHIF/Viewers/commit/bb5f30ce2a0192d2e021beaaadfff22fd38e17b9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.3.3...@ohif/extension-dicom-segmentation@0.3.4) (2020-08-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.3.2...@ohif/extension-dicom-segmentation@0.3.3) (2020-07-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.3.1...@ohif/extension-dicom-segmentation@0.3.2) (2020-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix RT Panel hide/show and Fix looping load errors ([#1877](https://github.com/OHIF/Viewers/issues/1877)) ([e7cc735](https://github.com/OHIF/Viewers/commit/e7cc735c03d02eeb0d3af4ba02c15ed4f81bbec2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.3.0...@ohif/extension-dicom-segmentation@0.3.1) (2020-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Disable seg panel when data for seg unavailable ([#1732](https://github.com/OHIF/Viewers/issues/1732)) ([698e900](https://github.com/OHIF/Viewers/commit/698e900b85121d3c2a46747c443ef69fb7a8c95b)), closes [#1728](https://github.com/OHIF/Viewers/issues/1728)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.1.1",
|
||||
"version": "0.4.6",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -30,14 +30,16 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"dcmjs": "^0.6.1",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.16.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"
|
||||
"gl-matrix": "^3.3.0",
|
||||
"react-select": "^3.0.8",
|
||||
"react-tooltip": "4.2.10"
|
||||
}
|
||||
}
|
||||
@@ -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,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TableListItem, Icon } from '@ohif/ui';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
import './SegmentItem.css';
|
||||
|
||||
@@ -17,48 +18,87 @@ 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...')}
|
||||
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 }}>
|
||||
<a data-tip data-for={`SegmentHover${index}`}>
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
<ReactTooltip
|
||||
id={`SegmentHover${index}`}
|
||||
delayShow={250}
|
||||
place="right"
|
||||
border={true}
|
||||
type="light"
|
||||
>
|
||||
<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>
|
||||
<span>{label}</span>
|
||||
</ReactTooltip>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</TableListItem>
|
||||
</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>
|
||||
Description
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableListItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentItem.propTypes = {
|
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
@@ -70,7 +110,7 @@ SegmentItem.propTypes = {
|
||||
|
||||
SegmentItem.defaultProps = {
|
||||
itemClass: '',
|
||||
onClick: () => { }
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
export default SegmentItem;
|
||||
@@ -8,6 +8,11 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel.disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel h3 {
|
||||
padding-left: 20px;
|
||||
margin-top: 0;
|
||||
|
||||
+379
-181
@@ -1,10 +1,14 @@
|
||||
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';
|
||||
import moment from 'moment';
|
||||
import { utils, log } from '@ohif/core';
|
||||
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
|
||||
import DICOMSegTempCrosshairsTool from '../../tools/DICOMSegTempCrosshairsTool';
|
||||
|
||||
import setActiveLabelmap from '../../utils/setActiveLabelMap';
|
||||
import refreshViewports from '../../utils/refreshViewports';
|
||||
|
||||
import {
|
||||
BrushColorSelector,
|
||||
@@ -19,37 +23,59 @@ import SegmentationSettings from '../SegmentationSettings/SegmentationSettings';
|
||||
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
const refreshViewport = () => {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 { configuration } = cornerstoneTools.getModule('segmentation');
|
||||
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
|
||||
|
||||
/*
|
||||
* 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)', /* TODO: We shouldn't hardcode this color, in the future the SEG may set the colorLUT to whatever it wants. */
|
||||
brushColor: 'rgba(221, 85, 85, 1)',
|
||||
selectedSegment: null,
|
||||
selectedSegmentation: null,
|
||||
showSegSettings: false,
|
||||
showSegmentationSettings: false,
|
||||
brushStackState: null,
|
||||
labelmapList: [],
|
||||
segmentList: []
|
||||
segmentList: [],
|
||||
cachedSegmentsProperties: [],
|
||||
isLoading: false,
|
||||
isDisabled: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,10 +83,37 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
log.warn('Segmentation Panel: labelmap modified', event);
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const firstImageId = studyMetadata.getFirstImageId(activeViewport.displaySetInstanceUID);
|
||||
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?
|
||||
@@ -74,6 +127,10 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
'extensiondicomsegmentationsegloaded',
|
||||
refreshSegmentations
|
||||
);
|
||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||
enabledElement.removeEventListener(
|
||||
'cornerstonetoolslabelmapmodified',
|
||||
@@ -81,154 +138,324 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
)
|
||||
);
|
||||
};
|
||||
});
|
||||
}, [activeIndex, viewports]);
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
setState(state => ({
|
||||
...state,
|
||||
brushStackState,
|
||||
selectedSegmentation: brushStackState.activeLabelmapIndex,
|
||||
labelmapList,
|
||||
segmentList
|
||||
}));
|
||||
} else {
|
||||
setState(state => ({
|
||||
...state,
|
||||
labelmapList: [],
|
||||
segmentList: [],
|
||||
}));
|
||||
const isDisabled = !activeViewport || !activeViewport.StudyInstanceUID;
|
||||
if (!isDisabled) {
|
||||
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,
|
||||
isDisabled,
|
||||
}));
|
||||
} else {
|
||||
setState(state => ({
|
||||
...state,
|
||||
labelmapList: [],
|
||||
segmentList: [],
|
||||
isDisabled,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [studies, viewports, activeIndex]);
|
||||
}, [viewports, activeIndex, state.isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshSegmentations();
|
||||
}, [
|
||||
viewports,
|
||||
activeIndex,
|
||||
isOpen,
|
||||
state.selectedSegmentation,
|
||||
activeContexts,
|
||||
state.isLoading,
|
||||
]);
|
||||
|
||||
/* Handle open/closed panel behaviour */
|
||||
useEffect(() => {
|
||||
updateState('showSegSettings', state.showSegSettings && !isOpen);
|
||||
setState(state => ({
|
||||
...state,
|
||||
showSegmentationSettings: state.showSegmentationSettings && !isOpen,
|
||||
}));
|
||||
}, [isOpen]);
|
||||
|
||||
const getLabelmapList = (brushStackState, firstImageId, activeViewport) => {
|
||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||
activeViewport.StudyInstanceUID,
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
const getLabelmapList = useCallback(
|
||||
(brushStackState, firstImageId, activeViewport) => {
|
||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||
activeViewport.StudyInstanceUID,
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
||||
|
||||
/* 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;
|
||||
/* 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;
|
||||
|
||||
return {
|
||||
value: labelmapIndex,
|
||||
title: displayDescription,
|
||||
description: displayDate,
|
||||
onClick: async () => {
|
||||
const activatedLabelmapIndex = await _setActiveLabelmap(
|
||||
activeViewport,
|
||||
studies,
|
||||
displaySet,
|
||||
firstImageId,
|
||||
brushStackState.activeLabelmapIndex
|
||||
);
|
||||
updateState('selectedSegmentation', activatedLabelmapIndex);
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
return {
|
||||
value: labelmapIndex,
|
||||
title: displayDescription,
|
||||
description: displayDate,
|
||||
onClick: async () => {
|
||||
const activatedLabelmapIndex = await setActiveLabelmap(
|
||||
activeViewport,
|
||||
studies,
|
||||
displaySet,
|
||||
() => onSelectedSegmentationChange(),
|
||||
onDisplaySetLoadFailure
|
||||
);
|
||||
updateState('selectedSegmentation', activatedLabelmapIndex);
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[studies]
|
||||
);
|
||||
|
||||
const getSegmentList = (labelmap3D, firstImageId) => {
|
||||
/*
|
||||
* 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;
|
||||
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]);
|
||||
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 module = cornerstoneTools.getModule('segmentation');
|
||||
const colorLutTable =
|
||||
module.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;
|
||||
});
|
||||
|
||||
const segmentList = [];
|
||||
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
|
||||
);
|
||||
|
||||
DICOMSegTempCrosshairsTool.addCrosshair(
|
||||
element,
|
||||
imageId,
|
||||
segmentNumber
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
refreshViewports();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sameSegment = state.selectedSegment === segmentNumber;
|
||||
const setCurrentSelectedSegment = () => {
|
||||
_setActiveSegment(
|
||||
firstImageId,
|
||||
segmentNumber,
|
||||
labelmap3D.activeSegmentIndex
|
||||
);
|
||||
updateState('selectedSegment', 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]
|
||||
);
|
||||
|
||||
return segmentList;
|
||||
const updateCachedSegmentsProperties = (segmentNumber, properties) => {
|
||||
const segmentsProperties = state.cachedSegmentsProperties;
|
||||
const segmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
segmentsProperties[segmentNumber] = segmentProperties
|
||||
? { ...segmentProperties, ...properties }
|
||||
: properties;
|
||||
|
||||
updateState('cachedSegmentsProperties', segmentsProperties);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateState('cachedSegmentsProperties', []);
|
||||
}, [activeContexts]);
|
||||
|
||||
const updateState = (field, value) => {
|
||||
setState(state => ({ ...state, [field]: value }));
|
||||
};
|
||||
@@ -270,15 +497,13 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
}
|
||||
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const colorLutTable =
|
||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
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 =
|
||||
@@ -288,26 +513,35 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
configuration.outlineWidth = newConfiguration.outlineWidth;
|
||||
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
|
||||
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
|
||||
refreshViewport();
|
||||
onConfigurationChange(newConfiguration);
|
||||
refreshViewports();
|
||||
};
|
||||
|
||||
if (state.showSegSettings) {
|
||||
const disabledConfigurationFields = [
|
||||
'outlineAlpha',
|
||||
'shouldRenderInactiveLabelmaps',
|
||||
];
|
||||
if (state.showSegmentationSettings) {
|
||||
return (
|
||||
<SegmentationSettings
|
||||
disabledFields={isVTK() ? disabledConfigurationFields : []}
|
||||
configuration={configuration}
|
||||
onBack={() => updateState('showSegSettings', false)}
|
||||
onBack={() => updateState('showSegmentationSettings', false)}
|
||||
onChange={updateConfiguration}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="dcmseg-segmentation-panel">
|
||||
<div
|
||||
className={`dcmseg-segmentation-panel ${state.isDisabled &&
|
||||
'disabled'}`}
|
||||
>
|
||||
<Icon
|
||||
className="cog-icon"
|
||||
name="cog"
|
||||
width="25px"
|
||||
height="25px"
|
||||
onClick={() => updateState('showSegSettings', true)}
|
||||
onClick={() => updateState('showSegmentationSettings', true)}
|
||||
/>
|
||||
{false && (
|
||||
<form className="selector-form">
|
||||
@@ -328,9 +562,9 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
<h3>Segmentations</h3>
|
||||
<div className="segmentations">
|
||||
<SegmentationSelect
|
||||
value={
|
||||
state.labelmapList.find(i => i.value === state.selectedSegmentation) || null
|
||||
}
|
||||
value={state.labelmapList.find(
|
||||
i => i.value === state.selectedSegmentation
|
||||
)}
|
||||
formatOptionLabel={SegmentationItem}
|
||||
options={state.labelmapList}
|
||||
/>
|
||||
@@ -371,6 +605,7 @@ SegmentationPanel.propTypes = {
|
||||
}),
|
||||
activeIndex: PropTypes.number.isRequired,
|
||||
studies: PropTypes.array.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
SegmentationPanel.defaultProps = {};
|
||||
|
||||
@@ -399,43 +634,6 @@ const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
|
||||
return referencedDisplaysets;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} viewportSpecificData
|
||||
* @param {*} studies
|
||||
* @param {*} displaySet
|
||||
* @param {*} firstImageId
|
||||
* @param {*} activeLabelmapIndex
|
||||
* @returns
|
||||
*/
|
||||
const _setActiveLabelmap = async (
|
||||
viewportSpecificData,
|
||||
studies,
|
||||
displaySet,
|
||||
firstImageId,
|
||||
activeLabelmapIndex
|
||||
) => {
|
||||
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
||||
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
||||
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 { state } = cornerstoneTools.getModule('segmentation');
|
||||
const brushStackState = state.series[firstImageId];
|
||||
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
|
||||
|
||||
refreshViewport();
|
||||
|
||||
return displaySet.labelmapIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} firstImageId
|
||||
@@ -455,7 +653,7 @@ const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex) => {
|
||||
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||
labelmap3D.activeSegmentIndex = segmentIndex;
|
||||
|
||||
refreshViewport();
|
||||
refreshViewports();
|
||||
|
||||
return segmentIndex;
|
||||
};
|
||||
|
||||
@@ -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,72 @@
|
||||
import { MODULE_TYPES, utils } from '@ohif/core';
|
||||
import loadSegmentation from './loadSegmentation';
|
||||
import getSourceDisplaySet from './getSourceDisplaySet';
|
||||
|
||||
// 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,
|
||||
SeriesNumber,
|
||||
} = 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,
|
||||
SeriesNumber,
|
||||
SeriesDescription,
|
||||
metadata,
|
||||
};
|
||||
|
||||
segDisplaySet.getSourceDisplaySet = function(studies) {
|
||||
return getSourceDisplaySet(studies, segDisplaySet);
|
||||
};
|
||||
|
||||
segDisplaySet.load = function(referencedDisplaySet, studies) {
|
||||
return loadSegmentation(segDisplaySet, referencedDisplaySet, studies);
|
||||
};
|
||||
|
||||
return segDisplaySet;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import setActiveLabelmap from './utils/setActiveLabelMap';
|
||||
import { classes } from '@ohif/core';
|
||||
|
||||
const { ImageSet } = classes;
|
||||
|
||||
export default function getSourceDisplaySet(studies, segDisplaySet) {
|
||||
const referencedDisplaySet = _getReferencedDisplaySet(segDisplaySet, studies);
|
||||
|
||||
setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet);
|
||||
|
||||
return referencedDisplaySet;
|
||||
}
|
||||
|
||||
const _getReferencedDisplaySet = (segDisplaySet, studies) => {
|
||||
let allDisplaySets = [];
|
||||
|
||||
studies.forEach(study => {
|
||||
allDisplaySets = allDisplaySets.concat(study.displaySets);
|
||||
});
|
||||
|
||||
const otherDisplaySets = allDisplaySets.filter(
|
||||
ds => ds.displaySetInstanceUID !== segDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const { metadata } = segDisplaySet;
|
||||
|
||||
let referencedSeriesInstanceUIDs;
|
||||
|
||||
if (metadata.ReferencedSeriesSequence) {
|
||||
const ReferencedSeriesSequence = _toArray(
|
||||
metadata.ReferencedSeriesSequence
|
||||
);
|
||||
|
||||
referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
|
||||
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
const { PerFrameFunctionalGroupsSequence } = metadata;
|
||||
|
||||
let SourceImageSequence;
|
||||
|
||||
if (metadata.SourceImageSequence) {
|
||||
SourceImageSequence = metadata.SourceImageSequence;
|
||||
} else {
|
||||
const firstFunctionalGroups = _toArray(
|
||||
PerFrameFunctionalGroupsSequence
|
||||
)[0];
|
||||
const { DerivationImageSequence } = firstFunctionalGroups;
|
||||
|
||||
SourceImageSequence = DerivationImageSequence;
|
||||
}
|
||||
|
||||
const firstSourceImage = _toArray(SourceImageSequence)[0];
|
||||
|
||||
const { ReferencedSOPInstanceUID } = firstSourceImage;
|
||||
|
||||
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
|
||||
otherDisplaySets,
|
||||
ReferencedSOPInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
const referencedDisplaySet = otherDisplaySets.find(ds =>
|
||||
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
|
||||
);
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
|
||||
const _findReferencedSeriesInstanceUIDsFromSOPInstanceUID = (
|
||||
displaySets,
|
||||
SOPInstanceUID
|
||||
) => {
|
||||
const imageSets = displaySets.filter(ds => ds instanceof ImageSet);
|
||||
|
||||
for (let i = 0; i < imageSets.length; i++) {
|
||||
const { images } = imageSets[i];
|
||||
for (let j = 0; j < images.length; j++) {
|
||||
if (images[j].SOPInstanceUID === SOPInstanceUID) {
|
||||
return [images[j].getData().metadata.SeriesInstanceUID];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _toArray(arrayOrObject) {
|
||||
return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject];
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
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';
|
||||
import { version } from '../package.json';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'com.ohif.dicom-segmentation',
|
||||
version,
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -21,10 +24,97 @@ export default {
|
||||
getToolbarModule({ servicesManager }) {
|
||||
return toolbarModule;
|
||||
},
|
||||
getPanelModule({ servicesManager }) {
|
||||
return panelModule;
|
||||
},
|
||||
getSopClassHandlerModule({ servicesManager }) {
|
||||
return sopClassHandlerModule;
|
||||
getPanelModule({ commandsManager, api, servicesManager }) {
|
||||
const { UINotificationService } = servicesManager.services;
|
||||
|
||||
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,4 +1,5 @@
|
||||
import csTools from 'cornerstone-tools';
|
||||
import DICOMSegTempCrosshairsTool from './tools/DICOMSegTempCrosshairsTool';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -17,4 +18,6 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
alwaysEraseOnClick: true,
|
||||
},
|
||||
});
|
||||
|
||||
csTools.addTool(DICOMSegTempCrosshairsTool);
|
||||
}
|
||||
@@ -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,59 @@ 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;
|
||||
segDisplaySet.loadError = true;
|
||||
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 +110,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'],
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import cornerstoneTools, {
|
||||
importInternal,
|
||||
getToolState,
|
||||
toolColors,
|
||||
getModule,
|
||||
globalImageIdSpecificToolStateManager,
|
||||
} from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import drawCanvasCrosshairs from '../utils/drawCanvasCrosshairs';
|
||||
import TOOL_NAMES from './TOOL_NAMES';
|
||||
|
||||
const { DICOM_SEG_TEMP_CROSSHAIRS_TOOL } = TOOL_NAMES;
|
||||
const { getters } = getModule('segmentation');
|
||||
|
||||
// Cornerstone 3rd party dev kit imports
|
||||
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 DICOMSegTempCrosshairsTool extends BaseTool {
|
||||
constructor(props = {}) {
|
||||
const defaultProps = {
|
||||
mixins: ['enabledOrDisabledBinaryTool'],
|
||||
name: DICOM_SEG_TEMP_CROSSHAIRS_TOOL,
|
||||
};
|
||||
|
||||
const initialProps = Object.assign(defaultProps, props);
|
||||
|
||||
super(initialProps);
|
||||
|
||||
this._rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||
}
|
||||
|
||||
renderToolData(evt) {
|
||||
const eventData = evt.detail;
|
||||
const { element } = eventData;
|
||||
const toolState = getToolState(evt.currentTarget, this.name);
|
||||
|
||||
if (!toolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We have tool data for this element - iterate over each one and draw it
|
||||
|
||||
for (let i = 0; i < toolState.data.length; i++) {
|
||||
const data = toolState.data[i];
|
||||
const crossHairCenter = data.center;
|
||||
|
||||
drawCanvasCrosshairs(eventData, crossHairCenter, {
|
||||
color: toolColors.getActiveColor(),
|
||||
lineWidth: 1,
|
||||
});
|
||||
|
||||
// Remove the crosshairs, we only render them for one redraw.
|
||||
toolState.data.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DICOMSegTempCrosshairsTool.addCrosshair = (element, imageId, segmentNumber) => {
|
||||
const labelmap3D = getters.labelmap3D(element);
|
||||
const stackToolState = cornerstoneTools.getToolState(element, 'stack');
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
|
||||
const { rows, columns } = enabledElement.image;
|
||||
|
||||
if (!stackToolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = stackToolState.data[0].imageIds;
|
||||
const imageIdIndex = imageIds.findIndex(imgId => imgId === imageId);
|
||||
|
||||
const labelmap2D = labelmap3D.labelmaps2D[imageIdIndex];
|
||||
const { pixelData } = labelmap2D;
|
||||
|
||||
let xCenter = 0;
|
||||
let yCenter = 0;
|
||||
|
||||
let count = 0;
|
||||
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < columns; x++) {
|
||||
if (pixelData[y * columns + x] === segmentNumber) {
|
||||
count++;
|
||||
xCenter += x + 0.5;
|
||||
yCenter += y + 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xCenter /= count;
|
||||
yCenter /= count;
|
||||
|
||||
const globalToolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||
|
||||
if (!globalToolState[imageId]) {
|
||||
globalToolState[imageId] = {};
|
||||
}
|
||||
|
||||
const imageIdSpecificToolState = globalToolState[imageId];
|
||||
|
||||
if (!imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL]) {
|
||||
imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL] = { data: [] };
|
||||
} else if (!imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL].data) {
|
||||
imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL].data = [];
|
||||
}
|
||||
|
||||
const toolSpecificData =
|
||||
imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL].data;
|
||||
|
||||
toolSpecificData.push({ center: { x: xCenter, y: yCenter }, segmentNumber });
|
||||
|
||||
// Enable the tool if not enabled for the element.
|
||||
|
||||
const tool = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
DICOM_SEG_TEMP_CROSSHAIRS_TOOL
|
||||
);
|
||||
|
||||
if (tool.mode !== 'enabled') {
|
||||
// If not already active or passive, set passive so contours render.
|
||||
cornerstoneTools.setToolEnabled(DICOM_SEG_TEMP_CROSSHAIRS_TOOL);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const TOOL_NAMES = {
|
||||
DICOM_SEG_TEMP_CROSSHAIRS_TOOL: 'DICOMSegTempCrosshairsTool',
|
||||
};
|
||||
|
||||
export default TOOL_NAMES;
|
||||
@@ -0,0 +1,56 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
const { importInternal } = cornerstoneTools;
|
||||
const draw = importInternal('drawing/draw');
|
||||
const drawLine = importInternal('drawing/drawLine');
|
||||
const getNewContext = importInternal('drawing/getNewContext');
|
||||
|
||||
export default function _drawCanvasCrosshairs(eventData, center, options) {
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
const { element } = eventData;
|
||||
|
||||
const centerCanvas = cornerstone.pixelToCanvas(element, center);
|
||||
|
||||
const { clientWidth: width, clientHeight: height } = element;
|
||||
|
||||
const offset = 10;
|
||||
|
||||
draw(context, context => {
|
||||
drawLine(
|
||||
context,
|
||||
element,
|
||||
{ x: centerCanvas.x + offset, y: centerCanvas.y },
|
||||
{ x: width, y: centerCanvas.y },
|
||||
options,
|
||||
'canvas'
|
||||
);
|
||||
|
||||
drawLine(
|
||||
context,
|
||||
element,
|
||||
{ x: centerCanvas.x - offset, y: centerCanvas.y },
|
||||
{ x: 0, y: centerCanvas.y },
|
||||
options,
|
||||
'canvas'
|
||||
);
|
||||
|
||||
drawLine(
|
||||
context,
|
||||
element,
|
||||
{ x: centerCanvas.x, y: centerCanvas.y + offset },
|
||||
{ x: centerCanvas.x, y: height },
|
||||
options,
|
||||
'canvas'
|
||||
);
|
||||
|
||||
drawLine(
|
||||
context,
|
||||
element,
|
||||
{ x: centerCanvas.x, y: centerCanvas.y - offset },
|
||||
{ x: centerCanvas.x, y: 0 },
|
||||
options,
|
||||
'canvas'
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default function refreshViewports() {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { utils, log } from '@ohif/core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import refreshViewports from './refreshViewports';
|
||||
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} viewportSpecificData
|
||||
* @param {*} studies
|
||||
* @param {*} displaySet
|
||||
* @param {*} firstImageId
|
||||
* @param {*} activeLabelmapIndex
|
||||
* @returns
|
||||
*/
|
||||
export default async function setActiveLabelmap(
|
||||
referencedDisplaySet,
|
||||
studies,
|
||||
displaySet,
|
||||
callback = () => {},
|
||||
onDisplaySetLoadFailure = err => {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
) {
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
referencedDisplaySet.StudyInstanceUID
|
||||
);
|
||||
const firstImageId = studyMetadata.getFirstImageId(
|
||||
referencedDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
let { state } = cornerstoneTools.getModule('segmentation');
|
||||
|
||||
let brushStackState = state.series[firstImageId];
|
||||
const activeLabelmapIndex = brushStackState
|
||||
? brushStackState.activeLabelmapIndex
|
||||
: undefined;
|
||||
|
||||
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
||||
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
||||
return displaySet.labelmapIndex;
|
||||
}
|
||||
|
||||
if (!displaySet.isLoaded) {
|
||||
const loadPromise = displaySet.load(referencedDisplaySet, studies);
|
||||
|
||||
loadPromise.catch(error => {
|
||||
onDisplaySetLoadFailure(error);
|
||||
|
||||
// Return old index.
|
||||
return activeLabelmapIndex;
|
||||
});
|
||||
|
||||
await loadPromise;
|
||||
}
|
||||
|
||||
// This might have just been created, so need to use the non-cached value.
|
||||
state = cornerstoneTools.getModule('segmentation').state;
|
||||
brushStackState = state.series[firstImageId];
|
||||
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
|
||||
|
||||
refreshViewports();
|
||||
callback();
|
||||
|
||||
return displaySet.labelmapIndex;
|
||||
}
|
||||
@@ -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,8 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## 0.0.2 (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
@@ -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-tag-browser
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-tag-browser",
|
||||
"version": "0.0.2",
|
||||
"description": "OHIF extension for checking DICOM headers.",
|
||||
"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",
|
||||
"dcmjs": "0.16.0",
|
||||
"react": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"moment": "2.24.0",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
|
||||
const DicomBrowserSelect = ({ value, formatOptionLabel, options }) => (
|
||||
<Select
|
||||
value={value}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={dicomBrowserSelectStyles}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
|
||||
const computedstyle = getComputedStyle(document.body);
|
||||
const uiGrayDarker = computedstyle.getPropertyValue('--ui-gray-darker');
|
||||
const activeColor = computedstyle.getPropertyValue('--active-color');
|
||||
const defaultColor = computedstyle.getPropertyValue('--default-color');
|
||||
const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
|
||||
|
||||
const dicomBrowserSelectStyles = {
|
||||
singleValue: (base, state) => ({
|
||||
...base,
|
||||
width: '100%',
|
||||
}),
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
cursor: 'pointer',
|
||||
background: uiGrayDarker,
|
||||
borderRadius: state.isFocused ? '5px 5px 5px 5px' : 5,
|
||||
borderColor: state.isFocused ? activeColor : defaultColor,
|
||||
minHeight: '50px',
|
||||
'&:hover': {
|
||||
borderColor: activeColor,
|
||||
},
|
||||
}),
|
||||
menu: base => ({
|
||||
...base,
|
||||
borderRadius: 5,
|
||||
background: uiGrayDarker,
|
||||
}),
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
cursor: 'pointer',
|
||||
'&:first-of-type': {
|
||||
borderTopLeftRadius: 5,
|
||||
borderTopRightRadius: 5,
|
||||
},
|
||||
'&:last-of-type': {
|
||||
borderBottomLeftRadius: 5,
|
||||
borderBottomRightRadius: 5,
|
||||
},
|
||||
background: state.isSelected ? uiGrayDark : uiGrayDarker,
|
||||
'&:hover': {
|
||||
background: uiGrayDark,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default DicomBrowserSelect;
|
||||
@@ -0,0 +1,25 @@
|
||||
.dcmseg-segmentation-item {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-item .segmentation-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-item .segmentation-meta-title {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
max-width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-item .segmentation-meta-description {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './DicomBrowserSelectItem.css';
|
||||
|
||||
const DicomBrowserSelectItem = ({ onClick, title, description }) => {
|
||||
return (
|
||||
<li className="dcmseg-segmentation-item" onClick={onClick}>
|
||||
<div className="segmentation-meta">
|
||||
<div className="segmentation-meta-title">{title}</div>
|
||||
<div className="segmentation-meta-description">{description}</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
DicomBrowserSelectItem.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
DicomBrowserSelectItem.defaultProps = {
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default DicomBrowserSelectItem;
|
||||
@@ -0,0 +1,27 @@
|
||||
.dicom-tag-browser-table {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-table td {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
color: var(--table-text-primary-color);
|
||||
border-top: 1px solid #ddd;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-table td.dicom-tag-browser-table-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-table th {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
text-align: center;
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.dicom-tag-browser-table th.dicom-tag-browser-table-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import React, { useState } from 'react';
|
||||
import { classes } from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import DicomBrowserSelect from './DicomBrowserSelect';
|
||||
import moment from 'moment';
|
||||
import './DicomTagBrowser.css';
|
||||
import DicomBrowserSelectItem from './DicomBrowserSelectItem';
|
||||
|
||||
const { ImageSet } = classes;
|
||||
const { DicomMetaDictionary } = dcmjs.data;
|
||||
const { nameMap } = DicomMetaDictionary;
|
||||
|
||||
const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
const [
|
||||
activeDisplaySetInstanceUID,
|
||||
setActiveDisplaySetInstanceUID,
|
||||
] = useState(displaySetInstanceUID);
|
||||
const [activeInstance, setActiveInstance] = useState(0);
|
||||
|
||||
const activeDisplaySet = displaySets.find(
|
||||
ds => ds.displaySetInstanceUID === activeDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
const displaySetList = displaySets.map(displaySet => {
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesNumber,
|
||||
SeriesDescription,
|
||||
Modality,
|
||||
} = displaySet;
|
||||
|
||||
/* Map to display representation */
|
||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||
|
||||
return {
|
||||
value: displaySetInstanceUID,
|
||||
title: `${SeriesNumber} (${Modality}): ${SeriesDescription}`,
|
||||
description: displayDate,
|
||||
onClick: () => {
|
||||
setActiveDisplaySetInstanceUID(displaySetInstanceUID);
|
||||
setActiveInstance(0);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
let metadata;
|
||||
const isImageStack = activeDisplaySet instanceof ImageSet;
|
||||
|
||||
let selectedInstanceValue;
|
||||
let instanceList;
|
||||
|
||||
if (isImageStack) {
|
||||
const { images } = activeDisplaySet;
|
||||
const image = images[activeInstance];
|
||||
|
||||
instanceList = images.map((image, index) => {
|
||||
const metadata = image.getData().metadata;
|
||||
|
||||
const { InstanceNumber } = metadata;
|
||||
|
||||
return {
|
||||
value: index,
|
||||
title: `Instance Number: ${InstanceNumber}`,
|
||||
description: '',
|
||||
onClick: () => {
|
||||
setActiveInstance(index);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
selectedInstanceValue = instanceList[activeInstance];
|
||||
|
||||
metadata = image.getData().metadata;
|
||||
} else {
|
||||
metadata = activeDisplaySet.metadata;
|
||||
}
|
||||
|
||||
const selectedDisplaySetValue = displaySetList.find(
|
||||
ds => ds.value === activeDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DicomBrowserSelect
|
||||
value={selectedDisplaySetValue}
|
||||
formatOptionLabel={DicomBrowserSelectItem}
|
||||
options={displaySetList}
|
||||
/>
|
||||
{isImageStack ? (
|
||||
<DicomBrowserSelect
|
||||
value={selectedInstanceValue}
|
||||
formatOptionLabel={DicomBrowserSelectItem}
|
||||
options={instanceList}
|
||||
/>
|
||||
) : null}
|
||||
<DicomTagTable instanceMetadata={metadata}></DicomTagTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function DicomTagTable({ instanceMetadata }) {
|
||||
const rows = getRows(instanceMetadata);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="dicom-tag-browser-table">
|
||||
<tr>
|
||||
<th className="dicom-tag-browser-table-left">Tag</th>
|
||||
<th className="dicom-tag-browser-table-left">Value Representation</th>
|
||||
<th className="dicom-tag-browser-table-left">Keyword</th>
|
||||
<th className="dicom-tag-browser-table-left">Value</th>
|
||||
</tr>
|
||||
{rows.map(row => (
|
||||
<tr>
|
||||
<td>{row[0]}</td>
|
||||
<td>{row[1]}</td>
|
||||
<td>{row[2]}</td>
|
||||
<td>{row[3]}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getRows(metadata, depth = 0) {
|
||||
// Tag, Type, Value, Keyword
|
||||
|
||||
const keywords = Object.keys(metadata);
|
||||
|
||||
let tagIndent = '';
|
||||
|
||||
for (let i = 0; i < depth; i++) {
|
||||
tagIndent += '>';
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
|
||||
for (let i = 0; i < keywords.length; i++) {
|
||||
let keyword = keywords[i];
|
||||
|
||||
if (keyword === '_vrMap') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagInfo = nameMap[keyword];
|
||||
|
||||
let value = metadata[keyword];
|
||||
|
||||
if (tagInfo && tagInfo.vr === 'SQ') {
|
||||
const sequenceAsArray = toArray(value);
|
||||
|
||||
// Push line defining the sequence
|
||||
rows.push([`${tagIndent}${tagInfo.tag}`, tagInfo.vr, keyword, '']);
|
||||
|
||||
if (value === null) {
|
||||
// Type 2 Sequence
|
||||
continue;
|
||||
}
|
||||
|
||||
sequenceAsArray.forEach(item => {
|
||||
const sequenceRows = getRows(item, depth + 1);
|
||||
|
||||
sequenceRows.forEach(row => {
|
||||
rows.push(row);
|
||||
});
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value = value.join('\\');
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
if (value === null) {
|
||||
value = ' ';
|
||||
} else {
|
||||
if (typeof value === 'object') {
|
||||
if (value.InlineBinary) {
|
||||
value = 'Inline Binary';
|
||||
} else if (value.BulkDataURI) {
|
||||
value = `Bulk Data URI`; //: ${value.BulkDataURI}`;
|
||||
} else if (value.Alphabetic) {
|
||||
value = value.Alphabetic;
|
||||
} else {
|
||||
console.error('Unrecognised Value for element:');
|
||||
console.error(value);
|
||||
value = ' ';
|
||||
}
|
||||
} else {
|
||||
console.error('Unrecognised Value for element:');
|
||||
console.error(value);
|
||||
value = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove retired tags
|
||||
keyword = keyword.replace('RETIRED_', '');
|
||||
|
||||
if (tagInfo) {
|
||||
rows.push([`${tagIndent}${tagInfo.tag}`, tagInfo.vr, keyword, value]);
|
||||
} else {
|
||||
// Private tag
|
||||
const tag = `(${keyword.substring(0, 4)},${keyword.substring(4, 8)})`;
|
||||
|
||||
rows.push([`${tagIndent}${tag}`, '', 'Private Tag', value]);
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function toArray(objectOrArray) {
|
||||
return Array.isArray(objectOrArray) ? objectOrArray : [objectOrArray];
|
||||
}
|
||||
|
||||
export default DicomTagBrowser;
|
||||
Loaded 100 of 273 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user