Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -127,10 +127,107 @@ appropriate headers. You can find an example of this setup in our
|
|||||||
|
|
||||||
#### What if my archive doesn't support DicomWeb?
|
#### What if my archive doesn't support DicomWeb?
|
||||||
|
|
||||||
> This is possible to do with the OHIF Viewer, but not as straightforward. Look
|
It's possible to supply all Study data via JSON format, in the event you do not have a DicomWeb endpoint.
|
||||||
> out for documentation on this subject in the near future.
|
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
|
### Securing Your Data
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ are:
|
|||||||
```js
|
```js
|
||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
/**
|
||||||
|
* "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: {
|
whiteLabeling: {
|
||||||
/* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
/* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
||||||
createLogoComponentFn: function(React) {
|
createLogoComponentFn: function(React) {
|
||||||
@@ -30,6 +38,32 @@ window.config = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 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: [],
|
extensions: [],
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
filterQueryParam: false,
|
filterQueryParam: false,
|
||||||
|
|||||||
@@ -3,6 +3,91 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.8.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.8.0...@ohif/extension-cornerstone@2.8.1) (2020-06-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Infinite frame index change when quickly jumping between ([#1734](https://github.com/OHIF/Viewers/issues/1734)) ([a830577](https://github.com/OHIF/Viewers/commit/a8305772d0fbec506549ad7ea08504a8bc3a4f39)), closes [#1733](https://github.com/OHIF/Viewers/issues/1733)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.8.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.3...@ohif/extension-cornerstone@2.8.0) (2020-06-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.2...@ohif/extension-cornerstone@2.7.3) (2020-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.1...@ohif/extension-cornerstone@2.7.2) (2020-05-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.0...@ohif/extension-cornerstone@2.7.1) (2020-05-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.1...@ohif/extension-cornerstone@2.7.0) (2020-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.0...@ohif/extension-cornerstone@2.6.1) (2020-04-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Multiframe fix ([#1661](https://github.com/OHIF/Viewers/issues/1661)) ([7120561](https://github.com/OHIF/Viewers/commit/71205618ecb8b592247c5acb32284bfe7e18fce5))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.2...@ohif/extension-cornerstone@2.6.0) (2020-04-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.1...@ohif/extension-cornerstone@2.5.2) (2020-04-09)
|
## [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
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "2.5.2",
|
"version": "2.8.1",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.15.1",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.1.0",
|
||||||
"dcmjs": "^0.10.1",
|
"dcmjs": "^0.12.3",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@@ -51,6 +51,6 @@
|
|||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"react-cornerstone-viewport": "^2.3.6"
|
"react-cornerstone-viewport": "2.3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
displaySet.displaySetInstanceUID !==
|
displaySet.displaySetInstanceUID !==
|
||||||
prevDisplaySet.displaySetInstanceUID ||
|
prevDisplaySet.displaySetInstanceUID ||
|
||||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConnectedCornerstoneViewport
|
<ConnectedCornerstoneViewport
|
||||||
viewportIndex={viewportIndex}
|
viewportIndex={viewportIndex}
|
||||||
imageIds={imageIds}
|
imageIds={imageIds}
|
||||||
imageIdIndex={currentImageIdIndex}
|
imageIdIndex={currentImageIdIndex}
|
||||||
|
onNewImage={newImageHandler}
|
||||||
|
onNewImageDebounceTime={700}
|
||||||
// ~~ Connected (From REDUX)
|
// ~~ Connected (From REDUX)
|
||||||
// frameRate={frameRate}
|
// frameRate={frameRate}
|
||||||
// isPlaying={false}
|
// isPlaying={false}
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
const study = studyMetadataManager.get(StudyInstanceUID);
|
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||||
|
|
||||||
const displaySet = study.findDisplaySet(ds => {
|
const displaySet = study.findDisplaySet(ds => {
|
||||||
return ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
|
return ds.images && ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
|
||||||
});
|
});
|
||||||
|
|
||||||
displaySet.SOPInstanceUID = SOPInstanceUID;
|
displaySet.SOPInstanceUID = SOPInstanceUID;
|
||||||
|
|||||||
@@ -34,8 +34,17 @@ export default {
|
|||||||
preRegistration({ servicesManager, configuration = {} }) {
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
init({ servicesManager, configuration });
|
init({ servicesManager, configuration });
|
||||||
},
|
},
|
||||||
getViewportModule() {
|
getViewportModule({ commandsManager }) {
|
||||||
return OHIFCornerstoneViewport;
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
|
const onNewImageHandler = jumpData => {
|
||||||
|
commandsManager.runCommand('jumpToImage', jumpData);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return ExtendedOHIFCornerstoneViewport;
|
||||||
},
|
},
|
||||||
getToolbarModule() {
|
getToolbarModule() {
|
||||||
return toolbarModule;
|
return toolbarModule;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import Hammer from 'hammerjs';
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneMath from 'cornerstone-math';
|
import cornerstoneMath from 'cornerstone-math';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
|
|
||||||
|
const { log } = OHIF;
|
||||||
|
|
||||||
export default function(configuration = {}) {
|
export default function(configuration = {}) {
|
||||||
// For debugging
|
// For debugging
|
||||||
@@ -12,6 +15,13 @@ export default function(configuration = {}) {
|
|||||||
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
|
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
|
||||||
cornerstoneTools.init(configuration);
|
cornerstoneTools.init(configuration);
|
||||||
|
|
||||||
|
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(
|
||||||
|
(element, imageId, error) => {
|
||||||
|
log.error(imageId);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Set the tool font and font size
|
// Set the tool font and font size
|
||||||
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
|
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
|
||||||
const fontFamily =
|
const fontFamily =
|
||||||
|
|||||||
@@ -3,6 +3,36 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.1...@ohif/extension-dicom-html@1.2.2) (2020-05-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.0...@ohif/extension-dicom-html@1.2.1) (2020-04-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.2...@ohif/extension-dicom-html@1.2.0) (2020-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.1...@ohif/extension-dicom-html@1.1.2) (2020-04-02)
|
## [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
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-html",
|
"name": "@ohif/extension-dicom-html",
|
||||||
"version": "1.1.2",
|
"version": "1.2.2",
|
||||||
"description": "OHIF extension for rendering structured reports to HTML",
|
"description": "OHIF extension for rendering structured reports to HTML",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"dcmjs": "^0.10.1",
|
"dcmjs": "^0.12.3",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import * as dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import TypedArrayProp from './TypedArrayProp';
|
import TypedArrayProp from './TypedArrayProp';
|
||||||
|
|
||||||
import './DicomHtmlViewport.css';
|
import './DicomHtmlViewport.css';
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.9...@ohif/extension-dicom-microscopy@0.51.0) (2020-06-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.8...@ohif/extension-dicom-microscopy@0.50.9) (2020-04-02)
|
## [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
|
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-microscopy",
|
"name": "@ohif/extension-dicom-microscopy",
|
||||||
"version": "0.50.9",
|
"version": "0.51.0",
|
||||||
"description": "OHIF extension for Dicom Microscopy",
|
"description": "OHIF extension for Dicom Microscopy",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ class DicomMicroscopyViewport extends Component {
|
|||||||
{this.state.error ? (
|
{this.state.error ? (
|
||||||
<h2>{JSON.stringify(this.state.error)}</h2>
|
<h2>{JSON.stringify(this.state.error)}</h2>
|
||||||
) : (
|
) : (
|
||||||
<div style={style} ref={this.container} />
|
<div style={style} ref={this.container} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,25 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [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)
|
## 0.0.2 (2020-04-15)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-dicom-p10-downloader
|
**Note:** Version bump only for package @ohif/extension-dicom-p10-downloader
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-p10-downloader",
|
"name": "@ohif/extension-dicom-p10-downloader",
|
||||||
"version": "0.0.2",
|
"version": "0.1.1",
|
||||||
"description": "OHIF extension for downloading DICOM P10 files",
|
"description": "OHIF extension for downloading DICOM P10 files",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dicomweb-client": "^0.6.0",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"jszip": "^3.2.2"
|
"jszip": "^3.2.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import OHIF from '@ohif/core';
|
|||||||
import {
|
import {
|
||||||
save,
|
save,
|
||||||
getDicomWebClientFromContext,
|
getDicomWebClientFromContext,
|
||||||
|
getStudyInstanceUIDFromStudies,
|
||||||
getSOPInstanceReferenceFromActiveViewport,
|
getSOPInstanceReferenceFromActiveViewport,
|
||||||
getSOPInstanceReferencesFromViewports,
|
getSOPInstanceReferencesFromViewports,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@@ -41,6 +42,14 @@ export function getCommands(context) {
|
|||||||
listOfUIDs
|
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 }) {
|
downloadAndZipSeriesOnViewports({ servers, viewports, progress }) {
|
||||||
const dicomWebClient = getDicomWebClientFromContext(context, servers);
|
const dicomWebClient = getDicomWebClientFromContext(context, servers);
|
||||||
const listOfUIDs = getSOPInstanceReferencesFromViewports(viewports);
|
const listOfUIDs = getSOPInstanceReferencesFromViewports(viewports);
|
||||||
@@ -64,6 +73,11 @@ export function getCommands(context) {
|
|||||||
commandFn: queue.bindSafe(actions.downloadAndZip, error),
|
commandFn: queue.bindSafe(actions.downloadAndZip, error),
|
||||||
storeContexts: ['servers'],
|
storeContexts: ['servers'],
|
||||||
},
|
},
|
||||||
|
downloadAndZipStudy: {
|
||||||
|
commandFn: queue.bindSafe(actions.downloadAndZipStudy, error),
|
||||||
|
storeContexts: ['servers', 'studies'],
|
||||||
|
options: { progress },
|
||||||
|
},
|
||||||
downloadAndZipSeriesOnViewports: {
|
downloadAndZipSeriesOnViewports: {
|
||||||
commandFn: queue.bindSafe(actions.downloadAndZipSeriesOnViewports, error),
|
commandFn: queue.bindSafe(actions.downloadAndZipSeriesOnViewports, error),
|
||||||
storeContexts: ['servers', 'viewports'],
|
storeContexts: ['servers', 'viewports'],
|
||||||
|
|||||||
@@ -100,11 +100,16 @@ function save(promise, listOfUIDs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStudyInstanceUIDFromStudies(studies) {
|
||||||
|
return Object.keys(Object(Object(studies).studyData)).slice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
save,
|
save,
|
||||||
validDicomUid,
|
validDicomUid,
|
||||||
getDicomWebClientFromConfig,
|
getDicomWebClientFromConfig,
|
||||||
getDicomWebClientFromContext,
|
getDicomWebClientFromContext,
|
||||||
|
getStudyInstanceUIDFromStudies,
|
||||||
getSOPInstanceReferenceFromActiveViewport,
|
getSOPInstanceReferenceFromActiveViewport,
|
||||||
getSOPInstanceReferencesFromViewports,
|
getSOPInstanceReferencesFromViewports,
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,69 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.4...@ohif/extension-dicom-rt@0.2.0) (2020-06-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.3...@ohif/extension-dicom-rt@0.1.4) (2020-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Load default display set when no time metadata ([#1684](https://github.com/OHIF/Viewers/issues/1684)) ([f7b8b6a](https://github.com/OHIF/Viewers/commit/f7b8b6a41c4626084ef56b0fdf7363e914b143c4)), closes [#1683](https://github.com/OHIF/Viewers/issues/1683)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.2...@ohif/extension-dicom-rt@0.1.3) (2020-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.1...@ohif/extension-dicom-rt@0.1.2) (2020-05-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.0...@ohif/extension-dicom-rt@0.1.1) (2020-04-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.0.2...@ohif/extension-dicom-rt@0.1.0) (2020-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 0.0.2 (2020-04-09)
|
## 0.0.2 (2020-04-09)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-rt",
|
"name": "@ohif/extension-dicom-rt",
|
||||||
"version": "0.0.2",
|
"version": "0.2.0",
|
||||||
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-tools": "^4.0.9",
|
"cornerstone-tools": "4.15.1",
|
||||||
"dcmjs": "^0.8.3",
|
"dcmjs": "^0.12.3",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@@ -31,7 +31,18 @@ const refreshViewport = () => {
|
|||||||
* @param {number} props.isOpen - isOpen
|
* @param {number} props.isOpen - isOpen
|
||||||
* @returns component
|
* @returns component
|
||||||
*/
|
*/
|
||||||
const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => {
|
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 [selectedContour, setSelectedContour] = useState();
|
||||||
const DEFAULT_SET_INDEX = 0;
|
const DEFAULT_SET_INDEX = 0;
|
||||||
const DEFAULT_STATE = {
|
const DEFAULT_STATE = {
|
||||||
@@ -119,33 +130,35 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedContour(isSameContour ? null : ROINumber);
|
setSelectedContour(isSameContour ? null : ROINumber);
|
||||||
|
|
||||||
const enabledElements = cornerstone.getEnabledElements();
|
if (isCornerstone()) {
|
||||||
const element = enabledElements[activeIndex].element;
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
const element = enabledElements[activeIndex].element;
|
||||||
|
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
|
||||||
if (!toolState) {
|
if (!toolState) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIds = toolState.data[0].imageIds;
|
||||||
|
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
|
||||||
|
state.selectedSet.SeriesInstanceUID,
|
||||||
|
ROINumber,
|
||||||
|
imageIds
|
||||||
|
);
|
||||||
|
|
||||||
|
const frameIndex = imageIds.indexOf(imageId);
|
||||||
|
const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId);
|
||||||
|
const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId);
|
||||||
|
|
||||||
|
onContourItemClick({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex: activeIndex
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageIds = toolState.data[0].imageIds;
|
|
||||||
|
|
||||||
const module = cornerstoneTools.getModule('rtstruct');
|
|
||||||
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
|
|
||||||
state.selectedSet.SeriesInstanceUID,
|
|
||||||
ROINumber,
|
|
||||||
imageIds
|
|
||||||
);
|
|
||||||
|
|
||||||
const frameIndex = imageIds.indexOf(imageId);
|
|
||||||
const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId);
|
|
||||||
const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId);
|
|
||||||
|
|
||||||
onContourItemClick({
|
|
||||||
StudyInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
frameIndex,
|
|
||||||
activeViewportIndex: activeIndex
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
label={`${ROIName} ${interpretedType}`}
|
label={`${ROIName} ${interpretedType}`}
|
||||||
index={ROINumber}
|
index={ROINumber}
|
||||||
@@ -190,7 +203,7 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!state.referencedDisplaysets.length && <LoadingIndicator expand height="70px" width="70px" />}
|
{!state.referencedDisplaysets.length && <LoadingIndicator expand height="70px" width="70px" />}
|
||||||
{state.referencedDisplaysets.map(displaySet => {
|
{state.sets && state.referencedDisplaysets.map(displaySet => {
|
||||||
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
|
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
|
||||||
|
|
||||||
const module = cornerstoneTools.getModule('rtstruct');
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
@@ -205,9 +218,14 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
|||||||
visible={isLoaded && loadedSet.visible}
|
visible={isLoaded && loadedSet.visible}
|
||||||
hideVisibleButton={!isLoaded}
|
hideVisibleButton={!isLoaded}
|
||||||
expanded={isLoaded && loadedSet.SeriesInstanceUID === state.selectedSet.SeriesInstanceUID}
|
expanded={isLoaded && loadedSet.SeriesInstanceUID === state.selectedSet.SeriesInstanceUID}
|
||||||
onVisibilityChange={() => {
|
onVisibilityChange={newVisibility => {
|
||||||
const module = cornerstoneTools.getModule('rtstruct');
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
module.setters.toggleStructureSet(SeriesInstanceUID);
|
loadedSet.ROIContours.forEach(({ ROINumber }) => {
|
||||||
|
module.setters.toggleROIContour(loadedSet.SeriesInstanceUID, ROINumber);
|
||||||
|
});
|
||||||
|
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||||
|
setState(state => ({ ...state, sets }));
|
||||||
|
refreshViewport();
|
||||||
}}
|
}}
|
||||||
onExpandChange={async () => {
|
onExpandChange={async () => {
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
|
|||||||
@@ -61,14 +61,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dcmrt-structure-set-item .item-label {
|
.dcmrt-structure-set-item .item-label {
|
||||||
overflow: hidden;
|
|
||||||
max-width: calc(100% - 15px); /* calc(100% - 50px); 20px = eye icon */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow-wrap: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
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 {
|
.dcmrt-structure-set-item .item-label .eye-icon {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { TableListItem, Icon } from '@ohif/ui';
|
import { TableListItem, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
@@ -28,6 +28,11 @@ const StructureSetItem = ({
|
|||||||
selected = false,
|
selected = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isVisible, setIsVisible] = useState(visible);
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(visible);
|
||||||
|
}, [visible])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`dcmrt-structure-set-item ${selected && 'selected'}`}>
|
<div className={`dcmrt-structure-set-item ${selected && 'selected'}`}>
|
||||||
<TableListItem
|
<TableListItem
|
||||||
@@ -41,7 +46,7 @@ const StructureSetItem = ({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="item-label" style={{ marginBottom: 4 }}>
|
<div className="item-label" style={{ marginBottom: 4 }}>
|
||||||
{label}
|
<span>{label}</span>
|
||||||
<Icon
|
<Icon
|
||||||
className={`eye-icon ${isVisible && '--visible'}`}
|
className={`eye-icon ${isVisible && '--visible'}`}
|
||||||
name={isVisible ? 'eye' : 'eye-closed'}
|
name={isVisible ? 'eye' : 'eye-closed'}
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ export default {
|
|||||||
preRegistration({ servicesManager, configuration = {} }) {
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
init({ servicesManager, configuration });
|
init({ servicesManager, configuration });
|
||||||
},
|
},
|
||||||
getPanelModule({ commandsManager }) {
|
getPanelModule({ commandsManager, api }) {
|
||||||
const ExtendedRTPanel = props => {
|
const ExtendedRTPanel = props => {
|
||||||
|
const { activeContexts } = api.hooks.useAppContext();
|
||||||
|
|
||||||
const contourItemClickHandler = contourData => {
|
const contourItemClickHandler = contourData => {
|
||||||
commandsManager.runCommand('jumpToImage', contourData);
|
commandsManager.runCommand('jumpToImage', contourData);
|
||||||
};
|
};
|
||||||
@@ -30,6 +32,8 @@ export default {
|
|||||||
<RTPanel
|
<RTPanel
|
||||||
{...props}
|
{...props}
|
||||||
onContourItemClick={contourItemClickHandler}
|
onContourItemClick={contourItemClickHandler}
|
||||||
|
activeContexts={activeContexts}
|
||||||
|
contexts={api.contexts}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -51,8 +55,10 @@ export default {
|
|||||||
if (study && study.series) {
|
if (study && study.series) {
|
||||||
for (let j = 0; j < study.series.length; j++) {
|
for (let j = 0; j < study.series.length; j++) {
|
||||||
const series = study.series[j];
|
const series = study.series[j];
|
||||||
console.log(studies);
|
if (
|
||||||
if (['RTSTRUCT', 'RTPLAN', 'RTDOSE'].includes(series.Modality)) {
|
/* Could be expanded to contain RTPLAN and RTDOSE information in the future */
|
||||||
|
['RTSTRUCT'].includes(series.Modality)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import * as dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
||||||
|
|||||||
@@ -3,6 +3,85 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [0.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.6...@ohif/extension-dicom-segmentation@0.3.0) (2020-06-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.5...@ohif/extension-dicom-segmentation@0.2.6) (2020-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.4...@ohif/extension-dicom-segmentation@0.2.5) (2020-05-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.3...@ohif/extension-dicom-segmentation@0.2.4) (2020-05-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.2...@ohif/extension-dicom-segmentation@0.2.3) (2020-05-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.1...@ohif/extension-dicom-segmentation@0.2.2) (2020-04-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.2.0...@ohif/extension-dicom-segmentation@0.2.1) (2020-04-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Dropdown not updating when selecting segment ([#1675](https://github.com/OHIF/Viewers/issues/1675)) ([20e366f](https://github.com/OHIF/Viewers/commit/20e366ffbd76781f48dc99c9f59e24b255b5e5df)), closes [#1669](https://github.com/OHIF/Viewers/issues/1669)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.2...@ohif/extension-dicom-segmentation@0.2.0) (2020-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.1...@ohif/extension-dicom-segmentation@0.1.2) (2020-04-09)
|
## [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
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-segmentation",
|
"name": "@ohif/extension-dicom-segmentation",
|
||||||
"version": "0.1.2",
|
"version": "0.3.0",
|
||||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -30,14 +30,15 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.15.1",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.12.2",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"gl-matrix": "^3.3.0",
|
||||||
"react-select": "^3.0.8"
|
"react-select": "^3.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { MODULE_TYPES, utils } from '@ohif/core';
|
|
||||||
import loadSegmentation from './loadSegmentation';
|
|
||||||
|
|
||||||
// TODO: Should probably use dcmjs for this
|
|
||||||
const SOP_CLASS_UIDS = {
|
|
||||||
DICOM_SEG: '1.2.840.10008.5.1.4.1.1.66.4',
|
|
||||||
};
|
|
||||||
|
|
||||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
|
||||||
|
|
||||||
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
|
||||||
// same SOP Class.
|
|
||||||
const OHIFDicomSegSopClassHandler = {
|
|
||||||
id: 'OHIFDicomSegSopClassHandler',
|
|
||||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
|
||||||
sopClassUIDs,
|
|
||||||
getDisplaySetFromSeries: function (
|
|
||||||
series,
|
|
||||||
study,
|
|
||||||
dicomWebClient,
|
|
||||||
authorizationHeaders
|
|
||||||
) {
|
|
||||||
const instance = series.getFirstInstance();
|
|
||||||
const metadata = instance.getData().metadata;
|
|
||||||
|
|
||||||
const {
|
|
||||||
SeriesDate,
|
|
||||||
SeriesTime,
|
|
||||||
SeriesDescription,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = metadata;
|
|
||||||
|
|
||||||
const segDisplaySet = {
|
|
||||||
Modality: 'SEG',
|
|
||||||
displaySetInstanceUID: utils.guid(),
|
|
||||||
wadoRoot: study.getData().wadoRoot,
|
|
||||||
wadoUri: instance.getData().wadouri,
|
|
||||||
SOPInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
authorizationHeaders,
|
|
||||||
metadata,
|
|
||||||
isDerived: true,
|
|
||||||
referencedDisplaySetUID: null, // Assigned when loaded.
|
|
||||||
labelmapIndex: null, // Assigned when loaded.
|
|
||||||
isLoaded: false,
|
|
||||||
SeriesDate,
|
|
||||||
SeriesTime,
|
|
||||||
SeriesDescription,
|
|
||||||
};
|
|
||||||
|
|
||||||
segDisplaySet.load = function (referencedDisplaySet, studies) {
|
|
||||||
return loadSegmentation(
|
|
||||||
segDisplaySet,
|
|
||||||
referencedDisplaySet,
|
|
||||||
studies
|
|
||||||
).catch(error => {
|
|
||||||
segDisplaySet.isLoaded = false;
|
|
||||||
throw new Error(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return segDisplaySet;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OHIFDicomSegSopClassHandler;
|
|
||||||
@@ -12,14 +12,6 @@
|
|||||||
height: 20px;
|
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 {
|
.dcmseg-segment-item .segment-info {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
@@ -62,3 +54,30 @@
|
|||||||
.dcmseg-segment-item .segment-actions .btnAction i {
|
.dcmseg-segment-item .segment-actions .btnAction i {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label span {
|
||||||
|
overflow-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: calc(100% - 40px); /* calc(100% - 50px); 20px = eye icon */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label .eye-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label .eye-icon:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmseg-segment-item .segment-label .eye-icon.--visible {
|
||||||
|
color: var(--default-color);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { TableListItem, Icon } from '@ohif/ui';
|
import { TableListItem, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
@@ -17,48 +17,68 @@ ColoredCircle.propTypes = {
|
|||||||
color: PropTypes.array.isRequired,
|
color: PropTypes.array.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SegmentItem = ({ index, label, onClick, itemClass, color }) => (
|
const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true, onVisibilityChange }) => {
|
||||||
<div className="dcmseg-segment-item">
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
<TableListItem
|
|
||||||
key={index}
|
useEffect(() => {
|
||||||
itemKey={index}
|
setIsVisible(visible);
|
||||||
itemIndex={index}
|
}, [visible]);
|
||||||
itemClass={itemClass}
|
|
||||||
itemMeta={<ColoredCircle color={color} />}
|
return (
|
||||||
itemMetaClass="segment-color-section"
|
<div className="dcmseg-segment-item">
|
||||||
onItemClick={onClick}
|
<TableListItem
|
||||||
>
|
key={index}
|
||||||
<div>
|
itemKey={index}
|
||||||
<div className="segment-label" style={{ marginBottom: 4 }}>
|
itemIndex={index}
|
||||||
{label}
|
itemClass={itemClass}
|
||||||
</div>
|
itemMeta={<ColoredCircle color={color} />}
|
||||||
{false && <div className="segment-info">{'...'}</div>}
|
itemMetaClass="segment-color-section"
|
||||||
{false && (
|
onItemClick={onClick}
|
||||||
<div className="segment-actions">
|
>
|
||||||
<button
|
<div>
|
||||||
className="btnAction"
|
<div className="segment-label" style={{ marginBottom: 4 }}>
|
||||||
onClick={() => console.log('Relabelling...')}
|
<span>{label}</span>
|
||||||
>
|
<Icon
|
||||||
<span style={{ marginRight: '4px' }}>
|
className={`eye-icon ${isVisible && '--visible'}`}
|
||||||
<Icon name="edit" width="14px" height="14px" />
|
name={isVisible ? 'eye' : 'eye-closed'}
|
||||||
</span>
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const newVisibility = !isVisible;
|
||||||
|
setIsVisible(newVisibility);
|
||||||
|
onVisibilityChange(newVisibility);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{false && <div className="segment-info">{'...'}</div>}
|
||||||
|
{false && (
|
||||||
|
<div className="segment-actions">
|
||||||
|
<button
|
||||||
|
className="btnAction"
|
||||||
|
onClick={() => console.log('Relabelling...')}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: '4px' }}>
|
||||||
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
|
</span>
|
||||||
Relabel
|
Relabel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btnAction"
|
className="btnAction"
|
||||||
onClick={() => console.log('Editing description...')}
|
onClick={() => console.log('Editing description...')}
|
||||||
>
|
>
|
||||||
<span style={{ marginRight: '4px' }}>
|
<span style={{ marginRight: '4px' }}>
|
||||||
<Icon name="edit" width="14px" height="14px" />
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
</span>
|
</span>
|
||||||
Description
|
Description
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableListItem>
|
</TableListItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
SegmentItem.propTypes = {
|
SegmentItem.propTypes = {
|
||||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
|||||||
+344
-124
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
@@ -28,28 +28,56 @@ const refreshViewport = () => {
|
|||||||
/**
|
/**
|
||||||
* SegmentationPanel component
|
* SegmentationPanel component
|
||||||
*
|
*
|
||||||
* @param {Object} props
|
* @param {Array} props.studies - Studies data
|
||||||
* @param {Array} props.studies
|
* @param {Array} props.viewports - Viewports data (viewportSpecificData)
|
||||||
* @param {Array} props.viewports - viewportSpecificData
|
* @param {number} props.activeIndex - Active viewport index
|
||||||
* @param {number} props.activeIndex - activeViewportIndex
|
* @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
|
* @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
|
* TODO: wrap get/set interactions with the cornerstoneTools
|
||||||
* store with context to make these kind of things less blurry.
|
* store with context to make these kind of things less blurry.
|
||||||
*/
|
*/
|
||||||
const { configuration } = cornerstoneTools.getModule('segmentation');
|
const { configuration } = cornerstoneTools.getModule('segmentation');
|
||||||
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
|
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({
|
const [state, setState] = useState({
|
||||||
brushRadius: DEFAULT_BRUSH_RADIUS,
|
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,
|
selectedSegment: null,
|
||||||
selectedSegmentation: null,
|
selectedSegmentation: null,
|
||||||
showSegSettings: false,
|
showSegmentationSettings: false,
|
||||||
brushStackState: null,
|
brushStackState: null,
|
||||||
labelmapList: [],
|
labelmapList: [],
|
||||||
segmentList: []
|
segmentList: [],
|
||||||
|
cachedSegmentsProperties: [],
|
||||||
|
isLoading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,10 +85,34 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
log.warn('Segmentation Panel: labelmap modified', event);
|
log.warn('Segmentation Panel: labelmap modified', event);
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const module = cornerstoneTools.getModule('segmentation');
|
||||||
const activeViewport = viewports[activeIndex];
|
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]);
|
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;
|
* These are specific to each element;
|
||||||
* Need to iterate cornerstone-tools tracked enabled elements?
|
* Need to iterate cornerstone-tools tracked enabled elements?
|
||||||
@@ -74,6 +126,7 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
document.removeEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations);
|
||||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||||
enabledElement.removeEventListener(
|
enabledElement.removeEventListener(
|
||||||
'cornerstonetoolslabelmapmodified',
|
'cornerstonetoolslabelmapmodified',
|
||||||
@@ -81,25 +134,37 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
});
|
}, [activeIndex, viewports]);
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshSegmentations = useCallback(() => {
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const module = cornerstoneTools.getModule('segmentation');
|
||||||
const activeViewport = viewports[activeIndex];
|
const activeViewport = viewports[activeIndex];
|
||||||
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
|
const studyMetadata = studyMetadataManager.get(
|
||||||
const firstImageId = studyMetadata.getFirstImageId(activeViewport.displaySetInstanceUID);
|
activeViewport.StudyInstanceUID
|
||||||
|
);
|
||||||
|
const firstImageId = studyMetadata.getFirstImageId(
|
||||||
|
activeViewport.displaySetInstanceUID
|
||||||
|
);
|
||||||
const brushStackState = module.state.series[firstImageId];
|
const brushStackState = module.state.series[firstImageId];
|
||||||
|
|
||||||
if (brushStackState) {
|
if (brushStackState) {
|
||||||
const labelmap3D = brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
const labelmap3D =
|
||||||
const labelmapList = getLabelmapList(brushStackState, firstImageId, activeViewport);
|
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||||
const segmentList = getSegmentList(labelmap3D, firstImageId);
|
const labelmapList = getLabelmapList(
|
||||||
|
brushStackState,
|
||||||
|
firstImageId,
|
||||||
|
activeViewport
|
||||||
|
);
|
||||||
|
const segmentList = getSegmentList(
|
||||||
|
labelmap3D,
|
||||||
|
firstImageId,
|
||||||
|
brushStackState
|
||||||
|
);
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
brushStackState,
|
brushStackState,
|
||||||
selectedSegmentation: brushStackState.activeLabelmapIndex,
|
selectedSegmentation: brushStackState.activeLabelmapIndex,
|
||||||
labelmapList,
|
labelmapList,
|
||||||
segmentList
|
segmentList,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
@@ -108,127 +173,267 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
segmentList: [],
|
segmentList: [],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [studies, viewports, activeIndex]);
|
}, [
|
||||||
|
viewports,
|
||||||
|
activeIndex,
|
||||||
|
state.isLoading
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshSegmentations();
|
||||||
|
}, [viewports, activeIndex, state.selectedSegmentation, activeContexts, state.isLoading]);
|
||||||
|
|
||||||
/* Handle open/closed panel behaviour */
|
/* Handle open/closed panel behaviour */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateState('showSegSettings', state.showSegSettings && !isOpen);
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
showSegmentationSettings: state.showSegmentationSettings && !isOpen,
|
||||||
|
}));
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const getLabelmapList = (brushStackState, firstImageId, activeViewport) => {
|
const getLabelmapList = useCallback(
|
||||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
(brushStackState, firstImageId, activeViewport) => {
|
||||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||||
activeViewport.StudyInstanceUID,
|
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||||
activeViewport.SeriesInstanceUID
|
activeViewport.StudyInstanceUID,
|
||||||
);
|
activeViewport.SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||||
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
||||||
|
|
||||||
/* Map to display representation */
|
/* Map to display representation */
|
||||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||||
const isActiveLabelmap =
|
const isActiveLabelmap =
|
||||||
labelmapIndex === brushStackState.activeLabelmapIndex;
|
labelmapIndex === brushStackState.activeLabelmapIndex;
|
||||||
const displayDate = date.format('ddd, MMM Do YYYY');
|
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||||
const displayTime = date.format('h:mm:ss a');
|
const displayTime = date.format('h:mm:ss a');
|
||||||
const displayDescription = displaySet.SeriesDescription;
|
const displayDescription = displaySet.SeriesDescription;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: labelmapIndex,
|
value: labelmapIndex,
|
||||||
title: displayDescription,
|
title: displayDescription,
|
||||||
description: displayDate,
|
description: displayDate,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const activatedLabelmapIndex = await _setActiveLabelmap(
|
const activatedLabelmapIndex = await _setActiveLabelmap(
|
||||||
activeViewport,
|
activeViewport,
|
||||||
studies,
|
studies,
|
||||||
displaySet,
|
displaySet,
|
||||||
firstImageId,
|
firstImageId,
|
||||||
brushStackState.activeLabelmapIndex
|
brushStackState.activeLabelmapIndex,
|
||||||
);
|
() => onSelectedSegmentationChange(),
|
||||||
updateState('selectedSegmentation', activatedLabelmapIndex);
|
onDisplaySetLoadFailure
|
||||||
},
|
);
|
||||||
};
|
updateState('selectedSegmentation', activatedLabelmapIndex);
|
||||||
});
|
},
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[studies]
|
||||||
|
);
|
||||||
|
|
||||||
const getSegmentList = (labelmap3D, firstImageId) => {
|
const getSegmentList = useCallback(
|
||||||
/*
|
(labelmap3D, firstImageId, brushStackState) => {
|
||||||
* Newly created segments have no `meta`
|
/*
|
||||||
* So we instead build a list of all segment indexes in use
|
* Newly created segments have no `meta`
|
||||||
* Then find any associated metadata
|
* So we instead build a list of all segment indexes in use
|
||||||
*/
|
* Then find any associated metadata
|
||||||
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
|
*/
|
||||||
.reduce((acc, labelmap2D) => {
|
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
|
||||||
if (labelmap2D) {
|
.reduce((acc, labelmap2D) => {
|
||||||
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
if (labelmap2D) {
|
||||||
|
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
||||||
|
|
||||||
for (let i = 0; i < segmentIndexes.length; i++) {
|
for (let i = 0; i < segmentIndexes.length; i++) {
|
||||||
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
||||||
acc.push(segmentIndexes[i]);
|
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;
|
const sameSegment = state.selectedSegment === segmentNumber;
|
||||||
}, [])
|
const setCurrentSelectedSegment = () => {
|
||||||
.sort((a, b) => a - b);
|
_setActiveSegment(
|
||||||
|
firstImageId,
|
||||||
|
segmentNumber,
|
||||||
|
labelmap3D.activeSegmentIndex
|
||||||
|
);
|
||||||
|
updateState('selectedSegment', sameSegment ? null : segmentNumber);
|
||||||
|
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const validIndexList = [];
|
||||||
const colorLutTable =
|
labelmap3D.labelmaps2D.forEach((labelMap2D, index) => {
|
||||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
if (labelMap2D.segmentsOnLabelmap.includes(segmentNumber)) {
|
||||||
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
|
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 = [];
|
if (isCornerstone()) {
|
||||||
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
const segmentIndex = uniqueSegmentIndexes[i];
|
const element = enabledElements[activeIndex].element;
|
||||||
|
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
|
||||||
const color = colorLutTable[segmentIndex];
|
if (!toolState) {
|
||||||
let segmentLabel = '(unlabeled)';
|
return;
|
||||||
let segmentNumber = segmentIndex;
|
}
|
||||||
|
|
||||||
/* Meta */
|
const imageIds = toolState.data[0].imageIds;
|
||||||
if (hasLabelmapMeta) {
|
const imageId = imageIds[closest];
|
||||||
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
|
const frameIndex = imageIds.indexOf(imageId);
|
||||||
|
|
||||||
if (segmentMeta) {
|
const SOPInstanceUID = cornerstone.metaData.get(
|
||||||
segmentNumber = segmentMeta.SegmentNumber;
|
'SOPInstanceUID',
|
||||||
segmentLabel = segmentMeta.SegmentLabel;
|
imageId
|
||||||
|
);
|
||||||
|
const StudyInstanceUID = cornerstone.metaData.get(
|
||||||
|
'StudyInstanceUID',
|
||||||
|
imageId
|
||||||
|
);
|
||||||
|
|
||||||
|
onSegmentItemClick({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex: activeIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVTK()) {
|
||||||
|
const activeViewport = viewports[activeIndex];
|
||||||
|
const studyMetadata = studyMetadataManager.get(
|
||||||
|
activeViewport.StudyInstanceUID
|
||||||
|
);
|
||||||
|
const allDisplaySets = studyMetadata.getDisplaySets();
|
||||||
|
const currentDisplaySet = allDisplaySets.find(
|
||||||
|
displaySet =>
|
||||||
|
displaySet.displaySetInstanceUID ===
|
||||||
|
activeViewport.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const frame = labelmap3D.labelmaps2D[closest];
|
||||||
|
|
||||||
|
onSegmentItemClick({
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID: currentDisplaySet.StudyInstanceUID,
|
||||||
|
displaySetInstanceUID: currentDisplaySet.displaySetInstanceUID,
|
||||||
|
SOPClassUID: viewports[activeIndex].sopClassUIDs[0],
|
||||||
|
SOPInstanceUID: currentDisplaySet.SOPInstanceUID,
|
||||||
|
segmentNumber,
|
||||||
|
frameIndex: closest,
|
||||||
|
frame,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSegmentVisible = () => {
|
||||||
|
return !labelmap3D.segmentsHidden[segmentIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSegmentVisibility = () => {
|
||||||
|
const segmentsHidden = labelmap3D.segmentsHidden;
|
||||||
|
segmentsHidden[segmentIndex] = !segmentsHidden[segmentIndex];
|
||||||
|
return !segmentsHidden[segmentIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedSegmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||||
|
let visible = isSegmentVisible();
|
||||||
|
if (cachedSegmentProperties && cachedSegmentProperties.visible !== visible) {
|
||||||
|
toggleSegmentVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
segmentList.push(
|
||||||
|
<SegmentItem
|
||||||
|
key={segmentNumber}
|
||||||
|
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
||||||
|
onClick={() => setCurrentSelectedSegment()}
|
||||||
|
label={segmentLabel}
|
||||||
|
index={segmentNumber}
|
||||||
|
color={color}
|
||||||
|
visible={visible}
|
||||||
|
onVisibilityChange={newVisibility => {
|
||||||
|
if (isCornerstone()) {
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
const element = enabledElements[activeIndex].element;
|
||||||
|
module.setters.toggleSegmentVisibility(
|
||||||
|
element,
|
||||||
|
segmentNumber,
|
||||||
|
brushStackState.activeLabelmapIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVTK()) {
|
||||||
|
onSegmentVisibilityChange(segmentNumber, newVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCachedSegmentsProperties(segmentNumber, { visible: newVisibility });
|
||||||
|
refreshViewport();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sameSegment = state.selectedSegment === segmentNumber;
|
return segmentList;
|
||||||
const setCurrentSelectedSegment = () => {
|
|
||||||
_setActiveSegment(
|
|
||||||
firstImageId,
|
|
||||||
segmentNumber,
|
|
||||||
labelmap3D.activeSegmentIndex
|
|
||||||
);
|
|
||||||
updateState('selectedSegment', sameSegment ? null : segmentNumber);
|
|
||||||
};
|
|
||||||
|
|
||||||
segmentList.push(
|
/*
|
||||||
<SegmentItem
|
* Let's iterate over segmentIndexes ^ above
|
||||||
key={segmentNumber}
|
* If meta has a match, use it to show info
|
||||||
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
* If now, add "no-meta" class
|
||||||
onClick={setCurrentSelectedSegment}
|
* Show default name
|
||||||
label={segmentLabel}
|
*/
|
||||||
index={segmentNumber}
|
},
|
||||||
color={color}
|
[activeIndex, onSegmentItemClick, state.selectedSegment, state.isLoading]
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return segmentList;
|
const updateCachedSegmentsProperties = (segmentNumber, properties) => {
|
||||||
|
const segmentsProperties = state.cachedSegmentsProperties;
|
||||||
|
const segmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||||
|
|
||||||
/*
|
segmentsProperties[segmentNumber] =
|
||||||
* Let's iterate over segmentIndexes ^ above
|
segmentProperties ?
|
||||||
* If meta has a match, use it to show info
|
{ ...segmentProperties, ...properties } :
|
||||||
* If now, add "no-meta" class
|
properties;
|
||||||
* Show default name
|
|
||||||
*/
|
updateState('cachedSegmentsProperties', segmentsProperties);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateState('cachedSegmentsProperties', []);
|
||||||
|
}, [activeContexts]);
|
||||||
|
|
||||||
const updateState = (field, value) => {
|
const updateState = (field, value) => {
|
||||||
setState(state => ({ ...state, [field]: value }));
|
setState(state => ({ ...state, [field]: value }));
|
||||||
};
|
};
|
||||||
@@ -270,15 +475,13 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const module = cornerstoneTools.getModule('segmentation');
|
||||||
const colorLutTable =
|
const colorLutTable = module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
|
||||||
const color = colorLutTable[labelmap3D.activeSegmentIndex];
|
const color = colorLutTable[labelmap3D.activeSegmentIndex];
|
||||||
|
|
||||||
return `rgba(${color.join(',')})`;
|
return `rgba(${color.join(',')})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConfiguration = newConfiguration => {
|
const updateConfiguration = newConfiguration => {
|
||||||
/* Supported configuration */
|
|
||||||
configuration.renderFill = newConfiguration.renderFill;
|
configuration.renderFill = newConfiguration.renderFill;
|
||||||
configuration.renderOutline = newConfiguration.renderOutline;
|
configuration.renderOutline = newConfiguration.renderOutline;
|
||||||
configuration.shouldRenderInactiveLabelmaps =
|
configuration.shouldRenderInactiveLabelmaps =
|
||||||
@@ -288,14 +491,17 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
configuration.outlineWidth = newConfiguration.outlineWidth;
|
configuration.outlineWidth = newConfiguration.outlineWidth;
|
||||||
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
|
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
|
||||||
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
|
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
|
||||||
|
onConfigurationChange(newConfiguration);
|
||||||
refreshViewport();
|
refreshViewport();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state.showSegSettings) {
|
const disabledConfigurationFields = ['outlineAlpha', 'shouldRenderInactiveLabelmaps'];
|
||||||
|
if (state.showSegmentationSettings) {
|
||||||
return (
|
return (
|
||||||
<SegmentationSettings
|
<SegmentationSettings
|
||||||
|
disabledFields={isVTK() ? disabledConfigurationFields : []}
|
||||||
configuration={configuration}
|
configuration={configuration}
|
||||||
onBack={() => updateState('showSegSettings', false)}
|
onBack={() => updateState('showSegmentationSettings', false)}
|
||||||
onChange={updateConfiguration}
|
onChange={updateConfiguration}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -307,7 +513,7 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
name="cog"
|
name="cog"
|
||||||
width="25px"
|
width="25px"
|
||||||
height="25px"
|
height="25px"
|
||||||
onClick={() => updateState('showSegSettings', true)}
|
onClick={() => updateState('showSegmentationSettings', true)}
|
||||||
/>
|
/>
|
||||||
{false && (
|
{false && (
|
||||||
<form className="selector-form">
|
<form className="selector-form">
|
||||||
@@ -328,9 +534,9 @@ const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
|||||||
<h3>Segmentations</h3>
|
<h3>Segmentations</h3>
|
||||||
<div className="segmentations">
|
<div className="segmentations">
|
||||||
<SegmentationSelect
|
<SegmentationSelect
|
||||||
value={
|
value={state.labelmapList.find(
|
||||||
state.labelmapList.find(i => i.value === state.selectedSegmentation) || null
|
i => i.value === state.selectedSegmentation
|
||||||
}
|
)}
|
||||||
formatOptionLabel={SegmentationItem}
|
formatOptionLabel={SegmentationItem}
|
||||||
options={state.labelmapList}
|
options={state.labelmapList}
|
||||||
/>
|
/>
|
||||||
@@ -415,7 +621,9 @@ const _setActiveLabelmap = async (
|
|||||||
studies,
|
studies,
|
||||||
displaySet,
|
displaySet,
|
||||||
firstImageId,
|
firstImageId,
|
||||||
activeLabelmapIndex
|
activeLabelmapIndex,
|
||||||
|
callback = () => { },
|
||||||
|
onDisplaySetLoadFailure
|
||||||
) => {
|
) => {
|
||||||
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
||||||
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
||||||
@@ -425,7 +633,17 @@ const _setActiveLabelmap = async (
|
|||||||
if (!displaySet.isLoaded) {
|
if (!displaySet.isLoaded) {
|
||||||
// What props does this expect `viewportSpecificData` to have?
|
// What props does this expect `viewportSpecificData` to have?
|
||||||
// TODO: Should this return the `labelmapIndex`?
|
// TODO: Should this return the `labelmapIndex`?
|
||||||
await displaySet.load(viewportSpecificData, studies);
|
|
||||||
|
const loadPromise = displaySet.load(viewportSpecificData, studies);
|
||||||
|
|
||||||
|
loadPromise.catch(error => {
|
||||||
|
onDisplaySetLoadFailure(error);
|
||||||
|
|
||||||
|
// Return old index.
|
||||||
|
return activeLabelmapIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
await loadPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { state } = cornerstoneTools.getModule('segmentation');
|
const { state } = cornerstoneTools.getModule('segmentation');
|
||||||
@@ -434,6 +652,8 @@ const _setActiveLabelmap = async (
|
|||||||
|
|
||||||
refreshViewport();
|
refreshViewport();
|
||||||
|
|
||||||
|
callback();
|
||||||
|
|
||||||
return displaySet.labelmapIndex;
|
return displaySet.labelmapIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const defaultColor = computedstyle.getPropertyValue('--default-color');
|
|||||||
const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
|
const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
|
||||||
|
|
||||||
const segmentationSelectStyles = {
|
const segmentationSelectStyles = {
|
||||||
|
singleValue: (base, state) => ({
|
||||||
|
...base,
|
||||||
|
width: '100%'
|
||||||
|
}),
|
||||||
control: (base, state) => ({
|
control: (base, state) => ({
|
||||||
...base,
|
...base,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|||||||
+26
-22
@@ -4,7 +4,7 @@ import { Range } from '@ohif/ui';
|
|||||||
|
|
||||||
import './SegmentationSettings.css';
|
import './SegmentationSettings.css';
|
||||||
|
|
||||||
const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
const SegmentationSettings = ({ configuration, onBack, onChange, disabledFields = [] }) => {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
renderFill: configuration.renderFill,
|
renderFill: configuration.renderFill,
|
||||||
renderOutline: configuration.renderOutline,
|
renderOutline: configuration.renderOutline,
|
||||||
@@ -70,28 +70,32 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
|||||||
/>
|
/>
|
||||||
{state.renderOutline && (
|
{state.renderOutline && (
|
||||||
<>
|
<>
|
||||||
<CustomRange
|
{!disabledFields.includes('outlineAlpha') && (
|
||||||
value={state.outlineAlpha * 100}
|
<CustomRange
|
||||||
label="Opacity"
|
value={state.outlineAlpha * 100}
|
||||||
showPercentage
|
label="Opacity"
|
||||||
step={1}
|
showPercentage
|
||||||
min={0}
|
step={1}
|
||||||
max={100}
|
min={0}
|
||||||
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
|
max={100}
|
||||||
/>
|
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
|
||||||
<CustomRange
|
/>
|
||||||
value={state.outlineWidth}
|
)}
|
||||||
label="Width"
|
{!disabledFields.includes('outlineWidth') && (
|
||||||
showValue
|
<CustomRange
|
||||||
step={1}
|
value={state.outlineWidth}
|
||||||
min={0}
|
label="Width"
|
||||||
max={5}
|
showValue
|
||||||
onChange={event => save('outlineWidth', parseInt(event.target.value))}
|
step={1}
|
||||||
/>
|
min={0}
|
||||||
|
max={5}
|
||||||
|
onChange={event => save('outlineWidth', parseInt(event.target.value))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(state.renderFill || state.renderOutline) && (
|
{(state.renderFill || state.renderOutline) && !disabledFields.includes('shouldRenderInactiveLabelmaps') && (
|
||||||
<div
|
<div
|
||||||
className="settings-group"
|
className="settings-group"
|
||||||
style={{ marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0 }}
|
style={{ marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0 }}
|
||||||
@@ -103,7 +107,7 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
|||||||
/>
|
/>
|
||||||
{state.shouldRenderInactiveLabelmaps && (
|
{state.shouldRenderInactiveLabelmaps && (
|
||||||
<>
|
<>
|
||||||
{state.renderFill && (
|
{state.renderFill && !disabledFields.includes('fillAlphaInactive') && (
|
||||||
<CustomRange
|
<CustomRange
|
||||||
label="Fill Opacity"
|
label="Fill Opacity"
|
||||||
showPercentage
|
showPercentage
|
||||||
@@ -114,7 +118,7 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
|||||||
onChange={event => save('fillAlphaInactive', toFloat(event.target.value))}
|
onChange={event => save('fillAlphaInactive', toFloat(event.target.value))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{state.renderOutline && (
|
{state.renderOutline && !disabledFields.includes('outlineAlphaInactive') && (
|
||||||
<CustomRange
|
<CustomRange
|
||||||
label="Outline Opacity"
|
label="Outline Opacity"
|
||||||
showPercentage
|
showPercentage
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { MODULE_TYPES, utils } from '@ohif/core';
|
||||||
|
import loadSegmentation from './loadSegmentation';
|
||||||
|
|
||||||
|
// TODO: Should probably use dcmjs for this
|
||||||
|
const SOP_CLASS_UIDS = {
|
||||||
|
DICOM_SEG: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||||
|
|
||||||
|
export default function getSopClassHandlerModule({ servicesManager }) {
|
||||||
|
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
||||||
|
// same SOP Class.
|
||||||
|
return {
|
||||||
|
id: 'OHIFDicomSegSopClassHandler',
|
||||||
|
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||||
|
sopClassUIDs,
|
||||||
|
getDisplaySetFromSeries: function(
|
||||||
|
series,
|
||||||
|
study,
|
||||||
|
dicomWebClient,
|
||||||
|
authorizationHeaders
|
||||||
|
) {
|
||||||
|
const instance = series.getFirstInstance();
|
||||||
|
const metadata = instance.getData().metadata;
|
||||||
|
|
||||||
|
const {
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesDescription,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = metadata;
|
||||||
|
|
||||||
|
const segDisplaySet = {
|
||||||
|
Modality: 'SEG',
|
||||||
|
displaySetInstanceUID: utils.guid(),
|
||||||
|
wadoRoot: study.getData().wadoRoot,
|
||||||
|
wadoUri: instance.getData().wadouri,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
authorizationHeaders,
|
||||||
|
metadata,
|
||||||
|
isDerived: true,
|
||||||
|
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||||
|
labelmapIndex: null, // Assigned when loaded.
|
||||||
|
isLoaded: false,
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesDescription,
|
||||||
|
};
|
||||||
|
|
||||||
|
segDisplaySet.load = function(referencedDisplaySet, studies) {
|
||||||
|
return loadSegmentation(segDisplaySet, referencedDisplaySet, studies);
|
||||||
|
};
|
||||||
|
|
||||||
|
return segDisplaySet;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import init from './init.js';
|
import init from './init.js';
|
||||||
import toolbarModule from './toolbarModule.js';
|
import toolbarModule from './toolbarModule.js';
|
||||||
import panelModule from './panelModule.js';
|
import getSopClassHandlerModule from './getOHIFDicomSegSopClassHandler.js';
|
||||||
import sopClassHandlerModule from './OHIFDicomSegSopClassHandler.js';
|
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -21,10 +23,97 @@ export default {
|
|||||||
getToolbarModule({ servicesManager }) {
|
getToolbarModule({ servicesManager }) {
|
||||||
return toolbarModule;
|
return toolbarModule;
|
||||||
},
|
},
|
||||||
getPanelModule({ servicesManager }) {
|
getPanelModule({ commandsManager, api, servicesManager }) {
|
||||||
return panelModule;
|
const { UINotificationService } = servicesManager;
|
||||||
},
|
|
||||||
getSopClassHandlerModule({ servicesManager }) {
|
const ExtendedSegmentationPanel = props => {
|
||||||
return sopClassHandlerModule;
|
const { activeContexts } = api.hooks.useAppContext();
|
||||||
|
|
||||||
|
const onDisplaySetLoadFailureHandler = error => {
|
||||||
|
UINotificationService.show({
|
||||||
|
title: 'DICOM Segmentation Loader',
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
autoClose: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const segmentItemClickHandler = data => {
|
||||||
|
commandsManager.runCommand('jumpToImage', data);
|
||||||
|
commandsManager.runCommand('jumpToSlice', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSegmentVisibilityChangeHandler = (segmentNumber, visible) => {
|
||||||
|
commandsManager.runCommand('setSegmentConfiguration', {
|
||||||
|
segmentNumber,
|
||||||
|
visible,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfigurationChangeHandler = configuration => {
|
||||||
|
commandsManager.runCommand('setSegmentationConfiguration', {
|
||||||
|
globalOpacity: configuration.fillAlpha,
|
||||||
|
outlineThickness: configuration.outlineWidth,
|
||||||
|
renderOutline: configuration.renderOutline,
|
||||||
|
visible: configuration.renderFill,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectedSegmentationChangeHandler = () => {
|
||||||
|
commandsManager.runCommand('requestNewSegmentation');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SegmentationPanel
|
||||||
|
{...props}
|
||||||
|
activeContexts={activeContexts}
|
||||||
|
contexts={api.contexts}
|
||||||
|
onSegmentItemClick={segmentItemClickHandler}
|
||||||
|
onSegmentVisibilityChange={onSegmentVisibilityChangeHandler}
|
||||||
|
onConfigurationChange={onConfigurationChangeHandler}
|
||||||
|
onSelectedSegmentationChange={onSelectedSegmentationChangeHandler}
|
||||||
|
onDisplaySetLoadFailure={onDisplaySetLoadFailureHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
icon: 'list',
|
||||||
|
label: 'Segmentations',
|
||||||
|
target: 'segmentation-panel',
|
||||||
|
isDisabled: studies => {
|
||||||
|
if (!studies) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < studies.length; i++) {
|
||||||
|
const study = studies[i];
|
||||||
|
|
||||||
|
if (study && study.series) {
|
||||||
|
for (let j = 0; j < study.series.length; j++) {
|
||||||
|
const series = study.series[j];
|
||||||
|
|
||||||
|
if (series.Modality === 'SEG') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: 'segmentation-panel',
|
||||||
|
component: ExtendedSegmentationPanel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultContext: ['VIEWER'],
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
getSopClassHandlerModule,
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import * as dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
|
||||||
@@ -34,28 +34,58 @@ export default async function loadSegmentation(
|
|||||||
referencedDisplaySet.SeriesInstanceUID
|
referencedDisplaySet.SeriesInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = _parseSeg(segArrayBuffer, imageIds);
|
return new Promise((resolve, reject) => {
|
||||||
|
let results;
|
||||||
|
|
||||||
if (!results) {
|
try {
|
||||||
throw new Error('Fractional segmentations are not yet supported');
|
results = _parseSeg(segArrayBuffer, imageIds);
|
||||||
}
|
} catch (error) {
|
||||||
|
segDisplaySet.isLoaded = false;
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
|
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
|
||||||
const { setters } = cornerstoneTools.getModule('segmentation');
|
const { setters } = cornerstoneTools.getModule('segmentation');
|
||||||
|
|
||||||
// TODO: Could define a color LUT based on colors in the SEG.
|
// TODO: Could define a color LUT based on colors in the SEG.
|
||||||
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
|
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
|
||||||
|
const colorLUTIndex = _makeColorLUTAndGetIndex(segMetadata);
|
||||||
|
|
||||||
setters.labelmap3DByFirstImageId(
|
setters.labelmap3DByFirstImageId(
|
||||||
imageIds[0],
|
imageIds[0],
|
||||||
labelmapBuffer,
|
labelmapBuffer,
|
||||||
labelmapIndex,
|
labelmapIndex,
|
||||||
segMetadata,
|
segMetadata,
|
||||||
imageIds.length,
|
imageIds.length,
|
||||||
segmentsOnFrame
|
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) {
|
function _getNextLabelmapIndex(firstImageId) {
|
||||||
@@ -79,6 +109,69 @@ function _getNextLabelmapIndex(firstImageId) {
|
|||||||
return labelmapIndex;
|
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) {
|
function _parseSeg(arrayBuffer, imageIds) {
|
||||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||||
imageIds,
|
imageIds,
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
icon: 'list',
|
|
||||||
label: 'Segmentations',
|
|
||||||
target: 'segmentation-panel',
|
|
||||||
isDisabled: studies => {
|
|
||||||
if (!studies) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < studies.length; i++) {
|
|
||||||
const study = studies[i];
|
|
||||||
|
|
||||||
if (study && study.series) {
|
|
||||||
for (let j = 0; j < study.series.length; j++) {
|
|
||||||
const series = study.series[j];
|
|
||||||
|
|
||||||
if (series.Modality === 'SEG') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
id: 'segmentation-panel',
|
|
||||||
component: SegmentationPanel,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultContext: ['VIEWER'],
|
|
||||||
};
|
|
||||||
@@ -3,6 +3,106 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.6.0...@ohif/extension-vtk@1.6.1) (2020-06-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.6...@ohif/extension-vtk@1.6.0) (2020-06-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.5...@ohif/extension-vtk@1.5.6) (2020-05-14)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.4...@ohif/extension-vtk@1.5.5) (2020-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.3...@ohif/extension-vtk@1.5.4) (2020-05-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.2...@ohif/extension-vtk@1.5.3) (2020-05-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.1...@ohif/extension-vtk@1.5.2) (2020-05-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.5.0...@ohif/extension-vtk@1.5.1) (2020-04-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.4.1...@ohif/extension-vtk@1.5.0) (2020-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.4.0...@ohif/extension-vtk@1.4.1) (2020-04-23)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.14...@ohif/extension-vtk@1.4.0) (2020-04-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.3.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.13...@ohif/extension-vtk@1.3.14) (2020-04-22)
|
## [1.3.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.13...@ohif/extension-vtk@1.3.14) (2020-04-22)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-vtk
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-vtk",
|
"name": "@ohif/extension-vtk",
|
||||||
"version": "1.3.14",
|
"version": "1.6.1",
|
||||||
"description": "OHIF extension for VTK.js",
|
"description": "OHIF extension for VTK.js",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -33,9 +33,9 @@
|
|||||||
"@ohif/i18n": "^0.50.0",
|
"@ohif/i18n": "^0.50.0",
|
||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.15.1",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.1.0",
|
||||||
"dcmjs": "^0.10.1",
|
"dcmjs": "^0.12.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
@@ -50,13 +50,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"react-vtkjs-viewport": "^0.8.3"
|
"react-vtkjs-viewport": "^0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^2.7.1",
|
"@ohif/core": "^2.9.7",
|
||||||
"@ohif/ui": "^1.4.3",
|
"@ohif/ui": "^1.5.0",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.15.1",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.1.0",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"gh-pages": "^2.0.1",
|
"gh-pages": "^2.0.1",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ class LoadingIndicator extends PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||||
<div className="indicatorContents">
|
<div className="indicatorContents">
|
||||||
<p>
|
<p>
|
||||||
{this.props.t('Reformatting')}...
|
{this.props.t('Loading...')}
|
||||||
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
||||||
{percComplete}
|
{percComplete}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
state = {
|
state = {
|
||||||
volumes: null,
|
volumes: null,
|
||||||
paintFilterLabelMapImageData: null,
|
paintFilterLabelMapImageData: null,
|
||||||
paintFilterBackgroundImageData: null,
|
paintFilterBackgroundImageData: null
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -69,7 +69,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onScroll: () => {},
|
onScroll: () => { },
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = 'OHIFVTKViewport';
|
static id = 'OHIFVTKViewport';
|
||||||
@@ -156,6 +156,10 @@ class OHIFVTKViewport extends Component {
|
|||||||
const { activeLabelmapIndex } = brushStackState;
|
const { activeLabelmapIndex } = brushStackState;
|
||||||
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
|
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
|
||||||
|
|
||||||
|
this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map(isHidden => {
|
||||||
|
return { visible: !isHidden };
|
||||||
|
});
|
||||||
|
|
||||||
const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`;
|
const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`;
|
||||||
|
|
||||||
if (labelmapCache[vtkLabelmapID]) {
|
if (labelmapCache[vtkLabelmapID]) {
|
||||||
@@ -339,13 +343,13 @@ class OHIFVTKViewport extends Component {
|
|||||||
this.setStateFromProps();
|
this.setStateFromProps();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { displaySet } = this.props.viewportData;
|
const { displaySet } = this.props.viewportData;
|
||||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
displaySet.displaySetInstanceUID !==
|
displaySet.displaySetInstanceUID !==
|
||||||
prevDisplaySet.displaySetInstanceUID ||
|
prevDisplaySet.displaySetInstanceUID ||
|
||||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||||
) {
|
) {
|
||||||
@@ -405,10 +409,6 @@ class OHIFVTKViewport extends Component {
|
|||||||
|
|
||||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||||
|
|
||||||
const visible = configuration.renderFill || configuration.renderOutline;
|
|
||||||
const opacity = configuration.fillAlpha;
|
|
||||||
const outlineThickness = configuration.outlineThickness;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
@@ -428,10 +428,14 @@ class OHIFVTKViewport extends Component {
|
|||||||
dataDetails={this.state.dataDetails}
|
dataDetails={this.state.dataDetails}
|
||||||
labelmapRenderingOptions={{
|
labelmapRenderingOptions={{
|
||||||
colorLUT: this.state.labelmapColorLUT,
|
colorLUT: this.state.labelmapColorLUT,
|
||||||
globalOpacity: opacity,
|
globalOpacity: configuration.fillAlpha,
|
||||||
visible,
|
visible: configuration.renderFill,
|
||||||
outlineThickness,
|
outlineThickness: configuration.outlineWidth,
|
||||||
renderOutline: true,
|
renderOutline: configuration.renderOutline,
|
||||||
|
segmentsDefaultProperties: this.segmentsDefaultProperties,
|
||||||
|
onNewSegmentationRequested: () => {
|
||||||
|
this.setStateFromProps();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import {
|
|||||||
vtkInteractorStyleMPRRotate,
|
vtkInteractorStyleMPRRotate,
|
||||||
vtkSVGCrosshairsWidget,
|
vtkSVGCrosshairsWidget,
|
||||||
} from 'react-vtkjs-viewport';
|
} from 'react-vtkjs-viewport';
|
||||||
|
import { getImageData } from 'react-vtkjs-viewport';
|
||||||
|
import { vec3 } from 'gl-matrix';
|
||||||
import setMPRLayout from './utils/setMPRLayout.js';
|
import setMPRLayout from './utils/setMPRLayout.js';
|
||||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||||
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
||||||
|
import OHIFVTKViewport from './OHIFVTKViewport';
|
||||||
|
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
||||||
|
|
||||||
const { BlendMode } = Constants;
|
const { BlendMode } = Constants;
|
||||||
|
|
||||||
@@ -34,7 +37,6 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displaySet = viewportSpecificData[activeViewportIndex];
|
const displaySet = viewportSpecificData[activeViewportIndex];
|
||||||
|
|
||||||
let api;
|
let api;
|
||||||
if (!api) {
|
if (!api) {
|
||||||
try {
|
try {
|
||||||
@@ -99,6 +101,20 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _convertModelToWorldSpace = (position, vtkImageData) => {
|
||||||
|
const indexToWorld = vtkImageData.getIndexToWorld();
|
||||||
|
const pos = vec3.create();
|
||||||
|
|
||||||
|
position[0] += 0.5; /* Move to the centre of the voxel. */
|
||||||
|
position[1] += 0.5; /* Move to the centre of the voxel. */
|
||||||
|
position[2] += 0.5; /* Move to the centre of the voxel. */
|
||||||
|
|
||||||
|
vec3.set(pos, position[0], position[1], position[2]);
|
||||||
|
vec3.transformMat4(pos, pos, indexToWorld);
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getVtkApis: ({ index }) => {
|
getVtkApis: ({ index }) => {
|
||||||
return apis[index];
|
return apis[index];
|
||||||
@@ -124,6 +140,119 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
|
|
||||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||||
},
|
},
|
||||||
|
requestNewSegmentation: async ({ viewports }) => {
|
||||||
|
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||||
|
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||||
|
let api = apis[viewportIndex];
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
apis[viewportIndex] = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.requestNewSegmentation();
|
||||||
|
api.updateImage();
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
},
|
||||||
|
jumpToSlice: async ({
|
||||||
|
viewports,
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
SOPClassUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
segmentNumber,
|
||||||
|
frameIndex,
|
||||||
|
frame,
|
||||||
|
done = () => { }
|
||||||
|
}) => {
|
||||||
|
let api = apis[viewports.activeViewportIndex];
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
apis[viewports.activeViewportIndex] = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
SOPClassUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||||
|
|
||||||
|
let pixelIndex = 0;
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const rows = imageDataObject.dimensions[1];
|
||||||
|
const cols = imageDataObject.dimensions[0];
|
||||||
|
|
||||||
|
for (let j = 0; j < rows; j++) {
|
||||||
|
for (let i = 0; i < cols; i++) {
|
||||||
|
// [i, j] =
|
||||||
|
const pixel = frame.pixelData[pixelIndex];
|
||||||
|
if (pixel === segmentNumber) {
|
||||||
|
x += i;
|
||||||
|
y += j;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
pixelIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x /= count;
|
||||||
|
y /= count;
|
||||||
|
|
||||||
|
const position = [x, y, frameIndex];
|
||||||
|
const worldPos = _convertModelToWorldSpace(position, imageDataObject.vtkImageData);
|
||||||
|
|
||||||
|
api.svgWidgets.crosshairsWidget.moveCrosshairs(worldPos, apis);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
setSegmentationConfiguration: async ({
|
||||||
|
viewports,
|
||||||
|
globalOpacity,
|
||||||
|
visible,
|
||||||
|
renderOutline,
|
||||||
|
outlineThickness,
|
||||||
|
}) => {
|
||||||
|
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||||
|
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||||
|
let api = apis[viewportIndex];
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
apis[viewportIndex] = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.setGlobalOpacity(globalOpacity);
|
||||||
|
api.setVisibility(visible);
|
||||||
|
api.setOutlineThickness(outlineThickness);
|
||||||
|
api.setOutlineRendering(renderOutline);
|
||||||
|
api.updateImage();
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
},
|
||||||
|
setSegmentConfiguration: async ({ viewports, visible, segmentNumber }) => {
|
||||||
|
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||||
|
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||||
|
let api = apis[viewportIndex];
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
apis[viewportIndex] = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.setSegmentVisibility(segmentNumber, visible);
|
||||||
|
api.updateImage();
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
},
|
||||||
enableRotateTool: () => {
|
enableRotateTool: () => {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||||
@@ -280,6 +409,26 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
window.vtkActions = actions;
|
window.vtkActions = actions;
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
|
requestNewSegmentation: {
|
||||||
|
commandFn: actions.requestNewSegmentation,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
jumpToSlice: {
|
||||||
|
commandFn: actions.jumpToSlice,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
setSegmentationConfiguration: {
|
||||||
|
commandFn: actions.setSegmentationConfiguration,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
setSegmentConfiguration: {
|
||||||
|
commandFn: actions.setSegmentConfiguration,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
axial: {
|
axial: {
|
||||||
commandFn: actions.axial,
|
commandFn: actions.axial,
|
||||||
storeContexts: ['viewports'],
|
storeContexts: ['viewports'],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import asyncComponent from './asyncComponent.js';
|
import asyncComponent from './asyncComponent.js';
|
||||||
import commandsModule from './commandsModule.js';
|
import commandsModule from './commandsModule.js';
|
||||||
import toolbarModule from './toolbarModule.js';
|
import toolbarModule from './toolbarModule.js';
|
||||||
@@ -16,7 +17,8 @@ const vtkExtension = {
|
|||||||
id: 'vtk',
|
id: 'vtk',
|
||||||
|
|
||||||
getViewportModule({ commandsManager }) {
|
getViewportModule({ commandsManager }) {
|
||||||
return withCommandsManager(OHIFVTKViewport, commandsManager);
|
const ExtendedVTKViewport = props => <OHIFVTKViewport {...props} />;
|
||||||
|
return withCommandsManager(ExtendedVTKViewport, commandsManager);
|
||||||
},
|
},
|
||||||
getToolbarModule() {
|
getToolbarModule() {
|
||||||
return toolbarModule;
|
return toolbarModule;
|
||||||
|
|||||||
@@ -3,6 +3,107 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.9.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.6...@ohif/core@2.9.7) (2020-06-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.9.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.5...@ohif/core@2.9.6) (2020-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Load default display set when no time metadata ([#1684](https://github.com/OHIF/Viewers/issues/1684)) ([f7b8b6a](https://github.com/OHIF/Viewers/commit/f7b8b6a41c4626084ef56b0fdf7363e914b143c4)), closes [#1683](https://github.com/OHIF/Viewers/issues/1683)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.9.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.4...@ohif/core@2.9.5) (2020-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.9.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.3...@ohif/core@2.9.4) (2020-05-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.9.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.2...@ohif/core@2.9.3) (2020-05-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.9.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.1...@ohif/core@2.9.2) (2020-05-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* use bit-appropriate array for palette lookup tables ([#1698](https://github.com/OHIF/Viewers/issues/1698)) ([7033886](https://github.com/OHIF/Viewers/commit/70338866978a76fa026c18d7c3c05257c5ece762))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.0...@ohif/core@2.9.1) (2020-04-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.8.1...@ohif/core@2.9.0) (2020-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.8.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.8.0...@ohif/core@2.8.1) (2020-04-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Multiframe fix ([#1661](https://github.com/OHIF/Viewers/issues/1661)) ([7120561](https://github.com/OHIF/Viewers/commit/71205618ecb8b592247c5acb32284bfe7e18fce5))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.8.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.7.1...@ohif/core@2.8.0) (2020-04-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.7.0...@ohif/core@2.7.1) (2020-04-22)
|
## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.7.0...@ohif/core@2.7.1) (2020-04-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "2.7.1",
|
"version": "2.9.7",
|
||||||
"description": "Generic business logic for web-based medical imaging applications",
|
"description": "Generic business logic for web-based medical imaging applications",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -32,15 +32,15 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.15.1",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.1.0",
|
||||||
"dicom-parser": "^1.8.3"
|
"dicom-parser": "^1.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
"dcmjs": "^0.12.0",
|
"dcmjs": "^0.12.2",
|
||||||
"dicomweb-client": "^0.5.2",
|
"dicomweb-client": "^0.6.0",
|
||||||
"immer": "6.0.2",
|
"immer": "6.0.2",
|
||||||
"isomorphic-base64": "^1.0.2",
|
"isomorphic-base64": "^1.0.2",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import * as dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
|
|
||||||
import DICOMWeb from '../DICOMWeb';
|
import DICOMWeb from '../DICOMWeb';
|
||||||
import parseDicomStructuredReport from './parseDicomStructuredReport';
|
import parseDicomStructuredReport from './parseDicomStructuredReport';
|
||||||
import parseMeasurementsData from './parseMeasurementsData';
|
import parseMeasurementsData from './parseMeasurementsData';
|
||||||
import getAllDisplaySets from './utils/getAllDisplaySets';
|
import getAllDisplaySets from './utils/getAllDisplaySets';
|
||||||
|
import errorHandler from '../errorHandler';
|
||||||
|
|
||||||
const VERSION_NAME = 'dcmjs-0.0';
|
const VERSION_NAME = 'dcmjs-0.0';
|
||||||
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
||||||
@@ -21,6 +22,7 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
|||||||
const config = {
|
const config = {
|
||||||
url: serverUrl,
|
url: serverUrl,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(),
|
headers: DICOMWeb.getAuthorizationHeader(),
|
||||||
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
@@ -71,6 +73,7 @@ const stowSRFromMeasurements = async (measurements, serverUrl) => {
|
|||||||
const config = {
|
const config = {
|
||||||
url: serverUrl,
|
url: serverUrl,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(),
|
headers: DICOMWeb.getAuthorizationHeader(),
|
||||||
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
|
|
||||||
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
import log from '../log';
|
import log from '../log';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const findMostRecentStructuredReport = studies => {
|
|||||||
// Skip series that may not have instances yet
|
// Skip series that may not have instances yet
|
||||||
// This can happen if we have retrieved just the initial
|
// This can happen if we have retrieved just the initial
|
||||||
// details about the series via QIDO-RS, but not the full metadata
|
// details about the series via QIDO-RS, but not the full metadata
|
||||||
if (!series.instances.length) {
|
if (!series.instances || !series.instances.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if dcmjs has support to determined tool
|
* Checks if dcmjs has support to determined tool
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import dicomParser from 'dicom-parser';
|
import dicomParser from 'dicom-parser';
|
||||||
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
|
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
|
||||||
@@ -400,6 +400,7 @@ class MetadataProvider {
|
|||||||
|
|
||||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
||||||
metadata = {
|
metadata = {
|
||||||
|
sopInstanceUid: instance.SOPInstanceUID,
|
||||||
instanceNumber: instance.InstanceNumber,
|
instanceNumber: instance.InstanceNumber,
|
||||||
lossyImageCompression: instance.LossyImageCompression,
|
lossyImageCompression: instance.LossyImageCompression,
|
||||||
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { api } from 'dicomweb-client';
|
|||||||
import { isImage } from '../../utils/isImage';
|
import { isImage } from '../../utils/isImage';
|
||||||
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
|
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
|
||||||
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||||
|
import errorHandler from '../../errorHandler';
|
||||||
|
|
||||||
export class StudyMetadata extends Metadata {
|
export class StudyMetadata extends Metadata {
|
||||||
constructor(data, uid) {
|
constructor(data, uid) {
|
||||||
@@ -78,7 +79,7 @@ export class StudyMetadata extends Metadata {
|
|||||||
Object.defineProperty(this, 'studyInstanceUID', {
|
Object.defineProperty(this, 'studyInstanceUID', {
|
||||||
configurable: false,
|
configurable: false,
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
get: function() {
|
get: function () {
|
||||||
return this.getStudyInstanceUID();
|
return this.getStudyInstanceUID();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -102,10 +103,7 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
* @returns {Array} The list of display sets created for the given series object
|
* @returns {Array} The list of display sets created for the given series object
|
||||||
*/
|
*/
|
||||||
_createDisplaySetsForSeries(
|
_createDisplaySetsForSeries(sopClassHandlerModules, series) {
|
||||||
sopClassHandlerModules,
|
|
||||||
series,
|
|
||||||
) {
|
|
||||||
const study = this;
|
const study = this;
|
||||||
const displaySets = [];
|
const displaySets = [];
|
||||||
|
|
||||||
@@ -293,14 +291,13 @@ export class StudyMetadata extends Metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loop through the series (SeriesMetadata)
|
// Loop through the series (SeriesMetadata)
|
||||||
this.forEachSeries(
|
this.forEachSeries(series => {
|
||||||
series => {
|
const displaySetsForSeries = this._createDisplaySetsForSeries(
|
||||||
const displaySetsForSeries = this._createDisplaySetsForSeries(
|
sopClassHandlerModules,
|
||||||
sopClassHandlerModules,
|
series
|
||||||
series,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
displaySets.push(...displaySetsForSeries);
|
displaySets.push(...displaySetsForSeries);
|
||||||
});
|
});
|
||||||
|
|
||||||
return sortDisplaySetList(displaySets);
|
return sortDisplaySetList(displaySets);
|
||||||
@@ -321,7 +318,10 @@ export class StudyMetadata extends Metadata {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySets = this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
const displaySets = this._createDisplaySetsForSeries(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
series
|
||||||
|
);
|
||||||
|
|
||||||
// Note: filtering in place because this._displaySets has writable: false
|
// Note: filtering in place because this._displaySets has writable: false
|
||||||
for (let i = this._displaySets.length - 1; i >= 0; i--) {
|
for (let i = this._displaySets.length - 1; i >= 0; i--) {
|
||||||
@@ -835,9 +835,11 @@ function _getDisplaySetFromSopClassModule(
|
|||||||
|
|
||||||
const plugin = handlersForSopClassUID[0];
|
const plugin = handlersForSopClassUID[0];
|
||||||
const headers = DICOMWeb.getAuthorizationHeader();
|
const headers = DICOMWeb.getAuthorizationHeader();
|
||||||
|
const errorInterceptor = errorHandler.getHTTPErrorHandler();
|
||||||
const dicomWebClient = new dwc({
|
const dicomWebClient = new dwc({
|
||||||
url: study.getData().wadoRoot,
|
url: study.getData().wadoRoot,
|
||||||
headers,
|
headers,
|
||||||
|
errorInterceptor,
|
||||||
});
|
});
|
||||||
|
|
||||||
let displaySet = plugin.getDisplaySetFromSeries(
|
let displaySet = plugin.getDisplaySetFromSeries(
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// These should be overridden by the implementation
|
||||||
|
const errorHandler = {
|
||||||
|
getHTTPErrorHandler: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default errorHandler;
|
||||||
@@ -2,7 +2,7 @@ import MODULE_TYPES from './MODULE_TYPES.js';
|
|||||||
import log from './../log.js';
|
import log from './../log.js';
|
||||||
|
|
||||||
export default class ExtensionManager {
|
export default class ExtensionManager {
|
||||||
constructor({ commandsManager, servicesManager, appConfig = {} }) {
|
constructor({ commandsManager, servicesManager, api, appConfig = {} }) {
|
||||||
this.modules = {};
|
this.modules = {};
|
||||||
this.registeredExtensionIds = [];
|
this.registeredExtensionIds = [];
|
||||||
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
||||||
@@ -10,6 +10,7 @@ export default class ExtensionManager {
|
|||||||
this._commandsManager = commandsManager;
|
this._commandsManager = commandsManager;
|
||||||
this._servicesManager = servicesManager;
|
this._servicesManager = servicesManager;
|
||||||
this._appConfig = appConfig;
|
this._appConfig = appConfig;
|
||||||
|
this._api = api;
|
||||||
|
|
||||||
this.moduleTypeNames.forEach(moduleType => {
|
this.moduleTypeNames.forEach(moduleType => {
|
||||||
this.modules[moduleType] = [];
|
this.modules[moduleType] = [];
|
||||||
@@ -119,6 +120,7 @@ export default class ExtensionManager {
|
|||||||
commandsManager: this._commandsManager,
|
commandsManager: this._commandsManager,
|
||||||
appConfig: this._appConfig,
|
appConfig: this._appConfig,
|
||||||
configuration,
|
configuration,
|
||||||
|
api: this._api
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!extensionModule) {
|
if (!extensionModule) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import string from './string.js';
|
|||||||
import studies from './studies/';
|
import studies from './studies/';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
|
import errorHandler from './errorHandler.js';
|
||||||
import utils, { hotkeys } from './utils/';
|
import utils, { hotkeys } from './utils/';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -46,6 +47,7 @@ const OHIF = {
|
|||||||
string,
|
string,
|
||||||
ui,
|
ui,
|
||||||
user,
|
user,
|
||||||
|
errorHandler,
|
||||||
object,
|
object,
|
||||||
log,
|
log,
|
||||||
DICOMWeb,
|
DICOMWeb,
|
||||||
@@ -79,6 +81,7 @@ export {
|
|||||||
string,
|
string,
|
||||||
ui,
|
ui,
|
||||||
user,
|
user,
|
||||||
|
errorHandler,
|
||||||
object,
|
object,
|
||||||
log,
|
log,
|
||||||
DICOMWeb,
|
DICOMWeb,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe('Top level exports', () => {
|
|||||||
'header',
|
'header',
|
||||||
'cornerstone',
|
'cornerstone',
|
||||||
'default', //
|
'default', //
|
||||||
|
'errorHandler',
|
||||||
'string',
|
'string',
|
||||||
'ui',
|
'ui',
|
||||||
'user',
|
'user',
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ const defaultState = {
|
|||||||
|
|
||||||
const servers = (state = defaultState, action) => {
|
const servers = (state = defaultState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_STUDY_DATA':
|
case 'SET_STUDY_DATA': {
|
||||||
const updatedStudyData = cloneDeep(state).studyData;
|
const updatedStudyData = cloneDeep(state.studyData);
|
||||||
updatedStudyData[action.StudyInstanceUID] = action.data;
|
updatedStudyData[action.StudyInstanceUID] = cloneDeep(action.data);
|
||||||
|
|
||||||
return Object.assign({}, state, { studyData: updatedStudyData });
|
return Object.assign({}, state, { studyData: updatedStudyData });
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import DICOMWeb from '../../../DICOMWeb/';
|
import DICOMWeb from '../../../DICOMWeb/';
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
|
|
||||||
|
import errorHandler from '../../../errorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses data returned from a QIDO search and transforms it into
|
* Parses data returned from a QIDO search and transforms it into
|
||||||
* an array of series that are present in the study
|
* an array of series that are present in the study
|
||||||
@@ -79,6 +81,7 @@ export default function Instances(server, StudyInstanceUID) {
|
|||||||
const config = {
|
const config = {
|
||||||
url: server.qidoRoot,
|
url: server.qidoRoot,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
};
|
};
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
const queryParams = getQIDOQueryParams(
|
const queryParams = getQIDOQueryParams(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import DICOMWeb from '../../../DICOMWeb/';
|
import DICOMWeb from '../../../DICOMWeb/';
|
||||||
|
|
||||||
|
import errorHandler from '../../../errorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a QIDO date string for a date range query
|
* Creates a QIDO date string for a date range query
|
||||||
* Assumes the year is positive, at most 4 digits long.
|
* Assumes the year is positive, at most 4 digits long.
|
||||||
@@ -115,6 +117,7 @@ export default function Studies(server, filter) {
|
|||||||
const config = {
|
const config = {
|
||||||
url: server.qidoRoot,
|
url: server.qidoRoot,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
addInstancesToStudy,
|
addInstancesToStudy,
|
||||||
} from './studyInstanceHelpers';
|
} from './studyInstanceHelpers';
|
||||||
|
|
||||||
|
import errorHandler from '../../../errorHandler';
|
||||||
|
|
||||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,6 +75,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
|||||||
const client = new api.DICOMwebClient({
|
const client = new api.DICOMwebClient({
|
||||||
url: server.qidoRoot,
|
url: server.qidoRoot,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client = client;
|
this.client = client;
|
||||||
@@ -117,7 +120,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
seriesInstanceUIDsMap,
|
seriesInstanceUIDsMap,
|
||||||
seriesData
|
seriesData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +158,14 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
|||||||
SeriesDescription: series.SeriesDescription,
|
SeriesDescription: series.SeriesDescription,
|
||||||
SeriesNumber: series.SeriesNumber,
|
SeriesNumber: series.SeriesNumber,
|
||||||
Modality: series.Modality,
|
Modality: series.Modality,
|
||||||
instances: []
|
instances: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (study.series[idx]) {
|
if (study.series[idx]) {
|
||||||
study.series[idx] = Object.assign(seriesDataFromQIDO, study.series[idx]);
|
study.series[idx] = Object.assign(
|
||||||
|
seriesDataFromQIDO,
|
||||||
|
study.series[idx]
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
study.series[idx] = seriesDataFromQIDO;
|
study.series[idx] = seriesDataFromQIDO;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import DICOMWeb from '../../../DICOMWeb/';
|
|||||||
import { createStudyFromSOPInstanceList } from './studyInstanceHelpers';
|
import { createStudyFromSOPInstanceList } from './studyInstanceHelpers';
|
||||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||||
|
|
||||||
|
import errorHandler from '../../../errorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for sync load of study metadata.
|
* Class for sync load of study metadata.
|
||||||
* It inherits from RetrieveMetadataLoader
|
* It inherits from RetrieveMetadataLoader
|
||||||
@@ -58,6 +60,7 @@ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader {
|
|||||||
const client = new api.DICOMwebClient({
|
const client = new api.DICOMwebClient({
|
||||||
url: server.wadoRoot,
|
url: server.wadoRoot,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ async function makeSOPInstance(server, study, instance) {
|
|||||||
wadorsuri,
|
wadorsuri,
|
||||||
wadoRoot: server.wadoRoot,
|
wadoRoot: server.wadoRoot,
|
||||||
imageRendering: server.imageRendering,
|
imageRendering: server.imageRendering,
|
||||||
thumbnailRendering: server.thumbnailRendering
|
thumbnailRendering: server.thumbnailRendering,
|
||||||
};
|
};
|
||||||
|
|
||||||
series.instances.push(sopInstance);
|
series.instances.push(sopInstance);
|
||||||
@@ -160,8 +160,10 @@ async function makeSOPInstance(server, study, instance) {
|
|||||||
|
|
||||||
const wadoRSMetadata = Object.assign(instance);
|
const wadoRSMetadata = Object.assign(instance);
|
||||||
|
|
||||||
if (sopInstance.NumberOfFrames) {
|
const { NumberOfFrames } = sopInstance.metadata;
|
||||||
for (let i = 0; i < sopInstance.NumberOfFrames; i++) {
|
|
||||||
|
if (NumberOfFrames) {
|
||||||
|
for (let i = 0; i < NumberOfFrames; i++) {
|
||||||
const wadorsImageId = getWADORSImageId(sopInstance, i);
|
const wadorsImageId = getWADORSImageId(sopInstance, i);
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
|
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
|
||||||
@@ -190,7 +192,7 @@ async function makeSOPInstance(server, study, instance) {
|
|||||||
*/
|
*/
|
||||||
async function addInstancesToStudy(server, study, sopInstanceList) {
|
async function addInstancesToStudy(server, study, sopInstanceList) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
sopInstanceList.map(function (sopInstance) {
|
sopInstanceList.map(function(sopInstance) {
|
||||||
return makeSOPInstance(server, study, sopInstance);
|
return makeSOPInstance(server, study, sopInstance);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
|||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import DICOMWeb from '../DICOMWeb';
|
import DICOMWeb from '../DICOMWeb';
|
||||||
|
|
||||||
|
import errorHandler from '../errorHandler';
|
||||||
|
|
||||||
const getImageId = imageObj => {
|
const getImageId = imageObj => {
|
||||||
if (!imageObj) {
|
if (!imageObj) {
|
||||||
return;
|
return;
|
||||||
@@ -57,11 +59,13 @@ const wadorsRetriever = (
|
|||||||
studyInstanceUID,
|
studyInstanceUID,
|
||||||
seriesInstanceUID,
|
seriesInstanceUID,
|
||||||
sopInstanceUID,
|
sopInstanceUID,
|
||||||
headers = DICOMWeb.getAuthorizationHeader()
|
headers = DICOMWeb.getAuthorizationHeader(),
|
||||||
|
errorInterceptor = errorHandler.getHTTPErrorHandler()
|
||||||
) => {
|
) => {
|
||||||
const config = {
|
const config = {
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
|
errorInterceptor,
|
||||||
};
|
};
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,12 @@ import studyMetadataManager from './studyMetadataManager';
|
|||||||
const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
||||||
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||||
|
|
||||||
if (!studyMetadata) {
|
if (!studyMetadata) {
|
||||||
return;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
const derivedDisplaySets = studyMetadata.getDerivedDatasets({
|
const derivedDisplaySets = studyMetadata.getDerivedDatasets({
|
||||||
@@ -61,7 +63,7 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!derivedDisplaySets.length) {
|
if (!derivedDisplaySets.length) {
|
||||||
return;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by type
|
// Filter by type
|
||||||
@@ -88,7 +90,7 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
|||||||
|
|
||||||
// find most recent and load it.
|
// find most recent and load it.
|
||||||
let recentDateTime = 0;
|
let recentDateTime = 0;
|
||||||
let recentDisplaySet;
|
let recentDisplaySet = displaySets[0];
|
||||||
|
|
||||||
displaySets.forEach(displaySet => {
|
displaySets.forEach(displaySet => {
|
||||||
const dateTime = Number(
|
const dateTime = Number(
|
||||||
@@ -100,8 +102,10 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
recentDisplaySet.load(referencedDisplaySet, studies);
|
promises.push(recentDisplaySet.load(referencedDisplaySet, studies));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return promises;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default loadAndCacheDerivedDisplaySets;
|
export default loadAndCacheDerivedDisplaySets;
|
||||||
@@ -3,6 +3,8 @@ import DICOMWeb from '../../DICOMWeb';
|
|||||||
import str2ab from '../str2ab';
|
import str2ab from '../str2ab';
|
||||||
import unpackOverlay from './unpackOverlay';
|
import unpackOverlay from './unpackOverlay';
|
||||||
|
|
||||||
|
import errorHandler from '../../errorHandler';
|
||||||
|
|
||||||
export default async function fetchOverlayData(instance, server) {
|
export default async function fetchOverlayData(instance, server) {
|
||||||
const OverlayDataPromises = [];
|
const OverlayDataPromises = [];
|
||||||
const OverlayDataTags = [];
|
const OverlayDataTags = [];
|
||||||
@@ -21,14 +23,20 @@ export default async function fetchOverlayData(instance, server) {
|
|||||||
const inlineBinaryData = atob(instance[OverlayDataTag].InlineBinary);
|
const inlineBinaryData = atob(instance[OverlayDataTag].InlineBinary);
|
||||||
const arraybuffer = str2ab(inlineBinaryData);
|
const arraybuffer = str2ab(inlineBinaryData);
|
||||||
|
|
||||||
instance[OverlayDataTag] = unpackOverlay(arraybuffer)
|
instance[OverlayDataTag] = unpackOverlay(arraybuffer);
|
||||||
} else if (instance[OverlayDataTag] && instance[OverlayDataTag].BulkDataURI) {
|
} else if (
|
||||||
|
instance[OverlayDataTag] &&
|
||||||
|
instance[OverlayDataTag].BulkDataURI
|
||||||
|
) {
|
||||||
OverlayDataPromises.push(
|
OverlayDataPromises.push(
|
||||||
_getOverlayData(instance[OverlayDataTag], server)
|
_getOverlayData(instance[OverlayDataTag], server)
|
||||||
);
|
);
|
||||||
OverlayDataTags.push(OverlayDataTag);
|
OverlayDataTags.push(OverlayDataTag);
|
||||||
} else if (instance[OverlayDataTag] && instance[OverlayDataTag] instanceof ArrayBuffer) {
|
} else if (
|
||||||
instance[OverlayDataTag] = unpackOverlay(instance[OverlayDataTag])
|
instance[OverlayDataTag] &&
|
||||||
|
instance[OverlayDataTag] instanceof ArrayBuffer
|
||||||
|
) {
|
||||||
|
instance[OverlayDataTag] = unpackOverlay(instance[OverlayDataTag]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +68,7 @@ async function _getOverlayData(tag, server) {
|
|||||||
const config = {
|
const config = {
|
||||||
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
};
|
};
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
const options = {
|
const options = {
|
||||||
@@ -71,5 +80,3 @@ async function _getOverlayData(tag, server) {
|
|||||||
.then(result => result[0])
|
.then(result => result[0])
|
||||||
.then(unpackOverlay);
|
.then(unpackOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2,6 +2,8 @@ import { api } from 'dicomweb-client';
|
|||||||
import DICOMWeb from '../../DICOMWeb';
|
import DICOMWeb from '../../DICOMWeb';
|
||||||
import str2ab from '../str2ab';
|
import str2ab from '../str2ab';
|
||||||
|
|
||||||
|
import errorHandler from '../../errorHandler';
|
||||||
|
|
||||||
export default async function fetchPaletteColorLookupTableData(
|
export default async function fetchPaletteColorLookupTableData(
|
||||||
instance,
|
instance,
|
||||||
server
|
server
|
||||||
@@ -113,22 +115,14 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) {
|
|||||||
const numLutEntries = lutDescriptor[0];
|
const numLutEntries = lutDescriptor[0];
|
||||||
const bits = lutDescriptor[2];
|
const bits = lutDescriptor[2];
|
||||||
|
|
||||||
const readUInt16 = (byteArray, position) => {
|
|
||||||
return byteArray[position] + byteArray[position + 1] * 256;
|
|
||||||
};
|
|
||||||
|
|
||||||
const arrayBufferToPaletteColorLUT = arraybuffer => {
|
const arrayBufferToPaletteColorLUT = arraybuffer => {
|
||||||
const byteArray = new Uint8Array(arraybuffer);
|
const byteArray = bits === 16 ?
|
||||||
|
new Uint16Array(arraybuffer) :
|
||||||
|
new Uint8Array(arraybuffer);
|
||||||
const lut = [];
|
const lut = [];
|
||||||
|
|
||||||
if (bits === 16) {
|
for (let i = 0; i < numLutEntries; i++) {
|
||||||
for (let i = 0; i < numLutEntries; i++) {
|
lut[i] = byteArray[i];
|
||||||
lut[i] = readUInt16(byteArray, i * 2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < numLutEntries; i++) {
|
|
||||||
lut[i] = byteArray[i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return lut;
|
return lut;
|
||||||
@@ -146,6 +140,7 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) {
|
|||||||
const config = {
|
const config = {
|
||||||
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
};
|
};
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
const options = {
|
const options = {
|
||||||
@@ -164,6 +159,8 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) {
|
|||||||
resolve(arrayBufferToPaletteColorLUT(arraybuffer));
|
resolve(arrayBufferToPaletteColorLUT(arraybuffer));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(arrayBufferToPaletteColorLUT(paletteColorLookupTableData));
|
return Promise.resolve(
|
||||||
|
arrayBufferToPaletteColorLUT(paletteColorLookupTableData)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,28 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.4...@ohif/ui@1.5.0) (2020-06-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 1729 - error boundary wrapper ([#1764](https://github.com/OHIF/Viewers/issues/1764)) ([c02b232](https://github.com/OHIF/Viewers/commit/c02b232b0cc24f38af5d5e3831d987d048e60ada))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.3...@ohif/ui@1.4.4) (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.4.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.2...@ohif/ui@1.4.3) (2020-04-09)
|
## [1.4.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.2...@ohif/ui@1.4.3) (2020-04-09)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/ui
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "1.4.3",
|
"version": "1.5.0",
|
||||||
"description": "A set of React components for Medical Imaging Viewers",
|
"description": "A set of React components for Medical Imaging Viewers",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
"react-dnd-html5-backend": "^9.4.0",
|
"react-dnd-html5-backend": "^9.4.0",
|
||||||
"react-dnd-touch-backend": "^9.4.0",
|
"react-dnd-touch-backend": "^9.4.0",
|
||||||
"react-draggable": "^4.1.0",
|
"react-draggable": "^4.1.0",
|
||||||
|
"react-error-boundary": "^2.2.1",
|
||||||
"react-i18next": "^10.11.0",
|
"react-i18next": "^10.11.0",
|
||||||
"react-modal": "^3.11.1",
|
"react-modal": "^3.11.1",
|
||||||
"react-with-direction": "1.3.0"
|
"react-with-direction": "1.3.0"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import './ErrorFallback.css';
|
||||||
|
|
||||||
|
const ErrorFallback = ({ error, componentStack, resetErrorBoundary }) => {
|
||||||
|
return (
|
||||||
|
<div className="ErrorFallback" role="alert">
|
||||||
|
<p>Something went wrong.</p>
|
||||||
|
<pre>{error.message}</pre>
|
||||||
|
<pre>{componentStack}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OHIFErrorBoundary = ({
|
||||||
|
context = 'OHIF',
|
||||||
|
onReset = () => {},
|
||||||
|
onError = () => {},
|
||||||
|
fallbackComponent,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const onErrorHandler = (error, componentStack) => {
|
||||||
|
console.error(`${context} Error Boundary`, error, componentStack);
|
||||||
|
onError(error, componentStack);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResetHandler = () => {
|
||||||
|
onReset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
FallbackComponent={fallbackComponent || ErrorFallback}
|
||||||
|
onReset={onResetHandler}
|
||||||
|
onError={onErrorHandler}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
OHIFErrorBoundary.propTypes = {
|
||||||
|
context: PropTypes.string,
|
||||||
|
onReset: PropTypes.func,
|
||||||
|
onError: PropTypes.func,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
fallbackComponent: PropTypes.element,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OHIFErrorBoundary;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.ErrorFallback {
|
||||||
|
padding: 10px;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as ErrorBoundary } from './ErrorBoundary';
|
||||||
@@ -7,7 +7,6 @@ import { AboutContent } from './content/aboutContent/AboutContent';
|
|||||||
import { TabComponents, TabFooter } from './tabComponents';
|
import { TabComponents, TabFooter } from './tabComponents';
|
||||||
import { HotkeyField } from './customForm';
|
import { HotkeyField } from './customForm';
|
||||||
import { LanguageSwitcher } from './languageSwitcher';
|
import { LanguageSwitcher } from './languageSwitcher';
|
||||||
|
|
||||||
import { Checkbox } from './checkbox';
|
import { Checkbox } from './checkbox';
|
||||||
import { CineDialog } from './cineDialog';
|
import { CineDialog } from './cineDialog';
|
||||||
import { ViewportDownloadForm } from './content/viewportDownloadForm';
|
import { ViewportDownloadForm } from './content/viewportDownloadForm';
|
||||||
@@ -25,8 +24,10 @@ import {
|
|||||||
} from './studyList';
|
} from './studyList';
|
||||||
import { ToolbarSection } from './toolbarSection';
|
import { ToolbarSection } from './toolbarSection';
|
||||||
import { Tooltip } from './tooltip';
|
import { Tooltip } from './tooltip';
|
||||||
|
import { ErrorBoundary } from './errorBoundary';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
ErrorBoundary,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CineDialog,
|
CineDialog,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
|
|
||||||
.sb-message {
|
.sb-message {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
word-break: break-all;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-item {
|
.sb-item {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
AboutContent,
|
AboutContent,
|
||||||
OHIFModal,
|
OHIFModal,
|
||||||
|
ErrorBoundary
|
||||||
} from './components';
|
} from './components';
|
||||||
import { useDebounce, useMedia } from './hooks';
|
import { useDebounce, useMedia } from './hooks';
|
||||||
|
|
||||||
@@ -123,6 +124,7 @@ export {
|
|||||||
DialogProvider,
|
DialogProvider,
|
||||||
withDialog,
|
withDialog,
|
||||||
useDialog,
|
useDialog,
|
||||||
|
ErrorBoundary,
|
||||||
// Hooks
|
// Hooks
|
||||||
useDebounce,
|
useDebounce,
|
||||||
useMedia,
|
useMedia,
|
||||||
|
|||||||
@@ -3,6 +3,202 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [4.2.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.2.1...@ohif/viewer@4.2.2) (2020-06-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [4.2.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.2.0...@ohif/viewer@4.2.1) (2020-06-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [4.2.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.1.0...@ohif/viewer@4.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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [4.1.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.0.0...@ohif/viewer@4.1.0) (2020-05-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* expose some app internals as window.app ([#1735](https://github.com/OHIF/Viewers/issues/1735)) ([63fd656](https://github.com/OHIF/Viewers/commit/63fd65690cba450721870a6222e0fb3ad71bb291))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [4.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.11...@ohif/viewer@4.0.0) (2020-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix race condition when loading derived display sets ([#1718](https://github.com/OHIF/Viewers/issues/1718)) ([b1678ce](https://github.com/OHIF/Viewers/commit/b1678ce6399dde37a9878f45ccc7c63286d93fab)), closes [#1715](https://github.com/OHIF/Viewers/issues/1715)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* 🧨 However we start to load once the first set of metadata arrives. We need
|
||||||
|
to wait until all series metadata is fetched.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.10...@ohif/viewer@3.11.11) (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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.9...@ohif/viewer@3.11.10) (2020-05-13)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.8...@ohif/viewer@3.11.9) (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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.7...@ohif/viewer@3.11.8) (2020-05-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.6...@ohif/viewer@3.11.7) (2020-05-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.5...@ohif/viewer@3.11.6) (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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.4...@ohif/viewer@3.11.5) (2020-05-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.3...@ohif/viewer@3.11.4) (2020-05-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.2...@ohif/viewer@3.11.3) (2020-04-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add IHEInvokeImageDisplay routes back into viewer ([#1695](https://github.com/OHIF/Viewers/issues/1695)) ([f7162ce](https://github.com/OHIF/Viewers/commit/f7162ce61708776a6c192732b0904a022bcc6b3a))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.1...@ohif/viewer@3.11.2) (2020-04-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.11.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.0...@ohif/viewer@3.11.1) (2020-04-27)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [3.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.10.2...@ohif/viewer@3.11.0) (2020-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.10.1...@ohif/viewer@3.10.2) (2020-04-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* undefined `errorHandler` in cornerstoneWadoImageLoader configuration ([#1664](https://github.com/OHIF/Viewers/issues/1664)) ([709f147](https://github.com/OHIF/Viewers/commit/709f14708e2b0f912b5ea509114acd87af3149cb))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.10.0...@ohif/viewer@3.10.1) (2020-04-23)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [3.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.9.2...@ohif/viewer@3.10.0) (2020-04-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [3.9.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.9.1...@ohif/viewer@3.9.2) (2020-04-22)
|
## [3.9.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.9.1...@ohif/viewer@3.9.2) (2020-04-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('OHIF Study List', function() {
|
|||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
cy.get('@searchResult').should($list => {
|
cy.get('@searchResult').should($list => {
|
||||||
expect($list.length).to.be.eq(2);
|
expect($list.length).to.be.eq(3);
|
||||||
expect($list).to.contain('Juno');
|
expect($list).to.contain('Juno');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -127,7 +127,7 @@ describe('OHIF Study List', function() {
|
|||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
cy.get('@searchResult').should($list => {
|
cy.get('@searchResult').should($list => {
|
||||||
expect($list.length).to.be.eq(2);
|
expect($list.length).to.be.eq(3);
|
||||||
expect($list).to.contain('Juno');
|
expect($list).to.contain('Juno');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -137,7 +137,7 @@ describe('OHIF Study List', function() {
|
|||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
cy.get('@searchResult').should($list => {
|
cy.get('@searchResult').should($list => {
|
||||||
expect($list.length).to.be.eq(2);
|
expect($list.length).to.be.eq(6);
|
||||||
expect($list).to.contain('ProstateX-0000');
|
expect($list).to.contain('ProstateX-0000');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
describe('OHIF Microscopy Extension', () => {
|
describe('OHIF Microscopy Extension', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.openStudyModality('SM');
|
cy.openStudyModality('SM');
|
||||||
cy.expectMinimumThumbnails(6);
|
cy.expectMinimumThumbnails(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks if series thumbnails are being displayed', () => {
|
it('checks if series thumbnails are being displayed', () => {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ describe('OHIF VTK Extension', () => {
|
|||||||
//Select 2D MPR button
|
//Select 2D MPR button
|
||||||
cy.get('[data-cy="2d mpr"]').click();
|
cy.get('[data-cy="2d mpr"]').click();
|
||||||
|
|
||||||
//Wait Reformatting Images
|
//Wait waitVTKLoading Images
|
||||||
cy.waitVTKReformatting();
|
cy.waitVTKLoading();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
+2
-2
@@ -18,8 +18,8 @@ describe('Visual Regression - OHIF VTK Extension', () => {
|
|||||||
//Select 2D MPR button
|
//Select 2D MPR button
|
||||||
cy.get('[data-cy="2d mpr"]').click();
|
cy.get('[data-cy="2d mpr"]').click();
|
||||||
|
|
||||||
//Wait Reformatting Images
|
//Wait waitVTKLoading Images
|
||||||
cy.waitVTKReformatting();
|
cy.waitVTKwaitVTKLoading();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -107,15 +107,15 @@ Cypress.Commands.add('waitStudyList', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('waitVTKReformatting', () => {
|
Cypress.Commands.add('waitVTKLoading', () => {
|
||||||
// Wait for start reformatting
|
// Wait for start loading
|
||||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => {
|
cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => {
|
||||||
expect($grid).to.contain.text('Reform');
|
expect($grid).to.contain.text('Loading');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for finish reformatting
|
// Wait for finish loading
|
||||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => {
|
cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => {
|
||||||
expect($grid).not.to.contain.text('Reform');
|
expect($grid).not.to.contain.text('Loading');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/viewer",
|
"name": "@ohif/viewer",
|
||||||
"version": "3.9.2",
|
"version": "4.2.2",
|
||||||
"description": "OHIF Viewer",
|
"description": "OHIF Viewer",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -48,26 +48,26 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@ohif/core": "^2.7.1",
|
"@ohif/core": "^2.9.7",
|
||||||
"@ohif/extension-cornerstone": "^2.5.2",
|
"@ohif/extension-cornerstone": "^2.8.1",
|
||||||
"@ohif/extension-dicom-html": "^1.1.2",
|
"@ohif/extension-dicom-html": "^1.2.2",
|
||||||
"@ohif/extension-dicom-microscopy": "^0.50.9",
|
"@ohif/extension-dicom-microscopy": "^0.51.0",
|
||||||
"@ohif/extension-dicom-p10-downloader": "^0.0.2",
|
"@ohif/extension-dicom-p10-downloader": "^0.1.1",
|
||||||
"@ohif/extension-dicom-pdf": "^1.0.3",
|
"@ohif/extension-dicom-pdf": "^1.0.3",
|
||||||
"@ohif/extension-dicom-rt": "^0.0.2",
|
"@ohif/extension-dicom-rt": "^0.2.0",
|
||||||
"@ohif/extension-dicom-segmentation": "^0.1.2",
|
"@ohif/extension-dicom-segmentation": "^0.3.0",
|
||||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||||
"@ohif/extension-vtk": "^1.3.14",
|
"@ohif/extension-vtk": "^1.6.1",
|
||||||
"@ohif/i18n": "^0.52.8",
|
"@ohif/i18n": "^0.52.8",
|
||||||
"@ohif/ui": "^1.4.3",
|
"@ohif/ui": "^1.5.0",
|
||||||
"@tanem/react-nprogress": "^1.1.25",
|
"@tanem/react-nprogress": "^1.1.25",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.15.1",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.1.0",
|
||||||
"dcmjs": "^0.12.0",
|
"dcmjs": "^0.12.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"dicomweb-client": "^0.4.4",
|
"dicomweb-client": "^0.4.4",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ window.config = {
|
|||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
enableStudyLazyLoad: true,
|
enableStudyLazyLoad: true,
|
||||||
|
supportsFuzzyMatching: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
window.config = function(props) {
|
||||||
|
var servicesManager = props.servicesManager;
|
||||||
|
|
||||||
|
return {
|
||||||
|
routerBasename: '/',
|
||||||
|
enableGoogleCloudAdapter: true,
|
||||||
|
enableGoogleCloudAdapterUI: false,
|
||||||
|
showStudyList: true,
|
||||||
|
httpErrorHandler: error => {
|
||||||
|
// This is 429 when rejected from the public idc sandbox too often.
|
||||||
|
console.warn(error.status);
|
||||||
|
|
||||||
|
// Could use services manager here to bring up a dialog/modal if needed.
|
||||||
|
console.warn('test, navigate to https://ohif.org/');
|
||||||
|
window.location = 'https://ohif.org/';
|
||||||
|
},
|
||||||
|
healthcareApiEndpoint: 'https://idc-sandbox-002.appspot.com/v1beta1',
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ window.config = {
|
|||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
enableStudyLazyLoad: true,
|
enableStudyLazyLoad: true,
|
||||||
|
supportsFuzzyMatching: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ window.config = {
|
|||||||
qidoSupportsIncludeField: true,
|
qidoSupportsIncludeField: true,
|
||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
|
supportsFuzzyMatching: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+63
-41
@@ -13,6 +13,7 @@ import {
|
|||||||
ModalProvider,
|
ModalProvider,
|
||||||
DialogProvider,
|
DialogProvider,
|
||||||
OHIFModal,
|
OHIFModal,
|
||||||
|
ErrorBoundary
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +32,8 @@ import {
|
|||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
|
|
||||||
// TODO: This should not be here
|
// TODO: This should not be here
|
||||||
import './config';
|
//import './config';
|
||||||
|
import { setConfiguration } from './config';
|
||||||
|
|
||||||
/** Utils */
|
/** Utils */
|
||||||
import {
|
import {
|
||||||
@@ -52,7 +54,7 @@ import store from './store';
|
|||||||
/** Contexts */
|
/** Contexts */
|
||||||
import WhiteLabelingContext from './context/WhiteLabelingContext';
|
import WhiteLabelingContext from './context/WhiteLabelingContext';
|
||||||
import UserManagerContext from './context/UserManagerContext';
|
import UserManagerContext from './context/UserManagerContext';
|
||||||
import AppContext from './context/AppContext';
|
import { AppProvider, useAppContext, CONTEXTS } from './context/AppContext';
|
||||||
|
|
||||||
/** ~~~~~~~~~~~~~ Application Setup */
|
/** ~~~~~~~~~~~~~ Application Setup */
|
||||||
const commandsManagerConfig = {
|
const commandsManagerConfig = {
|
||||||
@@ -70,6 +72,14 @@ let extensionManager;
|
|||||||
// TODO[react] Use a provider when the whole tree is React
|
// TODO[react] Use a provider when the whole tree is React
|
||||||
window.store = store;
|
window.store = store;
|
||||||
|
|
||||||
|
window.ohif = window.ohif || {};
|
||||||
|
window.ohif.app = {
|
||||||
|
commandsManager,
|
||||||
|
hotkeysManager,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
};
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
config: PropTypes.oneOfType([
|
config: PropTypes.oneOfType([
|
||||||
@@ -123,6 +133,8 @@ class App extends Component {
|
|||||||
oidc,
|
oidc,
|
||||||
} = this._appConfig;
|
} = this._appConfig;
|
||||||
|
|
||||||
|
setConfiguration(this._appConfig);
|
||||||
|
|
||||||
this.initUserManager(oidc);
|
this.initUserManager(oidc);
|
||||||
_initServices([
|
_initServices([
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
@@ -156,53 +168,57 @@ class App extends Component {
|
|||||||
|
|
||||||
if (this._userManager) {
|
if (this._userManager) {
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ appConfig: this._appConfig }}>
|
<ErrorBoundary context='App'>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<AppProvider config={this._appConfig}>
|
||||||
<OidcProvider store={store} userManager={this._userManager}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<UserManagerContext.Provider value={this._userManager}>
|
<OidcProvider store={store} userManager={this._userManager}>
|
||||||
<Router basename={routerBasename}>
|
<UserManagerContext.Provider value={this._userManager}>
|
||||||
<WhiteLabelingContext.Provider value={whiteLabeling}>
|
<Router basename={routerBasename}>
|
||||||
<SnackbarProvider service={UINotificationService}>
|
<WhiteLabelingContext.Provider value={whiteLabeling}>
|
||||||
<DialogProvider service={UIDialogService}>
|
<SnackbarProvider service={UINotificationService}>
|
||||||
<ModalProvider
|
<DialogProvider service={UIDialogService}>
|
||||||
modal={OHIFModal}
|
<ModalProvider
|
||||||
service={UIModalService}
|
modal={OHIFModal}
|
||||||
>
|
service={UIModalService}
|
||||||
<OHIFStandaloneViewer
|
>
|
||||||
userManager={this._userManager}
|
<OHIFStandaloneViewer
|
||||||
/>
|
userManager={this._userManager}
|
||||||
</ModalProvider>
|
/>
|
||||||
</DialogProvider>
|
</ModalProvider>
|
||||||
</SnackbarProvider>
|
</DialogProvider>
|
||||||
</WhiteLabelingContext.Provider>
|
</SnackbarProvider>
|
||||||
</Router>
|
</WhiteLabelingContext.Provider>
|
||||||
</UserManagerContext.Provider>
|
</Router>
|
||||||
</OidcProvider>
|
</UserManagerContext.Provider>
|
||||||
</I18nextProvider>
|
</OidcProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
</AppProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</AppContext.Provider>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ appConfig: this._appConfig }}>
|
<ErrorBoundary context='App'>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<AppProvider config={this._appConfig}>
|
||||||
<Router basename={routerBasename}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<WhiteLabelingContext.Provider value={whiteLabeling}>
|
<Router basename={routerBasename}>
|
||||||
<SnackbarProvider service={UINotificationService}>
|
<WhiteLabelingContext.Provider value={whiteLabeling}>
|
||||||
<DialogProvider service={UIDialogService}>
|
<SnackbarProvider service={UINotificationService}>
|
||||||
<ModalProvider modal={OHIFModal} service={UIModalService}>
|
<DialogProvider service={UIDialogService}>
|
||||||
<OHIFStandaloneViewer />
|
<ModalProvider modal={OHIFModal} service={UIModalService}>
|
||||||
</ModalProvider>
|
<OHIFStandaloneViewer />
|
||||||
</DialogProvider>
|
</ModalProvider>
|
||||||
</SnackbarProvider>
|
</DialogProvider>
|
||||||
</WhiteLabelingContext.Provider>
|
</SnackbarProvider>
|
||||||
</Router>
|
</WhiteLabelingContext.Provider>
|
||||||
</I18nextProvider>
|
</Router>
|
||||||
|
</I18nextProvider>
|
||||||
|
</AppProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</AppContext.Provider>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +268,12 @@ function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) {
|
|||||||
commandsManager,
|
commandsManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
appConfig,
|
appConfig,
|
||||||
|
api: {
|
||||||
|
contexts: CONTEXTS,
|
||||||
|
hooks: {
|
||||||
|
useAppContext
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const requiredExtensions = [
|
const requiredExtensions = [
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Route, Switch } from 'react-router-dom';
|
|||||||
import { NProgress } from '@tanem/react-nprogress';
|
import { NProgress } from '@tanem/react-nprogress';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { ViewerbaseDragDropContext } from '@ohif/ui';
|
import { ViewerbaseDragDropContext, ErrorBoundary } from '@ohif/ui';
|
||||||
import { SignoutCallbackComponent } from 'redux-oidc';
|
import { SignoutCallbackComponent } from 'redux-oidc';
|
||||||
import asyncComponent from './components/AsyncComponent.js';
|
import asyncComponent from './components/AsyncComponent.js';
|
||||||
import * as RoutesUtil from './routes/routesUtil';
|
import * as RoutesUtil from './routes/routesUtil';
|
||||||
@@ -191,8 +191,10 @@ class OHIFStandaloneViewer extends Component {
|
|||||||
{match === null ? (
|
{match === null ? (
|
||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
<Component match={match} location={this.props.location} />
|
<ErrorBoundary context={match.url}>
|
||||||
)}
|
<Component match={match} location={this.props.location} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)}
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
)}
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { ErrorBoundary } from '@ohif/ui';
|
||||||
|
import { servicesManager } from './../../App';
|
||||||
|
|
||||||
|
const { UIModalService } = servicesManager.services;
|
||||||
|
|
||||||
|
const ErrorBoundaryDialog = ({ context, children }) => {
|
||||||
|
const handleOnError = (error, componentStack) => {
|
||||||
|
const ErrorDialog = () => (
|
||||||
|
<div className="ErrorFallback" role="alert">
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
{context}: <span>{error.message}</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<pre>{componentStack}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
UIModalService.show({
|
||||||
|
content: ErrorDialog,
|
||||||
|
title: `${context}: ${error.message}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackComponent = () => (
|
||||||
|
<div className="ErrorFallback" role="alert">
|
||||||
|
<p>
|
||||||
|
Error rendering {context}. <br /> Check the browser console for more
|
||||||
|
details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
fallbackComponent={fallbackComponent}
|
||||||
|
context={context}
|
||||||
|
onError={handleOnError}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ErrorBoundaryDialog.propTypes = {
|
||||||
|
context: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorBoundaryDialog;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import ErrorBoundaryDialog from './ErrorBoundaryDialog';
|
||||||
|
|
||||||
|
export default ErrorBoundaryDialog;
|
||||||
@@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
|
import { useSnackbarContext } from '@ohif/ui';
|
||||||
//
|
//
|
||||||
import ViewportPane from './ViewportPane.js';
|
import ViewportPane from './ViewportPane.js';
|
||||||
import DefaultViewport from './DefaultViewport.js';
|
import DefaultViewport from './DefaultViewport.js';
|
||||||
@@ -11,7 +12,7 @@ import EmptyViewport from './EmptyViewport.js';
|
|||||||
|
|
||||||
const { loadAndCacheDerivedDisplaySets } = utils;
|
const { loadAndCacheDerivedDisplaySets } = utils;
|
||||||
|
|
||||||
const ViewportGrid = function (props) {
|
const ViewportGrid = function(props) {
|
||||||
const {
|
const {
|
||||||
activeViewportIndex,
|
activeViewportIndex,
|
||||||
availablePlugins,
|
availablePlugins,
|
||||||
@@ -23,6 +24,7 @@ const ViewportGrid = function (props) {
|
|||||||
studies,
|
studies,
|
||||||
viewportData,
|
viewportData,
|
||||||
children,
|
children,
|
||||||
|
isStudyLoaded,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const rowSize = 100 / numRows;
|
const rowSize = 100 / numRows;
|
||||||
@@ -33,11 +35,26 @@ const ViewportGrid = function (props) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const snackbar = useSnackbarContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
viewportData.forEach(displaySet => {
|
if (isStudyLoaded) {
|
||||||
loadAndCacheDerivedDisplaySets(displaySet, studies);
|
viewportData.forEach(displaySet => {
|
||||||
});
|
const promises = loadAndCacheDerivedDisplaySets(displaySet, studies);
|
||||||
}, [studies, viewportData]);
|
|
||||||
|
promises.forEach(promise => {
|
||||||
|
promise.catch(error => {
|
||||||
|
snackbar.show({
|
||||||
|
title: 'Error loading derived display set:',
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
autoClose: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [studies, viewportData, isStudyLoaded, snackbar]);
|
||||||
|
|
||||||
const getViewportPanes = () =>
|
const getViewportPanes = () =>
|
||||||
layout.viewports.map((layout, viewportIndex) => {
|
layout.viewports.map((layout, viewportIndex) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import './ViewportPane.css';
|
import './ViewportPane.css';
|
||||||
|
|
||||||
const ViewportPane = function(props) {
|
const ViewportPane = function (props) {
|
||||||
const { children, onDrop, viewportIndex, className: propClassName } = props;
|
const { children, onDrop, viewportIndex, className: propClassName } = props;
|
||||||
const [{ hovered, highlighted }, drop] = useDrop({
|
const [{ hovered, highlighted }, drop] = useDrop({
|
||||||
accept: 'thumbnail',
|
accept: 'thumbnail',
|
||||||
|
|||||||
@@ -3,43 +3,59 @@ import cornerstone from 'cornerstone-core';
|
|||||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import dicomParser from 'dicom-parser';
|
import dicomParser from 'dicom-parser';
|
||||||
import version from './version.js';
|
import version from './version.js';
|
||||||
|
import AppContext from './context/AppContext';
|
||||||
|
|
||||||
let homepage;
|
export function setConfiguration(appConfig) {
|
||||||
const { process } = window;
|
let homepage;
|
||||||
if (process && process.env && process.env.PUBLIC_URL) {
|
const { process } = window;
|
||||||
homepage = process.env.PUBLIC_URL;
|
if (process && process.env && process.env.PUBLIC_URL) {
|
||||||
}
|
homepage = process.env.PUBLIC_URL;
|
||||||
|
|
||||||
window.info = {
|
|
||||||
version,
|
|
||||||
homepage,
|
|
||||||
};
|
|
||||||
|
|
||||||
// For debugging
|
|
||||||
//if (process.env.node_env === 'development') {
|
|
||||||
window.cornerstone = cornerstone;
|
|
||||||
window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader;
|
|
||||||
//}
|
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
|
||||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
|
||||||
|
|
||||||
OHIF.user.getAccessToken = () => {
|
|
||||||
// TODO: Get the Redux store from somewhere else
|
|
||||||
const state = window.store.getState();
|
|
||||||
if (!state.oidc || !state.oidc.user) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.oidc.user.access_token;
|
window.info = {
|
||||||
};
|
version,
|
||||||
|
homepage,
|
||||||
|
};
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.configure({
|
// For debugging
|
||||||
beforeSend: function(xhr) {
|
//if (process.env.node_env === 'development') {
|
||||||
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
|
window.cornerstone = cornerstone;
|
||||||
|
window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader;
|
||||||
|
//}
|
||||||
|
|
||||||
if (headers.Authorization) {
|
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||||
xhr.setRequestHeader('Authorization', headers.Authorization);
|
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||||
|
|
||||||
|
OHIF.user.getAccessToken = () => {
|
||||||
|
// TODO: Get the Redux store from somewhere else
|
||||||
|
const state = window.store.getState();
|
||||||
|
if (!state.oidc || !state.oidc.user) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
});
|
return state.oidc.user.access_token;
|
||||||
|
};
|
||||||
|
|
||||||
|
OHIF.errorHandler.getHTTPErrorHandler = () => {
|
||||||
|
// const { appConfig = {} } = AppContext;
|
||||||
|
|
||||||
|
return appConfig.httpErrorHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
cornerstoneWADOImageLoader.configure({
|
||||||
|
beforeSend: function(xhr) {
|
||||||
|
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
|
||||||
|
|
||||||
|
if (headers.Authorization) {
|
||||||
|
xhr.setRequestHeader('Authorization', headers.Authorization);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorInterceptor: error => {
|
||||||
|
// const { appConfig = {} } = AppContext;
|
||||||
|
|
||||||
|
if (typeof appConfig.httpErrorHandler === 'function') {
|
||||||
|
appConfig.httpErrorHandler(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// TODO: REPLACE THIS WITH A CONTEXT PROVIDER
|
|
||||||
// EVERYTHING IN `VIEWER.JS` COULD USE THIS FOR APPROPRIATE CONTEXT
|
|
||||||
import ToolbarRow from './ToolbarRow';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { getActiveContexts } from './../store/layout/selectors.js';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
activeContexts: getActiveContexts(state),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedToolbarRow = connect(mapStateToProps)(ToolbarRow);
|
|
||||||
|
|
||||||
export default ConnectedToolbarRow;
|
|
||||||
@@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
|||||||
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
|
|
||||||
const { clearViewportSpecificData } = OHIF.redux.actions;
|
const { clearViewportSpecificData, setStudyData } = OHIF.redux.actions;
|
||||||
const isActive = a => a.active === true;
|
const isActive = a => a.active === true;
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
@@ -14,6 +14,9 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
};
|
};
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
|
setStudyData: (StudyInstanceUID, data) => {
|
||||||
|
dispatch(setStudyData(StudyInstanceUID, data));
|
||||||
|
},
|
||||||
clearViewportSpecificData: () => {
|
clearViewportSpecificData: () => {
|
||||||
dispatch(clearViewportSpecificData());
|
dispatch(clearViewportSpecificData());
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { commandsManager, extensionManager } from './../App.js';
|
|||||||
|
|
||||||
import ConnectedCineDialog from './ConnectedCineDialog';
|
import ConnectedCineDialog from './ConnectedCineDialog';
|
||||||
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
||||||
|
import { withAppContext } from '../context/AppContext';
|
||||||
|
|
||||||
class ToolbarRow extends Component {
|
class ToolbarRow extends Component {
|
||||||
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
||||||
@@ -381,5 +382,5 @@ function _handleBuiltIn(button) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation(['Common', 'ViewportDownloadForm'])(
|
export default withTranslation(['Common', 'ViewportDownloadForm'])(
|
||||||
withModal(withDialog(ToolbarRow))
|
withModal(withDialog(withAppContext(ToolbarRow)))
|
||||||
);
|
);
|
||||||
@@ -2,15 +2,15 @@ import React, { Component } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { MODULE_TYPES } from '@ohif/core';
|
import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core';
|
||||||
import OHIF, { DICOMSR } from '@ohif/core';
|
|
||||||
import { withDialog } from '@ohif/ui';
|
import { withDialog } from '@ohif/ui';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import ConnectedHeader from './ConnectedHeader.js';
|
import ConnectedHeader from './ConnectedHeader.js';
|
||||||
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
import ToolbarRow from './ToolbarRow.js';
|
||||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||||
import SidePanel from './../components/SidePanel.js';
|
import SidePanel from './../components/SidePanel.js';
|
||||||
|
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
|
||||||
import { extensionManager } from './../App.js';
|
import { extensionManager } from './../App.js';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
@@ -26,6 +26,7 @@ class Viewer extends Component {
|
|||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
StudyInstanceUID: PropTypes.string.isRequired,
|
StudyInstanceUID: PropTypes.string.isRequired,
|
||||||
StudyDate: PropTypes.string,
|
StudyDate: PropTypes.string,
|
||||||
|
PatientID: PropTypes.string,
|
||||||
displaySets: PropTypes.arrayOf(
|
displaySets: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||||
@@ -256,42 +257,44 @@ class Viewer extends Component {
|
|||||||
</WhiteLabelingContext.Consumer>
|
</WhiteLabelingContext.Consumer>
|
||||||
|
|
||||||
{/* TOOLBAR */}
|
{/* TOOLBAR */}
|
||||||
<ConnectedToolbarRow
|
<ErrorBoundaryDialog context="ToolbarRow">
|
||||||
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
<ToolbarRow
|
||||||
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
||||||
selectedLeftSidePanel={
|
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
||||||
this.state.isLeftSidePanelOpen
|
selectedLeftSidePanel={
|
||||||
? this.state.selectedLeftSidePanel
|
this.state.isLeftSidePanelOpen
|
||||||
: ''
|
? this.state.selectedLeftSidePanel
|
||||||
}
|
: ''
|
||||||
selectedRightSidePanel={
|
|
||||||
this.state.isRightSidePanelOpen
|
|
||||||
? this.state.selectedRightSidePanel
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
handleSidePanelChange={(side, selectedPanel) => {
|
|
||||||
const sideClicked = side && side[0].toUpperCase() + side.slice(1);
|
|
||||||
const openKey = `is${sideClicked}SidePanelOpen`;
|
|
||||||
const selectedKey = `selected${sideClicked}SidePanel`;
|
|
||||||
const updatedState = Object.assign({}, this.state);
|
|
||||||
|
|
||||||
const isOpen = updatedState[openKey];
|
|
||||||
const prevSelectedPanel = updatedState[selectedKey];
|
|
||||||
// RoundedButtonGroup returns `null` if selected button is clicked
|
|
||||||
const isSameSelectedPanel =
|
|
||||||
prevSelectedPanel === selectedPanel || selectedPanel === null;
|
|
||||||
|
|
||||||
updatedState[selectedKey] = selectedPanel || prevSelectedPanel;
|
|
||||||
|
|
||||||
const isClosedOrShouldClose = !isOpen || isSameSelectedPanel;
|
|
||||||
if (isClosedOrShouldClose) {
|
|
||||||
updatedState[openKey] = !updatedState[openKey];
|
|
||||||
}
|
}
|
||||||
|
selectedRightSidePanel={
|
||||||
|
this.state.isRightSidePanelOpen
|
||||||
|
? this.state.selectedRightSidePanel
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
handleSidePanelChange={(side, selectedPanel) => {
|
||||||
|
const sideClicked = side && side[0].toUpperCase() + side.slice(1);
|
||||||
|
const openKey = `is${sideClicked}SidePanelOpen`;
|
||||||
|
const selectedKey = `selected${sideClicked}SidePanel`;
|
||||||
|
const updatedState = Object.assign({}, this.state);
|
||||||
|
|
||||||
this.setState(updatedState);
|
const isOpen = updatedState[openKey];
|
||||||
}}
|
const prevSelectedPanel = updatedState[selectedKey];
|
||||||
studies={this.props.studies}
|
// RoundedButtonGroup returns `null` if selected button is clicked
|
||||||
/>
|
const isSameSelectedPanel =
|
||||||
|
prevSelectedPanel === selectedPanel || selectedPanel === null;
|
||||||
|
|
||||||
|
updatedState[selectedKey] = selectedPanel || prevSelectedPanel;
|
||||||
|
|
||||||
|
const isClosedOrShouldClose = !isOpen || isSameSelectedPanel;
|
||||||
|
if (isClosedOrShouldClose) {
|
||||||
|
updatedState[openKey] = !updatedState[openKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(updatedState);
|
||||||
|
}}
|
||||||
|
studies={this.props.studies}
|
||||||
|
/>
|
||||||
|
</ErrorBoundaryDialog>
|
||||||
|
|
||||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
||||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
||||||
@@ -299,37 +302,46 @@ class Viewer extends Component {
|
|||||||
{/* VIEWPORTS + SIDEPANELS */}
|
{/* VIEWPORTS + SIDEPANELS */}
|
||||||
<div className="FlexboxLayout">
|
<div className="FlexboxLayout">
|
||||||
{/* LEFT */}
|
{/* LEFT */}
|
||||||
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
|
<ErrorBoundaryDialog context="LeftSidePanel">
|
||||||
{VisiblePanelLeft ? (
|
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
|
||||||
<VisiblePanelLeft
|
{VisiblePanelLeft ? (
|
||||||
viewports={this.props.viewports}
|
<VisiblePanelLeft
|
||||||
studies={this.props.studies}
|
viewports={this.props.viewports}
|
||||||
activeIndex={this.props.activeViewportIndex}
|
studies={this.props.studies}
|
||||||
/>
|
activeIndex={this.props.activeViewportIndex}
|
||||||
) : (
|
/>
|
||||||
<ConnectedStudyBrowser
|
) : (
|
||||||
studies={this.state.thumbnails}
|
<ConnectedStudyBrowser
|
||||||
studyMetadata={this.props.studies}
|
studies={this.state.thumbnails}
|
||||||
/>
|
studyMetadata={this.props.studies}
|
||||||
)}
|
/>
|
||||||
</SidePanel>
|
)}
|
||||||
|
</SidePanel>
|
||||||
|
</ErrorBoundaryDialog>
|
||||||
|
|
||||||
{/* MAIN */}
|
{/* MAIN */}
|
||||||
<div className={classNames('main-content')}>
|
<div className={classNames('main-content')}>
|
||||||
<ConnectedViewerMain studies={this.props.studies} />
|
<ErrorBoundaryDialog context="ViewerMain">
|
||||||
|
<ConnectedViewerMain
|
||||||
|
studies={this.props.studies}
|
||||||
|
isStudyLoaded={this.props.isStudyLoaded}
|
||||||
|
/>
|
||||||
|
</ErrorBoundaryDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT */}
|
{/* RIGHT */}
|
||||||
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
|
<ErrorBoundaryDialog context="RightSidePanel">
|
||||||
{VisiblePanelRight && (
|
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
|
||||||
<VisiblePanelRight
|
{VisiblePanelRight && (
|
||||||
isOpen={this.state.isRightSidePanelOpen}
|
<VisiblePanelRight
|
||||||
viewports={this.props.viewports}
|
isOpen={this.state.isRightSidePanelOpen}
|
||||||
studies={this.props.studies}
|
viewports={this.props.viewports}
|
||||||
activeIndex={this.props.activeViewportIndex}
|
studies={this.props.studies}
|
||||||
/>
|
activeIndex={this.props.activeViewportIndex}
|
||||||
)}
|
/>
|
||||||
</SidePanel>
|
)}
|
||||||
|
</SidePanel>
|
||||||
|
</ErrorBoundaryDialog>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ class ViewerMain extends Component {
|
|||||||
<div className="ViewerMain">
|
<div className="ViewerMain">
|
||||||
{this.state.displaySets.length && (
|
{this.state.displaySets.length && (
|
||||||
<ConnectedViewportGrid
|
<ConnectedViewportGrid
|
||||||
|
isStudyLoaded={this.props.isStudyLoaded}
|
||||||
studies={this.props.studies}
|
studies={this.props.studies}
|
||||||
viewportData={viewportData}
|
viewportData={viewportData}
|
||||||
setViewportData={this.setViewportData}
|
setViewportData={this.setViewportData}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const _showUserMessage = (queryParamApplied, message, dialog = {}) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { show: showUserMessage = () => {} } = dialog;
|
const { show: showUserMessage = () => { } } = dialog;
|
||||||
showUserMessage({
|
showUserMessage({
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
@@ -154,11 +154,21 @@ const _sortStudyDisplaySet = (study, studyMetadata) => {
|
|||||||
studyMetadata.sortDisplaySets(study.displaySets);
|
studyMetadata.sortDisplaySets(study.displaySets);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _thinStudyData = study => {
|
||||||
|
return {
|
||||||
|
StudyInstanceUID: study.StudyInstanceUID,
|
||||||
|
series: study.series.map(item => ({
|
||||||
|
SeriesInstanceUID: item.SeriesInstanceUID
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function ViewerRetrieveStudyData({
|
function ViewerRetrieveStudyData({
|
||||||
server,
|
server,
|
||||||
studyInstanceUIDs,
|
studyInstanceUIDs,
|
||||||
seriesInstanceUIDs,
|
seriesInstanceUIDs,
|
||||||
clearViewportSpecificData,
|
clearViewportSpecificData,
|
||||||
|
setStudyData,
|
||||||
}) {
|
}) {
|
||||||
// hooks
|
// hooks
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
@@ -220,6 +230,7 @@ function ViewerRetrieveStudyData({
|
|||||||
if (Array.isArray(studiesData) && studiesData.length > 0) {
|
if (Array.isArray(studiesData) && studiesData.length > 0) {
|
||||||
// Map studies to new format, update metadata manager?
|
// Map studies to new format, update metadata manager?
|
||||||
const studies = studiesData.map(study => {
|
const studies = studiesData.map(study => {
|
||||||
|
setStudyData(study.StudyInstanceUID, _thinStudyData(study));
|
||||||
const studyMetadata = new OHIFStudyMetadata(
|
const studyMetadata = new OHIFStudyMetadata(
|
||||||
study,
|
study,
|
||||||
study.StudyInstanceUID
|
study.StudyInstanceUID
|
||||||
@@ -269,7 +280,8 @@ function ViewerRetrieveStudyData({
|
|||||||
const promises = Array(concurrentRequestsAllowed)
|
const promises = Array(concurrentRequestsAllowed)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map(loadNextSeries);
|
.map(loadNextSeries);
|
||||||
await Promise.all(promises);
|
|
||||||
|
return await Promise.all(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadStudies = async () => {
|
const loadStudies = async () => {
|
||||||
@@ -368,6 +380,7 @@ ViewerRetrieveStudyData.propTypes = {
|
|||||||
seriesInstanceUIDs: PropTypes.array,
|
seriesInstanceUIDs: PropTypes.array,
|
||||||
server: PropTypes.object,
|
server: PropTypes.object,
|
||||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
clearViewportSpecificData: PropTypes.func.isRequired,
|
||||||
|
setStudyData: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ViewerRetrieveStudyData;
|
export default ViewerRetrieveStudyData;
|
||||||
@@ -1,5 +1,33 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { getActiveContexts } from '../store/layout/selectors.js';
|
||||||
|
|
||||||
let AppContext = React.createContext({});
|
let AppContext = React.createContext({});
|
||||||
|
|
||||||
|
export const CONTEXTS = {
|
||||||
|
CORNERSTONE: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||||
|
VTK: 'ACTIVE_VIEWPORT::VTK'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppContext = () => useContext(AppContext);
|
||||||
|
|
||||||
|
export const AppProvider = ({ children, config }) => {
|
||||||
|
const activeContexts = useSelector(state => getActiveContexts(state));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={{ appConfig: config, activeContexts }}>
|
||||||
|
{children}
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withAppContext = Component => {
|
||||||
|
return function WrappedComponent(props) {
|
||||||
|
const { appConfig, activeContexts } = useAppContext();
|
||||||
|
return (
|
||||||
|
<Component {...props} appConfig={appConfig} activeContexts={activeContexts} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AppContext;
|
export default AppContext;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { httpErrorToStr, checkDicomFile } from '../utils/helpers';
|
import { httpErrorToStr, checkDicomFile } from '../utils/helpers';
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
|
import { errorHandler } from '@ohif/core';
|
||||||
|
|
||||||
class DicomUploadService {
|
class DicomUploadService {
|
||||||
async smartUpload(files, url, uploadCallback, cancellationToken) {
|
async smartUpload(files, url, uploadCallback, cancellationToken) {
|
||||||
@@ -22,8 +23,7 @@ class DicomUploadService {
|
|||||||
let error = null;
|
let error = null;
|
||||||
try {
|
try {
|
||||||
if (chunk.length > 1) throw new Error('Not implemented');
|
if (chunk.length > 1) throw new Error('Not implemented');
|
||||||
if (chunk.length === 1)
|
if (chunk.length === 1) await this.simpleUpload(chunk[0], url);
|
||||||
await this.simpleUpload(chunk[0], url);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// It looks like a stupid bug of Babel that err is not an actual Exception object
|
// It looks like a stupid bug of Babel that err is not an actual Exception object
|
||||||
error = httpErrorToStr(err);
|
error = httpErrorToStr(err);
|
||||||
@@ -76,11 +76,12 @@ class DicomUploadService {
|
|||||||
|
|
||||||
getClient(url) {
|
getClient(url) {
|
||||||
const headers = this.retrieveAuthHeaderFunc();
|
const headers = this.retrieveAuthHeaderFunc();
|
||||||
|
const errorInterceptor = errorHandler.getHTTPErrorHandler();
|
||||||
|
|
||||||
// TODO: a bit weird we are creating a new dicomweb client instance for every upload
|
// TODO: a bit weird we are creating a new dicomweb client instance for every upload
|
||||||
return new api.DICOMwebClient({
|
return new api.DICOMwebClient({
|
||||||
url,
|
url,
|
||||||
headers
|
headers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user