Compare commits

..
Author SHA1 Message Date
ohif-bot 383a0ef167 chore(release): publish [skip ci]
- @ohif/extension-debugging@0.3.2
 - @ohif/viewer@4.12.19
2022-03-10 13:26:11 +00:00
Davide Punzo 56bd49a0cd Re IDC #2698 : fix overflow text and adding referenced Segs and RTSTRUCT series info in the debug dialog (#2735) 2022-03-10 14:23:01 +01:00
ohif-bot 755b283574 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.12.16
 - @ohif/core@2.16.12
 - @ohif/viewer@4.12.18
2022-02-15 10:41:51 +00:00
Igor Octaviano a8a4fcee40 Merge pull request #2708 from OHIF/igoroctaviano/bump-libs
refactor: 💡 Bump lodash and immer to avoid vulnerabilities
2022-02-15 07:38:15 -03:00
igoroctaviano 9892320ffb refactor: 💡 Bump lodash and immer to avoid vulnerabilities
Bump lodash and immer to avoid vulnerabilities
2022-02-14 18:50:14 -03:00
ohif-bot 28befb1ea6 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.12.7
 - @ohif/extension-dicom-html@1.3.18
 - @ohif/extension-dicom-rt@0.7.8
 - @ohif/extension-dicom-segmentation@0.7.8
 - @ohif/extension-dicom-tag-browser@0.2.19
 - @ohif/extension-vtk@1.12.15
 - @ohif/core@2.16.11
 - @ohif/viewer@4.12.17
2022-02-03 17:12:08 +00:00
Davide Punzo c554c5fac2 IDC Re #2656: update dcmjs 0.19.4 and change tol to 0.01. + fix boundary cases in parseSCOORD3D (#2699)
* IDC: update dcmjs to version 0.19.4

* IDC Re #2656: change tol to 0.01 for segs in dcmjs. + fix boundary cases in parseSCOORD3D
2022-02-03 18:08:05 +01:00
ohif-bot 6e3a58931a chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.12.6
 - @ohif/extension-dicom-html@1.3.17
 - @ohif/extension-vtk@1.12.14
 - @ohif/core@2.16.10
 - @ohif/ui@1.10.6
 - @ohif/viewer@4.12.16
2022-02-03 08:37:00 +00:00
Davide Punzoandigoroctaviano 5beb12a7fc IDC (Re #1215): Feat/v2 annotations (#2658)
* Add scoord3d parse / display tool to v2

* Update measurement panel with sr measurements

* Update measurement panel to display read only items

* Cleanup debugger statement

* Check for empty measurement

* fix SCOORD3D coordinates

* Update getRenderableData.js

* fix scoord3d coordinate transformation

* implement logic to detect the ReferencedSOPInstanceUID if missing in the measurement. Fix also world to IJ coordinates.

* Fix broken unit test / import

* fix comment style

* Init tools only once

* restore default config dcmjs server

Co-authored-by: igoroctaviano <igoroctaviano@gmail.com>
2022-02-03 09:33:02 +01:00
ohif-bot 1953118c8a chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.12.5
 - @ohif/extension-dicom-html@1.3.16
 - @ohif/extension-dicom-rt@0.7.7
 - @ohif/extension-dicom-segmentation@0.7.7
 - @ohif/extension-dicom-tag-browser@0.2.18
 - @ohif/extension-vtk@1.12.13
 - @ohif/core@2.16.9
 - @ohif/viewer@4.12.15
2022-02-02 15:09:29 +00:00
Davide Punzo 7807f849f2 IDC: update dcmjs to version 0.19.3 (#2697) 2022-02-02 16:06:14 +01:00
ohif-bot b426256df5 chore(release): publish [skip ci]
- @ohif/viewer@4.12.14
2022-01-31 15:05:24 +00:00
Josiah VinsonandJosiah Vinson c9b7e97f9c Fixing broken OpenResty / Orthanc recipes and updating doc (#2652)
* Updating docker files and documentation for OpenResty / Orthanc recipes.

* fixing link formatting

Co-authored-by: Josiah Vinson <jovinson@microsoft.com>
2022-01-31 10:02:18 -05:00
ohif-bot 0fc7d93d73 chore(release): publish [skip ci]
- @ohif/extension-dicom-pdf@1.1.2
 - @ohif/extension-vtk@1.12.12
 - @ohif/core@2.16.8
 - @ohif/viewer@4.12.13
2022-01-29 01:06:23 +00:00
Bill Wallace 33307d3cd2 Feat/v2 e2e static tests (#2684)
Use the static data for hte e2e tests
2022-01-28 20:02:30 -05:00
ohif-bot c3ce909434 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.12.4
 - @ohif/extension-dicom-html@1.3.15
 - @ohif/extension-dicom-rt@0.7.6
 - @ohif/extension-dicom-segmentation@0.7.6
 - @ohif/extension-dicom-tag-browser@0.2.17
 - @ohif/extension-vtk@1.12.11
 - @ohif/core@2.16.7
 - @ohif/viewer@4.12.12
2022-01-20 09:18:41 +00:00
Davide Punzo ad2c750503 IDC update dcmjs to v0.19.2 (#2672)
* IDC: update dcmjs with SEG loading regression fix
2022-01-20 10:15:19 +01:00
ohif-bot cceb61f123 chore(release): publish [skip ci]
- @ohif/extension-dicom-rt@0.7.5
 - @ohif/viewer@4.12.11
2022-01-19 09:29:16 +00:00
Davide Punzo 13bcba8c39 IDC #2657: fix rt struct panel update (#2666)
* Fix observation in RT panel
2022-01-19 10:25:42 +01:00
ohif-bot d2db026158 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.12.10
 - @ohif/core@2.16.6
 - @ohif/viewer@4.12.10
2022-01-13 13:21:38 +00:00
Ștefan Silviu-AlexandruandStefan Silviu e4b0552db4 feature: configurable max prefetch request count (#2632)
* feature: configurable max prefetch request count

* update name and default value for prefetch number

Co-authored-by: Stefan Silviu <silviu.stefan@atta.systems>
2022-01-13 08:18:29 -05:00
ohif-bot d7ba2fac69 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.12.9
 - @ohif/core@2.16.5
 - @ohif/viewer@4.12.9
2021-12-23 22:25:30 +00:00
Alireza 761d54fbbf fix: DICOMFileLoadingListener typo in method name (#2647) 2021-12-23 17:22:35 -05:00
ohif-bot 79548d2380 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.12.8
 - @ohif/core@2.16.4
 - @ohif/viewer@4.12.8
2021-12-23 20:38:37 +00:00
6a401c9e52 fix: SeriesTime and SeriesDate are only set for the first series and undefined for the remaining ones (#2476)
* set SeriesDate and SeriesTime for all series, instead of only for the first series

* set SeriesDate and SeriesTime only when they are undefined

* fix: equality operator

Co-authored-by: Christian El Salloum <christian.elsalloum@emtensor.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2021-12-23 15:35:37 -05:00
Yo Yehudi 23376d279d chore: Add research notice (#2458) 2021-12-23 12:01:07 -05:00
ohif-bot 4110b0422c chore(release): publish [skip ci]
- @ohif/extension-dicom-tag-browser@0.2.16
2021-12-10 09:38:52 +00:00
Davide Punzo ed501bb462 IDC Re #2633: fix slider min and max (#2634)
* IDC Re #2633: fix slider min and max
2021-12-10 10:33:30 +01:00
Davide Punzo abf050e162 update docker remote version for CI (#2629) 2021-12-02 13:56:59 +01:00
ohif-bot 35ed82e161 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.12.3
 - @ohif/extension-debugging@0.3.1
 - @ohif/extension-dicom-html@1.3.14
 - @ohif/extension-dicom-pdf@1.1.1
 - @ohif/extension-dicom-rt@0.7.4
 - @ohif/extension-dicom-segmentation@0.7.5
 - @ohif/extension-dicom-tag-browser@0.2.15
 - @ohif/extension-vtk@1.12.7
 - @ohif/core@2.16.3
 - @ohif/viewer@4.12.7
2021-12-02 11:57:19 +00:00
Davide Punzo d0ccc22fd9 Update cornerstone deps (#2628)
* update cornerstone deps
2021-12-02 12:53:41 +01:00
ohif-bot 31841c495f chore(release): publish [skip ci]
- @ohif/extension-dicom-tag-browser@0.2.14
 - @ohif/extension-vtk@1.12.6
 - @ohif/core@2.16.2
 - @ohif/ui@1.10.5
 - @ohif/viewer@4.12.6
2021-11-30 14:20:06 +00:00
Davide Punzo 65505d9bce Re IDC #2604: show series (with unsupported modalities) on slides panel and on tag explorer (#2621)
* Re IDC #2604: show series (with unsupported modalities) on slides panel and on tag explorer
2021-11-30 15:15:51 +01:00
ohif-bot 460fdeb534 chore(release): publish [skip ci]
- @ohif/extension-dicom-segmentation@0.7.4
 - @ohif/viewer@4.12.5
2021-10-27 13:10:24 +00:00
Igor Octaviano f4b7091ae4 Merge pull request #2596 from OHIF/fix/ohif-2590
Re #2590: Fix segments visibility handler
2021-10-27 10:06:35 -03:00
Davide Punzo 1e72684455 Re #2590: Fix segments visibility handler 2021-10-26 13:00:01 +02:00
ohif-bot 5b0b6301b9 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.12.2
 - @ohif/extension-dicom-html@1.3.13
 - @ohif/extension-dicom-rt@0.7.3
 - @ohif/extension-dicom-segmentation@0.7.3
 - @ohif/extension-dicom-tag-browser@0.2.13
 - @ohif/extension-vtk@1.12.5
 - @ohif/core@2.16.1
 - @ohif/ui@1.10.4
 - @ohif/viewer@4.12.4
2021-10-26 08:23:03 +00:00
Igor OctavianoandDavide Punzo 03a34a75a1 Add checks for study metadata in rt and seg panels + bump dcmjs to fix circular references issues naturalizing datasets (#2588)
Co-authored-by: Davide Punzo <punzodavide@hotmail.it>
2021-10-26 10:19:12 +02:00
ohif-bot 729272a985 chore(release): publish [skip ci]
- @ohif/extension-dicom-html@1.3.12
 - @ohif/viewer@4.12.3
2021-10-25 15:11:00 +00:00
Bill Wallace 9194ec2404 fix(OHIF):The header depth was too deep for display SR files. (#2594) 2021-10-25 17:08:10 +02:00
ohif-bot 52c95a1209 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.12.4
 - @ohif/ui@1.10.3
 - @ohif/viewer@4.12.2
2021-10-15 11:37:07 +00:00
Davide Punzo 8903adaea7 Re #2571: thumbnail footer remove instance number and add link icon if any derived display set is present (#2582)
* Re #2571, thumbnail footer layout modifications: remove instance number and add link icon if any derived display set is present.
2021-10-15 13:29:36 +02:00
ohif-bot 9481e2a99e chore(release): publish [skip ci]
- @ohif/extension-dicom-rt@0.7.2
 - @ohif/extension-dicom-segmentation@0.7.2
 - @ohif/viewer@4.12.1
2021-10-06 21:01:00 +00:00
Igor Octaviano f783fd7650 Merge pull request #2575 from OHIF/fix/ohif-2570
OHIF-2570: Update badge number based on active ds
2021-10-06 17:53:07 -03:00
Igor Octaviano 79afee7615 Merge branch 'master' into fix/ohif-2570 2021-10-05 09:40:49 -03:00
ohif-bot 539141fe5a chore(release): publish [skip ci]
- @ohif/extension-vtk@1.12.3
 - @ohif/core@2.16.0
 - @ohif/viewer@4.12.0
2021-10-04 07:36:45 +00:00
Bill Wallace a046c6c901 feat: Added support for WADO servers built with static hosting (e.g. S3) (#2566) 2021-10-04 09:30:36 +02:00
igoroctaviano 073be065cf Update badge number based on active ds 2021-10-01 18:09:48 -03:00
ohif-bot 252536a1e6 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.12.1
 - @ohif/extension-dicom-html@1.3.11
 - @ohif/extension-dicom-rt@0.7.1
 - @ohif/extension-dicom-segmentation@0.7.1
 - @ohif/extension-dicom-tag-browser@0.2.12
 - @ohif/extension-vtk@1.12.2
 - @ohif/core@2.15.1
 - @ohif/viewer@4.11.2
2021-09-28 12:14:17 +00:00
Davide Punzo 53b1d71d56 fix regression not showing warnings list on thumbnail for segmentations (#2563)
* fix regression not showing warnings list on thumbnail for segmentations

* update dcmjs version
2021-09-28 14:07:29 +02:00
Erik Ziegler 702d99cb93 Add FUNDING.yml 2021-09-27 14:23:37 +02:00
Alireza bb7b78bf33 chore(github): Create config.yml for issues (#2561)
* chore(github): Create config.yml for issues

* Delete ---support-question.md
2021-09-27 14:20:38 +02:00
ohif-bot b796fc9c62 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.12.1
 - @ohif/ui@1.10.2
 - @ohif/viewer@4.11.1
2021-09-24 11:04:33 +00:00
Erik Ziegler 66c587b139 fix: throttle thumbnail progress updates (#2557) 2021-09-24 12:58:30 +02:00
ohif-bot f6e2e58aad chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.12.0
 - @ohif/extension-debugging@0.3.0
 - @ohif/extension-dicom-pdf@1.1.0
 - @ohif/extension-dicom-rt@0.7.0
 - @ohif/extension-dicom-segmentation@0.7.0
 - @ohif/extension-vtk@1.12.0
 - @ohif/core@2.15.0
 - @ohif/viewer@4.11.0
2021-09-23 16:42:56 +00:00
Erik ZieglerandAlireza 489d4b0584 feat: Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 (#2537)
* feat: bump to prerelease of WADO Image Loader for testing

* switch to released versions

* update react-vtkjs-viewport and fix requestpool usage in StudyPrefetcher

* attempt to fix e2e tests for script-tag usage

* fix e2e test error

* switch to using webpack alias in PWA to use dynamic import version of OHIF

* fix copy plugin usage

* try to fix publicpath issue

* bump to 4.0.3

Co-authored-by: Alireza <ar.sedghi@gmail.com>
2021-09-23 18:37:29 +02:00
ohif-bot 36019156da chore(release): publish [skip ci]
- @ohif/extension-vtk@1.11.18
 - @ohif/core@2.14.1
 - @ohif/ui@1.10.1
 - @ohif/viewer@4.10.1
2021-09-13 13:10:07 +00:00
Igor Octaviano f2a371fc2d Display warning messages if fails to parse sr report (#2543) 2021-09-13 15:06:30 +02:00
ohif-bot ad2f038817 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.11.0
 - @ohif/extension-vtk@1.11.17
 - @ohif/core@2.14.0
 - @ohif/ui@1.10.0
 - @ohif/viewer@4.10.0
2021-09-09 08:02:24 +00:00
Igor OctavianoandAlireza 8f2604509d feat: OHIF-1000: Study prefetcher (#2466)
* Enable study prefetcher and expose configs

* Fix progress for all images

* Enable study loading listener

* Add new default for displaying progress bar for series

* Use events instead of redux to improve performance

* Add documentation, resolve issues with different datasets

* CR Updates

* CR Updates

* CR Updates & Resolve issue when activating both stack and study prefetch

* Add upward and update display set logic

* Add new behavior to prefetching orders

* Rollback display set check and activate cache check

* Remove prefetch check

* Check if displayset has images

* Debounce check cache events

* Remove usued debounce flags

* Ignore prefetching same image

* Use new image instead of image rendered

* Continue prefetch after cache check

* Update prefetch logic after checking cache

* Remove log

* Update displayset count default to 3

* Update netlify config

Co-authored-by: Alireza <ar.sedghi@gmail.com>
2021-09-09 09:58:24 +02:00
137 changed files with 6724 additions and 1186 deletions

No files matched your search

+11 -7
View File
@@ -25,11 +25,9 @@ executors:
docker:
# Primary container image where all steps run.
- image: 'cypress/browsers:node14.15.0-chrome86-ff82'
- image: 'ohif/viewer-testdata:0.1-test'
defaults: &defaults
docker:
- image: circleci/node:14.15.0
- image: cimg/node:14.18
environment:
TERM: xterm # Enable colors in term
QUICK_BUILD: true
@@ -119,6 +117,7 @@ jobs:
command: yarn install --frozen-lockfile
- setup_remote_docker:
version: 19.03.13
docker_layer_caching: false
- run:
@@ -295,6 +294,7 @@ jobs:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
version: 19.03.13
docker_layer_caching: false
- run:
name: Build and push Docker image
@@ -347,7 +347,8 @@ workflows:
store_artifacts: true
working_directory: platform/viewer
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
git submodule update --init &&
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
yarn run build
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
@@ -375,7 +376,8 @@ workflows:
store_artifacts: true
working_directory: platform/viewer
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
git submodule update --init &&
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
@@ -408,7 +410,8 @@ workflows:
store_artifacts: false
working_directory: platform/viewer
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
git submodule update --init &&
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
@@ -487,7 +490,8 @@ workflows:
store_artifacts: false
working_directory: platform/viewer
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
git submodule update --init &&
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
+1
View File
@@ -0,0 +1 @@
custom: https://giving.massgeneral.org/ohif
@@ -1,18 +0,0 @@
---
name: "\U0001F917 Support Question"
about: "I have a question \U0001F4AC"
title: ''
labels: 'Community: Question :question:, Triage :white_flag:'
assignees: ''
---
> :hand: We are a small team with limited resources. Your question is much more
> likely to be answered if it is
> [a good question](https://stackoverflow.com/help/how-to-ask)
**Description**
Questions can often be answered by our documentation. Unable to find an answer
in our docs? We'll try to help. In the meantime, if you answer your own
question, please respond with the answer here so that others may benefit as
well. Better yet, open a PR to expand our docs ^\_^
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 🤗 Support Question
url: https://community.ohif.org/
about: Please use our forum if you have questions or need help.
+3
View File
@@ -10,6 +10,9 @@ junit.xml
coverage/
.docz/
*.js.map
.yarn
platform/docs/.docusaurus
platform/ui/storybook-static
# YALC (for Erik)
.yalc
+3
View File
@@ -0,0 +1,3 @@
[submodule "testdata"]
path = testdata
url = https://github.com/OHIF/viewer-testdata-dicomweb.git
+1
View File
@@ -0,0 +1 @@
14.18.0
+1 -1
View File
@@ -28,7 +28,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
const config = {
mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
devtool: isProdBuild ? 'source-map' : 'eval-cheap-module-source-map',
entry: {
app: `${SRC_DIR}/index.js`,
},
+11
View File
@@ -273,6 +273,17 @@ development of OHIF Viewer:
Leidos Biomedical Research under Task Order HHSN26100071 from NCI.
[IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized
version of the OHIF Viewer.
### Research notice
Please note that this repository is participating in a study into sustainability
of open source projects. Data will be gathered about this repository for
approximately the next 12 months, starting from June 2021.
Data collected will include number of contributors, number of PRs, time taken to
close/merge these PRs, and issues closed.
For more information, please visit
[our informational page](https://sustainable-open-science-and-software.github.io/) or download our [participant information sheet](https://sustainable-open-science-and-software.github.io/assets/PIS_sustainable_software.pdf).
## License
@@ -8,7 +8,9 @@ OHIF Viewer and its medical imaging data. This post covers one of many potential
setups that accomplish that. Please note, noticably absent is user account
control.
Do not use this recipe to host sensitive medical data on the open web. Depending
> **Do not use this recipe to host sensitive medical data on the open web!**
Depending
on your company's policies, this may be an appropriate setup on an internal
network when protected with a server's basic authentication. For a more robust
setup, check out our [user account control recipe](./user-account-control.md)
@@ -23,13 +25,13 @@ Our two biggest hurdles when hosting our image archive and web client are:
### Handling Web Requests
We mittigate our first issue by allowing [Nginx][nginx] to handle incoming web
We mitigate our first issue by allowing [Nginx][nginx] to handle incoming web
requests. Nginx is open source software for web serving, reverse proxying,
caching, and more. It's designed for maximum performance and stability --
allowing us to more reliably serve content than Orthanc's built-in server can.
More specifically, we accomplish this by using a
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
resources from our image archive (Orthanc), and when accessing its web admin.
> A reverse proxy is a type of proxy server that retrieves resources on behalf
@@ -38,7 +40,7 @@ resources from our image archive (Orthanc), and when accessing its web admin.
### CORS Issues
Cross-Origin Resource Sharing (CORS) is a mechanism that uses HTTP headers to
[Cross-Origin Resource Sharing][understanding-cors] (CORS) is a mechanism that uses HTTP headers to
tell a browser which web applications have permission to access selected
resources from a server at a different origin (domain, protocol, port). IE. By
default, a Web App located at `http://my-website.com` can't access resources
@@ -51,9 +53,6 @@ We can solve this one of two ways:
**This solution uses the first approach.**
You can read more about CORS in this Medium article: [Understanding
CORS][understanding-cors]
### Diagram
This setup allows us to create a setup similar to the one pictured below:
@@ -73,6 +72,8 @@ This setup allows us to create a setup similar to the one pictured below:
- Docker
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
- Node 16.x with optional Python and Visual Studio Build Tools ([webpack 4 has an issue](https://github.com/webpack/webpack/issues/14532) with Node 17.x)
- [Node.js](https://nodejs.org/en/download/)
_Not sure if you have `docker` installed already? Try running `docker --version`
in command prompt or terminal_
@@ -81,7 +82,7 @@ in command prompt or terminal_
_Spin Things Up_
- Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell
- Navigate to `./platform/viewer/.recipes/OpenResty-Orthanc` in your shell
- Run `docker-compose up`
_Upload Your First Study_
@@ -94,16 +95,16 @@ _Upload Your First Study_
### Troubleshooting
_Exit code 137_
#### Exit code 137
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
tab, and increase the amount of Memory available.
_Cannot create container for service X_
#### Cannot create container for service X
Use this one with caution: `docker system prune`
_X is already running_
#### X is already running
Stop running all containers:
@@ -120,15 +121,12 @@ likely want to update:
#### OHIF Viewer
The OHIF Viewer's configuration is imported from a static `.js` file. The
configuration we use is set to a specific file when we build the viewer, and
configuration we use is set to [a specific file][config-ohif] when we build the viewer, and
determined by the env variable: `APP_CONFIG`. You can see where we set its value
in the `dockerfile` for this solution:
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
You can find the configuration we're using here:
`/public/config/docker_openresty-orthanc.js`
To rebuild the `webapp` image created by our `dockerfile` after updating the
Viewer's configuration, you can run:
@@ -137,7 +135,7 @@ Viewer's configuration, you can run:
#### Other
All other files are found in: `/docker/OpenResty-Orthanc/`
All other files in this directory include
| Service | Configuration | Docs |
| ----------------- | --------------------------------- | ------------------------------------------- |
@@ -236,6 +234,8 @@ members put together:
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
<!-- SRC -->
[config-ohif]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/docker_openresty-orthanc.js
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
+39 -3
View File
@@ -67,6 +67,33 @@ window.config = {
extensions: [],
showStudyList: true,
filterQueryParam: false,
/**
* Disable caching of servers configuration.
*
* There will be no effect if you update the servers property of this config
* while your application is running because this property is cached in local storage.
*/
disableServersCache: false,
/**
* OHIF's study prefetcher configuration.
*
* @param {boolean} enabled Whether to enable/disable OHIF's study prefetcher
* @param {('all'|'closest'|'downward'|'upward'|'topdown')} order Fetching order: all display sets, the closest ones, downward or top down fashion based on the currently selected display set
* @param {number} displaySetCount How much display sets should be prefetched at once (note: this attribute is ignored if order was set to 'all')
* @param {boolean} preventCache Prevent images to be cached in Cornerstone Tools's request pool manager
* @param {number} prefetchDisplaySetsTimeout Prefetch timeout
* @param {boolean} displayProgress Whether to display or not the progress bar in the display set
* @param {boolean} includeActiveDisplaySet Include or not the active display set while prefetching
*/
studyPrefetcher: {
enabled: true,
order: 'all',
displaySetCount: 1,
preventCache: false,
prefetchDisplaySetsTimeout: 300,
displayProgress: false,
includeActiveDisplaySet: true,
},
servers: {
dicomWeb: [
{
@@ -81,7 +108,14 @@ window.config = {
},
],
},
// Supported Keys: https://craig.is/killing/mice
/**
* Hotkey definitions.
* Supported Keys: https://craig.is/killing/mice
*
* @param {string} commandName
* @param {string} label
* @param {array} keys
*/
hotkeys: [
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
@@ -97,14 +131,16 @@ window.config = {
keys: ['v'],
},
],
/* Configuration passed to the bundled cornerstone extension
/**
* Configuration passed to the bundled cornerstone extension
*
* The cornerstone extension is currently tightly coupled to the platform.
* Until we're able to decouple it, this key will serve as a workaround to
* pass it configuration.
*
* @param {boolean} hideHandles Whether to show/hide annotation "handles"
*/
cornerstoneExtensionConfig: {
/* Whether to show/hide annotation "handles" */
hideHandles: true,
},
};
+37
View File
@@ -0,0 +1,37 @@
# Viewer: Study Prefetcher & Stack Prefetch
Prefetching the stack of images of the active viewport is enabled by default for
cornerstone viewports via cornerstone tools's stack prefetch functionality.
In order to have a more customizable alternative for prefetching, OHIF provides
a built-in study prefetcher functionality which allows the user to set the order
in which the display sets are prefetched, the granularity, the timeout and
whether or not display a progress bar in the display set (close to the thumbnail
image) in the study browser.
You can customize these options via the viewer's configuration:
```js
window.config = {
/**
* OHIF's study prefetcher configuration.
*
* @param {boolean} enabled Whether to enable/disable OHIF's study prefetcher
* @param {('all'|'closest'|'downward'|'upward'|'topdown')} order Fetching order: all display sets, the closest ones, downward or top down fashion based on the currently selected display set
* @param {number} displaySetCount How much display sets should be prefetched at once (note: this attribute is ignored if order was set to 'all')
* @param {boolean} preventCache Prevent images to be cached in Cornerstone Tools's request pool manager
* @param {number} prefetchDisplaySetsTimeout Prefetch timeout
* @param {boolean} displayProgress Whether to display or not the progress bar in the display set
* @param {boolean} includeActiveDisplaySet Include or not the active display set while prefetching
*/
studyPrefetcher: {
enabled: true,
order: 'all',
displaySetCount: 1,
preventCache: false,
prefetchDisplaySetsTimeout: 300,
displayProgress: false,
includeActiveDisplaySet: true,
},
};
```
+78
View File
@@ -3,6 +3,84 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.12.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.6...@ohif/extension-cornerstone@2.12.7) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.12.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.5...@ohif/extension-cornerstone@2.12.6) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.12.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.4...@ohif/extension-cornerstone@2.12.5) (2022-02-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.12.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.3...@ohif/extension-cornerstone@2.12.4) (2022-01-20)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.12.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.2...@ohif/extension-cornerstone@2.12.3) (2021-12-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.12.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.1...@ohif/extension-cornerstone@2.12.2) (2021-10-26)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.12.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.0...@ohif/extension-cornerstone@2.12.1) (2021-09-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [2.12.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.11.0...@ohif/extension-cornerstone@2.12.0) (2021-09-23)
### Features
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
# [2.11.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.10...@ohif/extension-cornerstone@2.11.0) (2021-09-09)
### Features
* OHIF-1000: Study prefetcher ([#2466](https://github.com/OHIF/Viewers/issues/2466)) ([8f26045](https://github.com/OHIF/Viewers/commit/8f2604509d24092b721f1333289d838820d68cac))
## [2.10.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.9...@ohif/extension-cornerstone@2.10.10) (2021-07-26)
+23 -2
View File
@@ -85,12 +85,32 @@ Tools can be configured through extension configuration using the tools key:
...
```
## Stack prefetch configuration
Stack prefetch can be configured through extension configuration using the
stackPrefetch key:
```js
...
cornerstoneExtensionConfig: {
stackPrefetch: {
enabled: true,
maxImagesToPrefetch: Infinity,
preserveExistingPool: false,
maxSimultaneousRequests: 20,
},
},
...
```
## Annotate Tools Configuration
*We currently support one property for annotation tools.*
_We currently support one property for annotation tools._
### Hide handles
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
This extension configuration allows you to toggle on/off handle rendering for
all annotate tools:
```js
...
@@ -119,3 +139,4 @@ This extension configuration allows you to toggle on/off handle rendering for al
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
[cornerstone]: https://github.com/cornerstonejs/cornerstone
<!-- prettier-ignore-end -->
```
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.10.10",
"version": "2.12.7",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -32,12 +32,12 @@
"peerDependencies": {
"@ohif/core": "^0.50.0",
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.20.1",
"cornerstone-wado-image-loader": "^3.1.0",
"dcmjs": "0.18.8",
"dicom-parser": "^1.8.3",
"cornerstone-core": "^2.6.1",
"cornerstone-math": "^0.1.9",
"cornerstone-tools": "^6.0.6",
"cornerstone-wado-image-loader": "^4.1.0",
"dcmjs": "0.19.4",
"dicom-parser": "^1.8.11",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^16.8.6",
@@ -51,6 +51,6 @@
"classnames": "^2.2.6",
"lodash.merge": "^4.6.2",
"lodash.throttle": "^4.1.1",
"react-cornerstone-viewport": "4.0.5"
"react-cornerstone-viewport": "4.1.1"
}
}
@@ -3,6 +3,7 @@ import OHIF from '@ohif/core';
import { connect } from 'react-redux';
import throttle from 'lodash.throttle';
import { setEnabledElement } from './state';
import initSRTools from './tools/initSRTools';
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
const {
@@ -53,7 +54,11 @@ const mapStateToProps = (state, ownProps) => {
// Currently justing using escape hatch + commands
// activeTool: activeButton && activeButton.command,
...dataFromStore,
isStackPrefetchEnabled: isActive,
isStackPrefetchEnabled: ownProps.hasOwnProperty('isStackPrefetchEnabled')
? ownProps.isStackPrefetchEnabled
: ownProps.stackPrefetch
? ownProps.stackPrefetch.enabled
: isActive,
isPlaying,
frameRate,
//stack: viewportSpecificData.stack,
@@ -88,6 +93,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
plugin: 'cornerstone',
})
);
initSRTools(enabledElement);
},
onMeasurementsChanged: (event, action) => {
@@ -1,10 +1,11 @@
import React, { Component } from 'react';
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay'
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay';
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
import OHIF from '@ohif/core';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import checkForSRAnnotations from './tools/checkForSRAnnotations';
const { StackManager } = OHIF.utils;
@@ -15,6 +16,7 @@ class OHIFCornerstoneViewport extends Component {
static defaultProps = {
customProps: {},
isStackPrefetchEnabled: true,
};
static propTypes = {
@@ -23,6 +25,8 @@ class OHIFCornerstoneViewport extends Component {
viewportIndex: PropTypes.number,
children: PropTypes.node,
customProps: PropTypes.object,
stackPrefetch: PropTypes.object,
isStackPrefetchEnabled: PropTypes.bool,
};
static id = 'OHIFCornerstoneViewport';
@@ -180,10 +184,12 @@ class OHIFCornerstoneViewport extends Component {
if (
displaySet.displaySetInstanceUID !==
prevDisplaySet.displaySetInstanceUID ||
prevDisplaySet.displaySetInstanceUID ||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
displaySet.frameIndex !== prevDisplaySet.frameIndex
) {
const { viewportIndex } = this.props;
checkForSRAnnotations({ displaySet, viewportIndex });
this.setStateFromProps();
}
}
@@ -232,7 +238,12 @@ class OHIFCornerstoneViewport extends Component {
};
const warningsOverlay = props => {
return <OHIFCornerstoneViewportOverlay {...props} inconsistencyWarnings={inconsistencyWarnings} />
return (
<OHIFCornerstoneViewportOverlay
{...props}
inconsistencyWarnings={inconsistencyWarnings}
/>
);
};
return (
@@ -244,10 +255,11 @@ class OHIFCornerstoneViewport extends Component {
onNewImageDebounced={newImageHandler}
onNewImageDebounceTime={300}
viewportOverlayComponent={warningsOverlay}
stackPrefetch={this.props.stackPrefetch}
isStackPrefetchEnabled={this.props.isStackPrefetchEnabled}
// ~~ Connected (From REDUX)
// frameRate={frameRate}
// isPlaying={false}
// isStackPrefetchEnabled={true}
// onElementEnabled={() => {}}
// setViewportActive{() => {}}
{...this.props.customProps}
+11 -2
View File
@@ -36,7 +36,7 @@ export default {
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
getViewportModule({ commandsManager }) {
getViewportModule({ commandsManager, appConfig }) {
const ExtendedOHIFCornerstoneViewport = props => {
/**
* TODO: This appears to be used to set the redux parameters for
@@ -48,8 +48,17 @@ export default {
jumpData.refreshViewports = false;
commandsManager.runCommand('jumpToImage', jumpData);
};
const { studyPrefetcher } = appConfig;
const isStackPrefetchEnabled =
studyPrefetcher && !studyPrefetcher.enabled;
return (
<OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />
<OHIFCornerstoneViewport
{...props}
onNewImage={onNewImageHandler}
isStackPrefetchEnabled={isStackPrefetchEnabled}
/>
);
};
+13 -2
View File
@@ -5,6 +5,8 @@ import csTools from 'cornerstone-tools';
import merge from 'lodash.merge';
import initCornerstoneTools from './initCornerstoneTools.js';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import dicomSRModule from './tools/modules/dicomSRModule';
import srModuleId from './tools/id';
/**
*
@@ -15,6 +17,8 @@ import measurementServiceMappingsFactory from './utils/measurementServiceMapping
export default function init({ servicesManager, configuration }) {
const { UIDialogService, MeasurementService } = servicesManager.services;
csTools.register('module', srModuleId, dicomSRModule);
const callInputDialog = (data, event, callback) => {
if (UIDialogService) {
let dialogId = UIDialogService.create({
@@ -37,7 +41,14 @@ export default function init({ servicesManager, configuration }) {
}
};
const { csToolsConfig } = configuration;
const {
csToolsConfig,
stackPrefetch = {
maxImagesToPrefetch: Infinity,
preserveExistingPool: false,
maxSimultaneousRequests: 20,
},
} = configuration;
const metadataProvider = OHIF.cornerstone.metadataProvider;
cornerstone.metaData.addProvider(
@@ -52,7 +63,7 @@ export default function init({ servicesManager, configuration }) {
autoResizeViewports: false,
};
initCornerstoneTools(defaultCsToolsConfig);
initCornerstoneTools({ ...defaultCsToolsConfig, ...stackPrefetch });
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
@@ -37,8 +37,8 @@ export default function(configuration = {}) {
// Configure stack prefetch
cornerstoneTools.stackPrefetch.setConfiguration({
maxImagesToPrefetch: Infinity,
preserveExistingPool: false,
maxSimultaneousRequests: 20,
maxImagesToPrefetch: configuration.maxImagesToPrefetch,
preserveExistingPool: configuration.preserveExistingPool,
maxSimultaneousRequests: configuration.maxSimultaneousRequests,
});
}
@@ -0,0 +1,414 @@
import csTools, {
importInternal,
getToolState,
toolColors,
} from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
/** Internal imports */
import TOOL_NAMES from './constants/toolNames';
import SCOORD_TYPES from './constants/scoordTypes';
import id from './id';
/** Cornerstone 3rd party dev kit imports */
const draw = importInternal('drawing/draw');
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
const drawCircle = importInternal('drawing/drawCircle');
const drawEllipse = importInternal('drawing/drawEllipse');
const drawHandles = importInternal('drawing/drawHandles');
const drawArrow = importInternal('drawing/drawArrow');
const getNewContext = importInternal('drawing/getNewContext');
const BaseTool = importInternal('base/BaseTool');
const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox');
/**
* @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay).
*
* This is a generic render tool.
*
* A single tool that, given some schema, can render
* POINT, MULTIPOINT, POLYLINE, CIRCLE, and ELLIPSE
* value types for a given imageId.
*
*
* @extends cornerstoneTools.BaseTool
*/
export default class DICOMSRDisplayTool extends BaseTool {
constructor(props = {}) {
const defaultProps = {
mixins: ['enabledOrDisabledBinaryTool'],
name: TOOL_NAMES.DICOM_SR_DISPLAY_TOOL,
};
const initialProps = Object.assign(defaultProps, props);
super(initialProps);
this._module = csTools.getModule(id);
}
renderToolData(evt) {
const eventData = evt.detail;
const { element } = eventData;
const module = this._module;
const toolState = getToolState(element, this.name);
if (!toolState) {
return;
}
const trackingUniqueIdentifiersForElement = module.getters.trackingUniqueIdentifiersForElement(
element
);
const {
activeIndex,
trackingUniqueIdentifiers,
} = trackingUniqueIdentifiersForElement;
const activeTrackingUniqueIdentifier =
trackingUniqueIdentifiers[activeIndex];
// Filter toolData to only render the data for the active SR.
const filteredToolData = toolState.data.filter(td =>
trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier)
);
let shouldRepositionTextBoxes = false;
for (let i = 0; i < filteredToolData.length; i++) {
const data = filteredToolData[i];
const { renderableData, labels } = data;
const color =
data.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
? toolColors.getActiveColor()
: toolColors.getToolColor();
const lineWidth = 2;
const options = {
color,
lineWidth,
handleRadius: 6,
};
Object.keys(renderableData).forEach(GraphicType => {
const renderableDataForGraphicType = renderableData[GraphicType];
switch (GraphicType) {
case SCOORD_TYPES.POINT:
this.renderPoint(renderableDataForGraphicType, eventData, options);
break;
case SCOORD_TYPES.MULTIPOINT:
this.renderMultipoint(
renderableDataForGraphicType,
eventData,
options
);
break;
case SCOORD_TYPES.POLYGON:
case SCOORD_TYPES.POLYLINE:
this.renderPolyLine(
renderableDataForGraphicType,
eventData,
options
);
break;
case SCOORD_TYPES.CIRCLE:
this.renderCircle(renderableDataForGraphicType, eventData, options);
break;
case SCOORD_TYPES.ELLIPSE:
this.renderEllipse(
renderableDataForGraphicType,
eventData,
options
);
break;
}
});
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
if (!data.handles || !data.handles.textBox) {
const textBox = {
active: false,
hasMoved: true,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true,
};
const anchorPoints = _getTextBoxAnchorPointsForRenderableData(
renderableData,
eventData
);
textBox.anchorPoints = anchorPoints;
const bottomRight = {
x: Math.max(...anchorPoints.map(point => point.x)),
y: Math.max(...anchorPoints.map(point => point.y)),
};
textBox.x = bottomRight.x;
textBox.y = bottomRight.y;
data.handles = {};
data.handles.textBox = textBox;
shouldRepositionTextBoxes = true;
}
const text = _getTextBoxLinesFromLabels(labels);
function textBoxAnchorPoints() {
return data.handles.textBox.anchorPoints;
}
draw(context, context => {
drawLinkedTextBox(
context,
element,
data.handles.textBox,
text,
data.handles,
textBoxAnchorPoints,
color,
lineWidth,
0,
true
);
});
}
// TOOD -> text boxes may overlap with other annotations at the moment.
// To be fixed after we get requirements.
// if (shouldRepositionTextBoxes) {
// this.repositionTextBox(filteredToolData, eventData);
// }
}
// repositionTextBox(toolData, eventData) {
// const toolBoundingBoxes = [];
// for (let i = 0; i < toolData.length; i++) {
// const toolDataI = toolData[i];
// const { textBox } = toolDataI.handles;
// const { anchorPoints } = textBox;
// const boundingBox = _getBoundingBoxFromAnchorPoints(anchorPoints);
// // Get the textbox bounding locations.
// // Get the tool extents.
// }
// }
renderPolyLine(renderableData, eventData, options) {
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(points => {
draw(context, context => {
drawJoinedLines(context, element, points[0], points, options);
});
});
}
renderMultipoint(renderableData, eventData, options) {
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(points => {
draw(context, context => {
drawHandles(context, eventData, points, options);
});
});
}
renderPoint(renderableData, eventData, options) {
// Render single point as an arrow.
const { element, image } = eventData;
const { rows, columns } = image;
const context = getNewContext(eventData.canvasContext.canvas);
const { color, lineWidth } = options;
// Find a suitable length for the image size.
const xOffset = columns / 10;
const yOffset = rows / 10;
renderableData.forEach(points => {
const point = points[0]; // The SCOORD type is POINT so the array length is 1.
draw(context, context => {
// Draw the arrow
const handleStartCanvas = cornerstone.pixelToCanvas(element, point);
const handleEndCanvas = cornerstone.pixelToCanvas(element, {
x: point.x + xOffset,
y: point.y + yOffset,
});
drawArrow(
context,
handleEndCanvas,
handleStartCanvas,
color,
lineWidth,
false
);
});
});
}
renderCircle(renderableData, eventData, options) {
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(circle => {
const { center, radius } = circle;
drawCircle(context, element, center, radius, options);
});
}
renderEllipse(renderableData, eventData, options) {
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(ellipse => {
const { corner1, corner2 } = ellipse;
drawEllipse(
context,
element,
corner1,
corner2,
options,
'pixel',
0 // TODO -> Work our the initial rotation and add it here so we render appropriately rotated ellipses.
);
});
}
}
function _getTextBoxLinesFromLabels(labels) {
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 3);
const lines = [];
for (let i = 0; i < labelLength; i++) {
const labelEntry = labels[i];
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
}
return lines;
}
const SHORT_HAND_MAP = {
'Short Axis': 'W ',
'Long Axis': 'L ',
AREA: 'Area ',
Length: '',
CORNERSTONEFREETEXT: '',
};
function _labelToShorthand(label) {
const shortHand = SHORT_HAND_MAP[label];
if (shortHand !== undefined) {
return shortHand;
}
return label;
}
function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) {
let anchorPoints = [];
Object.keys(renderableData).forEach(GraphicType => {
const renderableDataForGraphicType = renderableData[GraphicType];
switch (GraphicType) {
case SCOORD_TYPES.POINT:
renderableDataForGraphicType.forEach(points => {
anchorPoints = [...anchorPoints, ...points];
// Add other arrow point based on image size.
const { image } = eventData;
const { rows, columns } = image;
const xOffset = columns / 10;
const yOffset = rows / 10;
const point = points[0];
anchorPoints.push({ x: point.x + xOffset, y: point.y + yOffset });
});
break;
case SCOORD_TYPES.MULTIPOINT:
case SCOORD_TYPES.POLYLINE:
case SCOORD_TYPES.POLYGON:
renderableDataForGraphicType.forEach(points => {
anchorPoints = [...anchorPoints, ...points];
});
break;
case SCOORD_TYPES.CIRCLE:
renderableDataForGraphicType.forEach(circle => {
const { center, radius } = circle;
anchorPoints.push({ x: center.x + radius, y: center.y });
anchorPoints.push({ x: center.x - radius, y: center.y });
anchorPoints.push({ x: center.x, y: center.y + radius });
anchorPoints.push({ x: center.x, y: center.y - radius });
});
break;
case SCOORD_TYPES.ELLIPSE:
renderableDataForGraphicType.forEach(ellipse => {
const { corner1, corner2 } = ellipse;
const halfWidth = Math.abs(corner1.x - corner2.x) / 2;
const halfHeight = Math.abs(corner1.y - corner2.y) / 2;
const center = {
x: (corner1.x + corner2.x) / 2,
y: (corner1.y + corner2.y) / 2,
};
anchorPoints.push({ x: center.x + halfWidth, y: center.y });
anchorPoints.push({ x: center.x - halfWidth, y: center.y });
anchorPoints.push({ x: center.x, y: center.y + halfHeight });
anchorPoints.push({ x: center.x, y: center.y - halfHeight });
});
break;
}
});
return anchorPoints;
}
function _getBoundingBoxFromAnchorPoints(anchorPoints) {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
anchorPoints.forEach(point => {
const { x, y } = point;
if (x > maxX) {
maxX = x;
} else if (x < minX) {
minX = x;
}
if (y > maxX) {
maxY = y;
} else if (y < minY) {
minY = y;
}
});
}
@@ -0,0 +1,57 @@
import csTools from 'cornerstone-tools';
import cs from 'cornerstone-core';
import OHIF from '@ohif/core';
import { getEnabledElement } from '../state';
import id from './id';
const { studyMetadataManager } = OHIF.utils;
const checkForSRAnnotations = ({ viewportIndex, displaySet }) => {
const srModule = csTools.getModule(id);
const element = getEnabledElement(viewportIndex);
if (!element) {
return;
}
const { StudyInstanceUID } = displaySet;
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
if (!studyMetadata) {
return;
}
const srDisplaySets = studyMetadata
.getDisplaySets()
.filter(ds => ds.Modality === 'SR');
if (srDisplaySets.length === 0) {
return;
}
const { measurements: _measurements } = srDisplaySets[0];
if (!_measurements || _measurements.length < 1) {
return;
}
const measurements = _measurements.filter(m => m.loaded === true);
const measurement = measurements[0];
if (!measurement) {
return;
}
srModule.setters.trackingUniqueIdentifiersForElement(
element,
measurements.map(measurement => measurement.TrackingUniqueIdentifier),
measurement
);
const { TrackingUniqueIdentifier } = measurement;
srModule.setters.activeTrackingUniqueIdentifierForElement(
element,
TrackingUniqueIdentifier
);
cs.updateImage(element);
};
export default checkForSRAnnotations;
@@ -0,0 +1,8 @@
export default {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
POLYGON: 'POLYGON',
};
@@ -0,0 +1,5 @@
const TOOL_NAMES = {
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
};
export default TOOL_NAMES;
+7
View File
@@ -0,0 +1,7 @@
const id = 'org.ohif.dicom-sr';
export default id;
const SOPClassHandlerName = 'dicom-sr';
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
export { SOPClassHandlerName, SOPClassHandlerId };
@@ -0,0 +1,108 @@
import cornerstoneTools from 'cornerstone-tools';
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
import TOOL_NAMES from './constants/toolNames';
import getToolAlias from './utils/getToolAlias';
/**
* Initialize SR cornerstone tools.
*
* @param {*} targetElement
*/
const initSRTools = targetElement => {
const primaryToolId = 'Wwwc';
const toolAlias = getToolAlias(primaryToolId); // These are 1:1 for built-in only
// ~~ MAGIC
cornerstoneTools.addToolForElement(targetElement, DICOMSRDisplayTool);
cornerstoneTools.setToolEnabledForElement(
targetElement,
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL
);
// ~~ Variants
cornerstoneTools.addToolForElement(
targetElement,
cornerstoneTools.LengthTool,
{
name: 'SRLength',
configuration: {
renderDashed: true,
},
}
);
cornerstoneTools.addToolForElement(
targetElement,
cornerstoneTools.ArrowAnnotateTool,
{
name: 'SRArrowAnnotate',
configuration: {
renderDashed: true,
},
}
);
cornerstoneTools.addToolForElement(
targetElement,
cornerstoneTools.BidirectionalTool,
{
name: 'SRBidirectional',
configuration: {
renderDashed: true,
},
}
);
cornerstoneTools.addToolForElement(
targetElement,
cornerstoneTools.EllipticalRoiTool,
{
name: 'SREllipticalRoi',
configuration: {
renderDashed: true,
},
}
);
cornerstoneTools.addToolForElement(
targetElement,
cornerstoneTools.RectangleRoiTool,
{
name: 'SRRectangleRoi',
configuration: {
renderDashed: true,
},
}
);
cornerstoneTools.addToolForElement(
targetElement,
cornerstoneTools.FreehandRoiTool,
{
name: 'SRFreehandRoi',
configuration: {
renderDashed: true,
},
}
);
// ~~ Business as usual
cornerstoneTools.setToolActiveForElement(targetElement, 'PanMultiTouch', {
pointers: 2,
});
cornerstoneTools.setToolActiveForElement(targetElement, 'ZoomTouchPinch', {});
// TODO: Add always dashed tool alternative aliases
// TODO: or same name... alternative config?
cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, {
mouseButtonMask: 1,
});
cornerstoneTools.setToolActiveForElement(targetElement, 'Pan', {
mouseButtonMask: 4,
});
cornerstoneTools.setToolActiveForElement(targetElement, 'Zoom', {
mouseButtonMask: 2,
});
cornerstoneTools.setToolActiveForElement(
targetElement,
'StackScrollMouseWheel',
{}
);
};
export default initSRTools;
@@ -0,0 +1,61 @@
import cornerstone from 'cornerstone-core';
const state = {
TrackingUniqueIdentifier: null,
trackingIdentifiersByEnabledElementUUID: {},
};
function setTrackingUniqueIdentifiersForElement(
element,
trackingUniqueIdentifiers,
activeIndex = 0
) {
const enabledElement = cornerstone.getEnabledElement(element);
const { uuid } = enabledElement;
state.trackingIdentifiersByEnabledElementUUID[uuid] = {
trackingUniqueIdentifiers,
activeIndex,
};
}
function setActiveTrackingUniqueIdentifierForElement(
element,
TrackingUniqueIdentifier
) {
const enabledElement = cornerstone.getEnabledElement(element);
const { uuid } = enabledElement;
const trackingIdentifiersForElement =
state.trackingIdentifiersByEnabledElementUUID[uuid];
if (trackingIdentifiersForElement) {
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(
tuid => tuid === TrackingUniqueIdentifier
);
trackingIdentifiersForElement.activeIndex = activeIndex;
}
}
function getTrackingUniqueIdentifiersForElement(element) {
const enabledElement = cornerstone.getEnabledElement(element);
const { uuid } = enabledElement;
if (state.trackingIdentifiersByEnabledElementUUID[uuid]) {
return state.trackingIdentifiersByEnabledElementUUID[uuid];
}
return { trackingUniqueIdentifiers: [] };
}
export default {
state,
getters: {
trackingUniqueIdentifiersForElement: getTrackingUniqueIdentifiersForElement,
},
setters: {
trackingUniqueIdentifiersForElement: setTrackingUniqueIdentifiersForElement,
activeTrackingUniqueIdentifierForElement: setActiveTrackingUniqueIdentifierForElement,
},
};
@@ -0,0 +1,32 @@
/**
* Get cornerstone tool alias.
*
* @param {string} toolName
* @returns tool alias
*/
export default function getToolAlias(toolName) {
let toolAlias = toolName;
switch (toolName) {
case 'Length':
toolAlias = 'SRLength';
break;
case 'Bidirectional':
toolAlias = 'SRBidirectional';
break;
case 'ArrowAnnotate':
toolAlias = 'SRArrowAnnotate';
break;
case 'EllipticalRoi':
toolAlias = 'SREllipticalRoi';
break;
case 'FreehandRoi':
toolAlias = 'SRFreehandRoi';
break;
case 'RectangleRoi':
toolAlias = 'SRRectangleRoi';
break;
}
return toolAlias;
}
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.3.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.3.1...@ohif/extension-debugging@0.3.2) (2022-03-10)
**Note:** Version bump only for package @ohif/extension-debugging
## [0.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.3.0...@ohif/extension-debugging@0.3.1) (2021-12-02)
**Note:** Version bump only for package @ohif/extension-debugging
# [0.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.2...@ohif/extension-debugging@0.3.0) (2021-09-23)
### Features
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
## [0.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.1...@ohif/extension-debugging@0.2.2) (2021-06-03)
**Note:** Version bump only for package @ohif/extension-debugging
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-debugging",
"version": "0.2.2",
"version": "0.3.2",
"description": "OHIF extension for debugging.",
"author": "OHIF",
"license": "MIT",
@@ -28,12 +28,12 @@
},
"peerDependencies": {
"@ohif/core": "^2.6.0",
"dicom-parser": "^1.8.3"
"dicom-parser": "^1.8.11"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"detect-browser": "5.1.1",
"dicomweb-client": "^0.8.1",
"dicomweb-client": "^0.8.3",
"file-saver": "^2.0.2",
"jszip": "^3.2.2"
}
@@ -24,3 +24,21 @@
overflow: scroll;
max-height: 300px;
}
.debug-overflowText {
margin: 0 auto;
max-width: 300px;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
}
.debug-overflowText-border {
margin: 0 auto;
max-width: 300px;
border: solid 2px #ccc;
padding: 12px;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
}
+169 -2
View File
@@ -2,6 +2,8 @@ import React from 'react';
import { detect } from 'detect-browser';
import './DebugReportModal.css';
import { ToolbarButton } from '@ohif/ui';
import { utils } from '@ohif/core';
const { studyMetadataManager } = utils;
const DubugReportModal = ({
viewports,
@@ -70,7 +72,7 @@ const DubugReportModal = ({
body += '== Viewport Layout ==\n';
body += `Rows\t${numRows}\n`;
body += `Columns\t${numColumns}\n\n`;
body += '== Viewports ==\n';
body += '== SeriesInstanceUIDs ==\n';
Object.keys(viewportSpecificData).forEach(viewportIndex => {
const vsd = viewportSpecificData[viewportIndex];
@@ -83,6 +85,40 @@ const DubugReportModal = ({
body += `[${row},${column}]\t${vsd.SeriesInstanceUID}\n`;
});
body += '== ReferencedSEGSeriesInstanceUIDs ==\n';
Object.keys(viewportSpecificData).forEach(viewportIndex => {
const displaySet = viewportSpecificData[viewportIndex];
const [
referencedDisplaySetsRef,
referencedDisplaySetsCount,
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'SEG');
const [row, column] = _viewportIndexToViewportPosition(
viewportIndex,
numColumns
);
body += `[${row},${column}]\t${referencedDisplaySetsRef}\n`;
});
body += '== ReferencedRTSTRUCTSeriesInstanceUIDs ==\n';
Object.keys(viewportSpecificData).forEach(viewportIndex => {
const displaySet = viewportSpecificData[viewportIndex];
const [
referencedDisplaySetsRef,
referencedDisplaySetsCount,
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'RTSTRUCT');
const [row, column] = _viewportIndexToViewportPosition(
viewportIndex,
numColumns
);
body += `[${row},${column}]\t${referencedDisplaySetsRef}\n`;
});
return body;
};
@@ -171,7 +207,7 @@ const getCurrentStudyUrl = () => {
</tr>
<tr>
<td>URL</td>
<td>{window.location.href}</td>
<td className="debug-overflowText">{window.location.href}</td>
</tr>
</React.Fragment>
);
@@ -222,6 +258,14 @@ const getLayout = viewports => {
<th>SeriesInstanceUIDs</th>
</tr>
{getSeriesInstanceUIDsPerRow(viewports)}
<tr>
<th>ReferencedSEGSeriesInstanceUIDs</th>
</tr>
{getReferencedSEGSeriesInstanceUIDsPerRow(viewports)}
<tr>
<th>ReferencedRTSTRUCTSeriesInstanceUIDs</th>
</tr>
{getReferencedRTSTRUCTSeriesInstanceUIDsPerRow(viewports)}
</React.Fragment>
);
};
@@ -256,6 +300,129 @@ const getBrowserInfo = () => {
);
};
/**
* Returns DisplaySets that reference the target series, sorted by dateTime
*
* @param {string} StudyInstanceUID
* @param {string} SeriesInstanceUID
* @returns Array
*/
const _getReferencedDisplaysets = (
StudyInstanceUID,
SeriesInstanceUID,
Modality
) => {
/* Referenced DisplaySets */
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: SeriesInstanceUID,
Modality,
});
/* Sort */
referencedDisplaysets.sort((a, b) => {
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
return bNumber - aNumber;
});
return referencedDisplaysets;
};
const _getReferencedSeriesInstanceUIDsString = (displaySet, Modality) => {
const referencedDisplaySets = _getReferencedDisplaysets(
displaySet.StudyInstanceUID,
displaySet.SeriesInstanceUID,
Modality
);
let referencedDisplaySetsRef = 'None';
const referencedDisplaySetsCount = referencedDisplaySets.length;
if (referencedDisplaySetsCount !== 0) {
referencedDisplaySetsRef = '';
for (let i = 0; i < referencedDisplaySetsCount - 1; i++) {
referencedDisplaySetsRef +=
referencedDisplaySets[i].SeriesInstanceUID + ', ';
}
referencedDisplaySetsRef +=
referencedDisplaySets[referencedDisplaySetsCount - 1].SeriesInstanceUID;
}
return [referencedDisplaySetsRef, referencedDisplaySetsCount];
};
const getReferencedSEGSeriesInstanceUIDsPerRow = viewports => {
const { viewportSpecificData, numColumns } = viewports;
// NOTE viewportSpecificData is actually an object with numerical keys.
return Object.keys(viewportSpecificData).map(viewportIndex => {
const displaySet = viewportSpecificData[viewportIndex];
const [
referencedDisplaySetsRef,
referencedDisplaySetsCount,
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'SEG');
const [row, column] = _viewportIndexToViewportPosition(
viewportIndex,
numColumns
);
if (referencedDisplaySetsCount > 1) {
return (
<tr>
<td>{`[${row},${column}]`}</td>
<td className="debug-overflowText-border">
{referencedDisplaySetsRef}
</td>
</tr>
);
} else {
return (
<tr>
<td>{`[${row},${column}]`}</td>
<td className="debug-overflowText">{referencedDisplaySetsRef}</td>
</tr>
);
}
});
};
const getReferencedRTSTRUCTSeriesInstanceUIDsPerRow = viewports => {
const { viewportSpecificData, numColumns } = viewports;
// NOTE viewportSpecificData is actually an object with numerical keys.
return Object.keys(viewportSpecificData).map(viewportIndex => {
const displaySet = viewportSpecificData[viewportIndex];
const [
referencedDisplaySetsRef,
referencedDisplaySetsCount,
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'RTSTRUCT');
const [row, column] = _viewportIndexToViewportPosition(
viewportIndex,
numColumns
);
if (referencedDisplaySetsCount > 1) {
return (
<tr>
<td>{`[${row},${column}]`}</td>
<td className="debug-overflowText-border">
{referencedDisplaySetsRef}
</td>
</tr>
);
} else {
return (
<tr>
<td>{`[${row},${column}]`}</td>
<td className="debug-overflowText">{referencedDisplaySetsRef}</td>
</tr>
);
}
});
};
const getSeriesInstanceUIDsPerRow = viewports => {
const { viewportSpecificData, numColumns } = viewports;
+64
View File
@@ -3,6 +3,70 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.17...@ohif/extension-dicom-html@1.3.18) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.16...@ohif/extension-dicom-html@1.3.17) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.15...@ohif/extension-dicom-html@1.3.16) (2022-02-02)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.14...@ohif/extension-dicom-html@1.3.15) (2022-01-20)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.13...@ohif/extension-dicom-html@1.3.14) (2021-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.12...@ohif/extension-dicom-html@1.3.13) (2021-10-26)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.11...@ohif/extension-dicom-html@1.3.12) (2021-10-25)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.10...@ohif/extension-dicom-html@1.3.11) (2021-09-28)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.9...@ohif/extension-dicom-html@1.3.10) (2021-06-02)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-html",
"version": "1.3.10",
"version": "1.3.18",
"description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "0.18.8",
"dcmjs": "0.19.4",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
@@ -160,7 +160,7 @@ const getContentSequence = (data, level = 1) => {
const root = [];
if (header) {
const HeaderDynamicLevel = `h${level}`;
const HeaderDynamicLevel = `h${Math.min(level, 6)}`;
root.push(<HeaderDynamicLevel key={header}>{header}</HeaderDynamicLevel>);
}
@@ -170,7 +170,7 @@ const getContentSequence = (data, level = 1) => {
if (key === '_meta') {
const HeaderDynamicLevel = `h3`;
root.push(<hr />);
root.push(<hr key={root.length} />);
root.push(
<HeaderDynamicLevel key="Metadata">
DICOM File Meta Information
@@ -217,7 +217,7 @@ class DicomHtmlViewport extends Component {
};
componentDidMount() {
const dataSet = this.setContentFromByteArray(this.props.byteArray);
this.setContentFromByteArray(this.props.byteArray);
}
setContentFromByteArray(byteArray) {
@@ -5,11 +5,12 @@ const SOP_CLASS_UIDS = {
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
COMPREHENSIVE_3D_SR: '1.2.840.10008.5.1.4.1.1.88.34',
PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40',
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
ACQUISITION_CONTEXT_SR_STORAGE: '1.2.840.10008.5.1.4.1.1.88.71',
ACQUISITION_CONTEXT_SR_STORAGE: '1.2.840.10008.5.1.4.1.1.88.71'
};
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
@@ -46,6 +47,8 @@ const OHIFDicomHtmlSopClassHandler = {
SeriesTime,
SeriesNumber,
authorizationHeaders,
sopClassUids: sopClassUIDs,
images: series._instances,
};
},
};
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.1.1...@ohif/extension-dicom-pdf@1.1.2) (2022-01-29)
**Note:** Version bump only for package @ohif/extension-dicom-pdf
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.1.0...@ohif/extension-dicom-pdf@1.1.1) (2021-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-pdf
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.6...@ohif/extension-dicom-pdf@1.1.0) (2021-09-23)
### Features
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
## [1.0.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.5...@ohif/extension-dicom-pdf@1.0.6) (2020-09-10)
**Note:** Version bump only for package @ohif/extension-dicom-pdf
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-pdf",
"version": "1.0.6",
"version": "1.1.2",
"description": "OHIF extension for Dicom PDF",
"author": "OHIF",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dicom-parser": "^1.8.3",
"dicom-parser": "^1.8.11",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
+3 -1
View File
@@ -33,6 +33,7 @@ class DicomPDFViewport extends Component {
static propTypes = {
byteArray: TypedArrayProp.uint8,
rawPdf: PropTypes.bool,
useNative: PropTypes.bool,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
@@ -45,7 +46,8 @@ class DicomPDFViewport extends Component {
};
async componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
const { rawPdf } = this.props
const dataSet = !rawPdf && this.parseByteArray(this.props.byteArray);
const fileURL = this.getPDFFileUrl(dataSet, this.props.byteArray);
this.setState(state => ({ ...state, fileURL }));
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import OHIF from '@ohif/core';
import OHIFComponentPlugin from './OHIFComponentPlugin.js';
import DicomPDFViewport from './DicomPDFViewport';
import { str2ab } from '@ohif/core';
const { DicomLoaderService } = OHIF.utils;
@@ -18,6 +19,7 @@ class OHIFDicomPDFViewport extends Component {
state = {
byteArray: null,
rawPdf: false,
error: null,
};
@@ -34,6 +36,15 @@ class OHIFDicomPDFViewport extends Component {
componentDidMount() {
const { displaySet, studies } = this.props.viewportData;
if (displaySet.metadata && displaySet.metadata.EncapsulatedDocument) {
const { InlineBinary, BulkDataURI } = displaySet.metadata.EncapsulatedDocument;
if (InlineBinary) {
const inlineBinaryData = atob(InlineBinary);
const byteArray = str2ab(inlineBinaryData);
this.setState({ byteArray, rawPdf: true });
return;
}
}
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => this.setState({ byteArray: new Uint8Array(data) }),
error => {
@@ -49,15 +60,16 @@ class OHIFDicomPDFViewport extends Component {
viewportIndex,
activeViewportIndex,
} = this.props;
const { byteArray, error } = this.state;
const { byteArray, error, rawPdf } = this.state;
const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy };
return (
<OHIFComponentPlugin {...pluginProps}>
{byteArray && (
{(byteArray) && (
<DicomPDFViewport
byteArray={byteArray}
rawPdf={rawPdf}
setViewportActive={setViewportActive}
viewportIndex={viewportIndex}
activeViewportIndex={activeViewportIndex}
+75
View File
@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.7.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.7...@ohif/extension-dicom-rt@0.7.8) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.7.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.6...@ohif/extension-dicom-rt@0.7.7) (2022-02-02)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.5...@ohif/extension-dicom-rt@0.7.6) (2022-01-20)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.7.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.4...@ohif/extension-dicom-rt@0.7.5) (2022-01-19)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.7.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.3...@ohif/extension-dicom-rt@0.7.4) (2021-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.2...@ohif/extension-dicom-rt@0.7.3) (2021-10-26)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.1...@ohif/extension-dicom-rt@0.7.2) (2021-10-06)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.0...@ohif/extension-dicom-rt@0.7.1) (2021-09-28)
**Note:** Version bump only for package @ohif/extension-dicom-rt
# [0.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.14...@ohif/extension-dicom-rt@0.7.0) (2021-09-23)
### Features
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
## [0.6.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.13...@ohif/extension-dicom-rt@0.6.14) (2021-09-03)
**Note:** Version bump only for package @ohif/extension-dicom-rt
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-rt",
"version": "0.6.14",
"version": "0.7.8",
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
"author": "OHIF",
"license": "MIT",
@@ -29,9 +29,9 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.20.1",
"dcmjs": "0.18.8",
"cornerstone-core": "^2.6.1",
"cornerstone-tools": "^6.0.6",
"dcmjs": "0.19.4",
"gl-matrix": "^3.3.0",
"prop-types": "^15.6.2",
"react": "^16.8.6",
@@ -81,13 +81,10 @@ const RTPanel = ({
useEffect(() => {
updateStructureSets();
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
document.addEventListener('rt-panel-tab-updated', updateStructureSets);
return () => {
document.removeEventListener(
'extensiondicomrtrtloaded',
updateStructureSets
);
document.removeEventListener('rt-panel-tab-updated', updateStructureSets);
};
}, []);
+7 -8
View File
@@ -91,17 +91,12 @@ export default {
}
if (activeViewport) {
const study = studies.find(
s => s.StudyInstanceUID === activeViewport.StudyInstanceUID
);
const ds = study.displaySets.find(
ds =>
ds.displaySetInstanceUID ===
activeViewport.displaySetInstanceUID
);
const studyMetadata = studyMetadataManager.get(
activeViewport.StudyInstanceUID
);
if (!studyMetadata) {
return;
}
const referencedDisplaySets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
Modality: 'RTSTRUCT',
@@ -112,6 +107,10 @@ export default {
['RTSTRUCT'].includes(ds.Modality)
)
) {
triggerRTPanelUpdatedEvent({
badgeNumber: referencedDisplaySets.length,
target: 'rt-panel',
});
return false;
}
}
@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.7.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.7...@ohif/extension-dicom-segmentation@0.7.8) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.7.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.6...@ohif/extension-dicom-segmentation@0.7.7) (2022-02-02)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.5...@ohif/extension-dicom-segmentation@0.7.6) (2022-01-20)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.7.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.4...@ohif/extension-dicom-segmentation@0.7.5) (2021-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.7.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.3...@ohif/extension-dicom-segmentation@0.7.4) (2021-10-27)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.2...@ohif/extension-dicom-segmentation@0.7.3) (2021-10-26)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.1...@ohif/extension-dicom-segmentation@0.7.2) (2021-10-06)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.0...@ohif/extension-dicom-segmentation@0.7.1) (2021-09-28)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
# [0.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.11...@ohif/extension-dicom-segmentation@0.7.0) (2021-09-23)
### Features
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
## [0.6.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.10...@ohif/extension-dicom-segmentation@0.6.11) (2021-09-03)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-segmentation",
"version": "0.6.11",
"version": "0.7.8",
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
"author": "OHIF",
"license": "MIT",
@@ -29,9 +29,9 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.20.1",
"dcmjs": "0.18.8",
"cornerstone-core": "^2.6.1",
"cornerstone-tools": "^6.0.6",
"dcmjs": "0.19.4",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
@@ -24,6 +24,7 @@ const SegmentItem = ({
onClick,
itemClass,
color,
labelmap3D,
visible,
onVisibilityChange,
}) => {
@@ -39,7 +40,7 @@ const SegmentItem = ({
event.stopPropagation();
const newVisibility = !isVisible;
setIsVisible(newVisibility);
onVisibilityChange(newVisibility, index);
onVisibilityChange(newVisibility, index, labelmap3D);
};
return (
@@ -431,23 +431,43 @@ const SegmentationPanel = ({
return enabledElements[activeIndex].element;
};
const onSegmentVisibilityChangeHandler = (isVisible, segmentNumber) => {
/** Get all labelmaps with this segmentNumber (overlapping segments) */
const { labelmaps3D } = getBrushStackState();
const possibleLabelMaps3D = labelmaps3D.filter(({ labelmaps2D }) => {
return labelmaps2D.some(({ segmentsOnLabelmap }) =>
segmentsOnLabelmap.includes(segmentNumber)
);
});
const onSegmentVisibilityChangeHandler = (
isVisible,
segmentNumber,
labelmap3D
) => {
let segmentsHidden = [];
possibleLabelMaps3D.forEach(labelmap3D => {
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
if (labelmap3D.metadata.hasOverlapping) {
/** Get all labelmaps with this segmentNumber and that
* are from the same series (overlapping segments) */
const { labelmaps3D } = getBrushStackState();
segmentsHidden = [
...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]),
];
});
const sameSeriesLabelMaps3D = labelmaps3D.filter(({ metadata }) => {
return (
labelmap3D.metadata.segmentationSeriesInstanceUID ===
metadata.segmentationSeriesInstanceUID
);
});
const possibleLabelMaps3D = sameSeriesLabelMaps3D.filter(
({ labelmaps2D }) => {
return labelmaps2D.some(({ segmentsOnLabelmap }) =>
segmentsOnLabelmap.includes(segmentNumber)
);
}
);
possibleLabelMaps3D.forEach(labelmap3D => {
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
segmentsHidden = [
...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]),
];
});
} else {
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
segmentsHidden = [...labelmap3D.segmentsHidden];
}
setState(state => ({ ...state, segmentsHidden }));
@@ -514,6 +534,7 @@ const SegmentationPanel = ({
label={segmentLabel}
index={segmentNumber}
color={color}
labelmap3D={labelmap3D}
visible={!labelmap3D.segmentsHidden[segmentIndex]}
onVisibilityChange={onSegmentVisibilityChangeHandler}
/>
@@ -64,8 +64,17 @@ export default function getSopClassHandlerModule({ servicesManager }) {
metadata,
};
segDisplaySet.getSourceDisplaySet = function(studies, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
return getSourceDisplaySet(studies, segDisplaySet, activateLabelMap, onDisplaySetLoadFailureHandler);
segDisplaySet.getSourceDisplaySet = function(
studies,
activateLabelMap = true,
onDisplaySetLoadFailureHandler
) {
return getSourceDisplaySet(
studies,
segDisplaySet,
activateLabelMap,
onDisplaySetLoadFailureHandler
);
};
segDisplaySet.load = async function(referencedDisplaySet, studies) {
@@ -98,6 +107,9 @@ export default function getSopClassHandlerModule({ servicesManager }) {
if (labelmapBufferArray.length > 1) {
let labelmapIndexes = [];
for (let i = 0; i < labelmapBufferArray.length; ++i) {
segMetadata.segmentationSeriesInstanceUID =
segDisplaySet.SeriesInstanceUID;
segMetadata.hasOverlapping = true;
labelmapIndexes.push(
await loadSegmentation(
imageIds,
@@ -118,6 +130,9 @@ export default function getSopClassHandlerModule({ servicesManager }) {
labelmapIndex = labelmapIndexes[0];
console.warn('Overlapping segments!');
} else {
segMetadata.segmentationSeriesInstanceUID =
segDisplaySet.SeriesInstanceUID;
segMetadata.hasOverlapping = false;
labelmapIndex = await loadSegmentation(
imageIds,
segDisplaySet,
@@ -135,10 +150,16 @@ export default function getSopClassHandlerModule({ servicesManager }) {
}
function _parseSeg(arrayBuffer, imageIds) {
const skipOverlapping = false;
const tolerance = 1e-2;
const cornerstoneToolsVersion = 4;
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
imageIds,
arrayBuffer,
cornerstone.metaData
cornerstone.metaData,
skipOverlapping,
tolerance,
cornerstoneToolsVersion
);
}
+18 -1
View File
@@ -126,7 +126,7 @@ export default {
label: 'Segmentations',
target: 'segmentation-panel',
stateEvent: SegmentationPanelTabUpdatedEvent,
isDisabled: studies => {
isDisabled: (studies, activeViewport) => {
if (!studies) {
return true;
}
@@ -139,6 +139,23 @@ export default {
const series = study.series[j];
if (series.Modality === 'SEG') {
if (activeViewport) {
const studyMetadata = studyMetadataManager.get(
activeViewport.StudyInstanceUID
);
if (!studyMetadata) {
return;
}
const referencedDS = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID:
activeViewport.SeriesInstanceUID,
Modality: 'SEG',
});
triggerSegmentationPanelTabUpdatedEvent({
badgeNumber: referencedDS.length,
target: 'segmentation-panel',
});
}
return false;
}
}
+64
View File
@@ -3,6 +3,70 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.18...@ohif/extension-dicom-tag-browser@0.2.19) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.17...@ohif/extension-dicom-tag-browser@0.2.18) (2022-02-02)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.16...@ohif/extension-dicom-tag-browser@0.2.17) (2022-01-20)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.15...@ohif/extension-dicom-tag-browser@0.2.16) (2021-12-10)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.14...@ohif/extension-dicom-tag-browser@0.2.15) (2021-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.13...@ohif/extension-dicom-tag-browser@0.2.14) (2021-11-30)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.12...@ohif/extension-dicom-tag-browser@0.2.13) (2021-10-26)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.11...@ohif/extension-dicom-tag-browser@0.2.12) (2021-09-28)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.10...@ohif/extension-dicom-tag-browser@0.2.11) (2021-06-03)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-tag-browser",
"version": "0.2.11",
"version": "0.2.19",
"description": "OHIF extension for checking DICOM headers.",
"author": "OHIF",
"license": "MIT",
@@ -28,12 +28,12 @@
},
"peerDependencies": {
"@ohif/core": "^2.6.0",
"dcmjs": "0.18.8",
"dcmjs": "0.19.4",
"react": "^16.8.6"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"dicomweb-client": "^0.8.1",
"dicomweb-client": "^0.8.3",
"moment": "2.24.0",
"react-select": "^3.0.8"
}
@@ -18,7 +18,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
activeDisplaySetInstanceUID,
setActiveDisplaySetInstanceUID,
] = useState(displaySetInstanceUID);
const [activeInstance, setActiveInstance] = useState(0);
const [activeInstance, setActiveInstance] = useState(1);
const [tags, setTags] = useState([]);
const [meta, setMeta] = useState('');
const [instanceList, setInstanceList] = useState([]);
@@ -51,20 +51,21 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
description: displayDate,
onClick: () => {
setActiveDisplaySetInstanceUID(displaySetInstanceUID);
setActiveInstance(0);
setActiveInstance(1);
},
};
});
let metadata;
const isImageStack = activeDisplaySet instanceof ImageSet;
const isImageStack =
activeDisplaySet instanceof ImageSet &&
activeDisplaySet.isModalitySupported === true;
let selectedInstanceValue;
let instanceList;
if (isImageStack) {
const { images } = activeDisplaySet;
const image = images[activeInstance];
const image = images[activeInstance - 1];
instanceList = images.map((image, index) => {
const metadata = image.getData().metadata;
@@ -91,7 +92,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
setInstanceList(instanceList);
setDisplaySetList(newDisplaySetList);
setIsImageStack(isImageStack);
}, [activeDisplaySetInstanceUID, activeInstance]);
}, [activeDisplaySetInstanceUID, activeInstance, displaySets]);
const selectedDisplaySetValue = displaySetList.find(
ds => ds.value === activeDisplaySetInstanceUID
@@ -106,7 +107,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
showValue
step={1}
min={1}
max={instanceList.length - 1}
max={instanceList.length}
value={activeInstance}
valueRenderer={value => <p>Instance Number: {value}</p>}
onChange={({ target }) => {
+155
View File
@@ -3,6 +3,161 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.15...@ohif/extension-vtk@1.12.16) (2022-02-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.14...@ohif/extension-vtk@1.12.15) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.13...@ohif/extension-vtk@1.12.14) (2022-02-03)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.12...@ohif/extension-vtk@1.12.13) (2022-02-02)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.11...@ohif/extension-vtk@1.12.12) (2022-01-29)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.10...@ohif/extension-vtk@1.12.11) (2022-01-20)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.9...@ohif/extension-vtk@1.12.10) (2022-01-13)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.8...@ohif/extension-vtk@1.12.9) (2021-12-23)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.7...@ohif/extension-vtk@1.12.8) (2021-12-23)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.6...@ohif/extension-vtk@1.12.7) (2021-12-02)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.5...@ohif/extension-vtk@1.12.6) (2021-11-30)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.4...@ohif/extension-vtk@1.12.5) (2021-10-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.3...@ohif/extension-vtk@1.12.4) (2021-10-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.2...@ohif/extension-vtk@1.12.3) (2021-10-04)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.1...@ohif/extension-vtk@1.12.2) (2021-09-28)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.12.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.0...@ohif/extension-vtk@1.12.1) (2021-09-24)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.18...@ohif/extension-vtk@1.12.0) (2021-09-23)
### Features
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
## [1.11.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.17...@ohif/extension-vtk@1.11.18) (2021-09-13)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.16...@ohif/extension-vtk@1.11.17) (2021-09-09)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.15...@ohif/extension-vtk@1.11.16) (2021-09-03)
**Note:** Version bump only for package @ohif/extension-vtk
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.11.16",
"version": "1.12.16",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -32,11 +32,11 @@
"@ohif/core": "^0.50.0",
"@ohif/i18n": "^0.50.0",
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.20.1",
"cornerstone-wado-image-loader": "^3.1.0",
"dcmjs": "0.18.8",
"dicom-parser": "^1.8.3",
"cornerstone-core": "^2.6.1",
"cornerstone-tools": "^6.0.6",
"cornerstone-wado-image-loader": "^4.1.0",
"dcmjs": "0.19.4",
"dicom-parser": "^1.8.11",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
"prop-types": "^15.7.2",
@@ -50,14 +50,14 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"lodash.throttle": "^4.1.1",
"react-vtkjs-viewport": "^0.14.2"
"react-vtkjs-viewport": "^0.14.4"
},
"devDependencies": {
"@ohif/core": "^2.13.15",
"@ohif/ui": "^1.9.3",
"cornerstone-tools": "^4.20.1",
"cornerstone-wado-image-loader": "^3.1.0",
"dicom-parser": "^1.8.3",
"@ohif/core": "^2.16.12",
"@ohif/ui": "^1.10.6",
"cornerstone-tools": "^6.0.6",
"cornerstone-wado-image-loader": "^4.1.0",
"dicom-parser": "^1.8.11",
"gh-pages": "^2.0.1",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
+23
View File
@@ -0,0 +1,23 @@
0 silly argv {
0 silly argv _: [ 'run' ],
0 silly argv stream: true,
0 silly argv lernaVersion: '3.18.3',
0 silly argv '$0': 'node_modules/.bin/lerna',
0 silly argv script: 'dev:viewer'
0 silly argv }
1 notice cli v3.18.3
2 verbose rootPath /home/davide/Development/OIHF/Viewers
3 info versioning independent
4 info Executing command in 1 package: "yarn run dev:viewer"
5 silly npmRunScript.stream [ 'dev:viewer', [], '@ohif/viewer' ]
6 silly getExecOpts /home/davide/Development/OIHF/Viewers/platform/viewer undefined
7 error MaxBufferError: stderr maxBuffer exceeded
7 error at PassThrough.<anonymous> (/home/davide/Development/OIHF/Viewers/node_modules/get-stream/index.js:41:19)
7 error at PassThrough.emit (events.js:412:35)
7 error at addChunk (internal/streams/readable.js:290:12)
7 error at readableAddChunk (internal/streams/readable.js:261:11)
7 error at PassThrough.Readable.push (internal/streams/readable.js:204:10)
7 error at PassThrough.Transform.push (internal/streams/transform.js:166:32)
7 error at PassThrough.afterTransform (internal/streams/transform.js:101:10)
7 error at PassThrough._transform (internal/streams/passthrough.js:46:3)
7 error at PassThrough.Transform._read (internal/streams/transform.js:205:10)
+4 -2
View File
@@ -56,6 +56,7 @@
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@percy/cypress": "^2.2.0",
"@webpack-cli/serve": "^1.6.1",
"babel-eslint": "9.x",
"babel-loader": "^8.0.6",
"babel-plugin-inline-react-svg": "1.1.0",
@@ -104,8 +105,8 @@
"stylus-loader": "^3.0.2",
"terser-webpack-plugin": "^2.1.0",
"webpack": "^4.35.2",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.3",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.1",
"workbox-webpack-plugin": "^5.0.0-beta.1",
@@ -123,6 +124,7 @@
]
},
"resolutions": {
"**/node-gyp": "8.4.1",
"**/@babel/runtime": "7.5.5"
}
}
+151
View File
@@ -3,6 +3,157 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.16.12](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.11...@ohif/core@2.16.12) (2022-02-15)
**Note:** Version bump only for package @ohif/core
## [2.16.11](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.10...@ohif/core@2.16.11) (2022-02-03)
**Note:** Version bump only for package @ohif/core
## [2.16.10](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.9...@ohif/core@2.16.10) (2022-02-03)
**Note:** Version bump only for package @ohif/core
## [2.16.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.8...@ohif/core@2.16.9) (2022-02-02)
**Note:** Version bump only for package @ohif/core
## [2.16.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.7...@ohif/core@2.16.8) (2022-01-29)
**Note:** Version bump only for package @ohif/core
## [2.16.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.6...@ohif/core@2.16.7) (2022-01-20)
**Note:** Version bump only for package @ohif/core
## [2.16.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.5...@ohif/core@2.16.6) (2022-01-13)
**Note:** Version bump only for package @ohif/core
## [2.16.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.4...@ohif/core@2.16.5) (2021-12-23)
### Bug Fixes
* DICOMFileLoadingListener typo in method name ([#2647](https://github.com/OHIF/Viewers/issues/2647)) ([761d54f](https://github.com/OHIF/Viewers/commit/761d54fbbfb376c20ee15794ef7bb7111f64a06f))
## [2.16.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.3...@ohif/core@2.16.4) (2021-12-23)
### Bug Fixes
* SeriesTime and SeriesDate are only set for the first series and undefined for the remaining ones ([#2476](https://github.com/OHIF/Viewers/issues/2476)) ([6a401c9](https://github.com/OHIF/Viewers/commit/6a401c9e5204cb4f7af6aa33190ad5a8d0879de6))
## [2.16.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.2...@ohif/core@2.16.3) (2021-12-02)
**Note:** Version bump only for package @ohif/core
## [2.16.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.1...@ohif/core@2.16.2) (2021-11-30)
**Note:** Version bump only for package @ohif/core
## [2.16.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.0...@ohif/core@2.16.1) (2021-10-26)
**Note:** Version bump only for package @ohif/core
# [2.16.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.15.1...@ohif/core@2.16.0) (2021-10-04)
### Features
* Added support for WADO servers built with static hosting (e.g. S3) ([#2566](https://github.com/OHIF/Viewers/issues/2566)) ([a046c6c](https://github.com/OHIF/Viewers/commit/a046c6c9017a84e5ead71d5113b5e892333b4130))
## [2.15.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.15.0...@ohif/core@2.15.1) (2021-09-28)
**Note:** Version bump only for package @ohif/core
# [2.15.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.14.1...@ohif/core@2.15.0) (2021-09-23)
### Features
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
## [2.14.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.14.0...@ohif/core@2.14.1) (2021-09-13)
**Note:** Version bump only for package @ohif/core
# [2.14.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.15...@ohif/core@2.14.0) (2021-09-09)
### Features
* OHIF-1000: Study prefetcher ([#2466](https://github.com/OHIF/Viewers/issues/2466)) ([8f26045](https://github.com/OHIF/Viewers/commit/8f2604509d24092b721f1333289d838820d68cac))
## [2.13.15](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.14...@ohif/core@2.13.15) (2021-09-03)
**Note:** Version bump only for package @ohif/core
+9 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.13.15",
"version": "2.16.12",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -31,20 +31,21 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
},
"peerDependencies": {
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.20.1",
"cornerstone-wado-image-loader": "^3.1.0",
"dicom-parser": "^1.8.3"
"cornerstone-core": "^2.6.1",
"cornerstone-tools": "^6.0.6",
"cornerstone-wado-image-loader": "^4.1.0",
"dicom-parser": "^1.8.11"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"ajv": "^6.10.0",
"dcmjs": "0.18.8",
"dicomweb-client": "^0.8.1",
"immer": "6.0.2",
"dcmjs": "0.19.4",
"dicomweb-client": "^0.8.3",
"immer": "9.0.12",
"isomorphic-base64": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
"lodash.merge": "^4.6.1",
"mathjs": "^10.1.0",
"mousetrap": "^1.6.3",
"retry": "^0.12.0",
"validate.js": "^0.12.0"
@@ -0,0 +1,8 @@
export default {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
POLYGON: 'POLYGON',
};
@@ -0,0 +1,5 @@
const TOOL_NAMES = {
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
};
export default TOOL_NAMES;
@@ -0,0 +1,29 @@
export const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
ImagingMeasurements: '126010',
MeasurementGroup: '125007',
ImageLibraryGroup: '126200',
TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4
Score: '246262008',
};
export const RELATIONSHIP_TYPE = {
INFERRED_FROM: 'INFERRED FROM',
SELECTED_FROM: 'SELECTED FROM',
};
export const CodingSchemeDesignators = {
SRT: 'SRT',
cornerstoneTools4: 'CST4',
};
export default {
CodeNameCodeSequenceValues,
RELATIONSHIP_TYPE,
CodingSchemeDesignators,
};
@@ -0,0 +1,155 @@
import { ImageSet } from '../../classes';
import getMeasurements from './utils/getMeasurements';
import getReferencedImagesList from './utils/getReferencedImagesList';
import isRehydratable from './utils/isRehydratable';
import addMeasurement from './utils/addMeasurement';
const parseSCOORD3D = ({ servicesManager, displaySets }) => {
const { MeasurementService } = servicesManager.services;
const srDisplaySets = displaySets.filter(ds => ds.Modality === 'SR');
srDisplaySets.forEach(srDisplaySet => {
const firstInstance = srDisplaySet.metadata;
if (!firstInstance) {
return;
}
const { ContentSequence } = firstInstance;
srDisplaySet.referencedImages = getReferencedImagesList(ContentSequence);
srDisplaySet.measurements = getMeasurements(ContentSequence, srDisplaySet);
const mappings = MeasurementService.getSourceMappings(
'CornerstoneTools',
'4'
);
srDisplaySet.isHydrated = false;
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
srDisplaySet.isLoaded = true;
const imageDisplaySets = displaySets.filter(
ds =>
ds.Modality !== 'SR' &&
ds.Modality !== 'SEG' &&
ds.Modality !== 'RTSTRUCT' &&
ds.Modality !== 'RTDOSE'
);
imageDisplaySets.forEach(imageDisplaySet => {
// Check currently added displaySets and add measurements if the sources exist.
checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, imageDisplaySet);
});
});
};
const checkIfCanAddMeasurementsToDisplaySet = (
srDisplaySet,
imageDisplaySet
) => {
let measurements = srDisplaySet.measurements;
/**
* Look for image sets.
* This also filters out _this_ displaySet, as it is not an image set.
*/
if (!(imageDisplaySet instanceof ImageSet)) {
return;
}
const { sopClassUIDs, images } = imageDisplaySet;
/**
* Filter measurements that references the correct sop class.
*/
measurements = measurements.filter(measurement => {
return measurement.coords.some(coord => {
if (coord.ReferencedSOPSequence === undefined) {
/** we miss the referenced information. We can compare the annotation SCOORD3D coordinates with
* the ImagePatientPosition of the frames. However (WARNING!!!),
* if more than a source series is present, this logic can find the wrong frame
* (i.e. two source series, with the same frameOfReferenceUID,
* that have each a frame with the same ImagePositionPatient of the annotation 3D coordinates)
*/
for (let i = 0; i < images.length; ++i) {
const imageMetadata = images[i].getData().metadata;
if (
imageMetadata.FrameOfReferenceUID !==
coord.ReferencedFrameOfReferenceSequence
) {
continue;
}
let sliceNormal = [0, 0, 0];
const orientation = imageMetadata.ImageOrientationPatient;
sliceNormal[0] =
orientation[1] * orientation[5] - orientation[2] * orientation[4];
sliceNormal[1] =
orientation[2] * orientation[3] - orientation[0] * orientation[5];
sliceNormal[2] =
orientation[0] * orientation[4] - orientation[1] * orientation[3];
let distanceAlongNormal = 0;
for (let j = 0; j < 3; ++j) {
distanceAlongNormal +=
sliceNormal[j] * imageMetadata.ImagePositionPatient[j];
}
// assuming 1 mm tolerance
if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) {
continue;
}
coord.ReferencedSOPSequence = {
ReferencedSOPClassUID: imageMetadata.SOPClassUID,
ReferencedSOPInstanceUID: imageMetadata.SOPInstanceUID,
};
break;
}
if (coord.ReferencedSOPSequence === undefined) {
return false;
}
}
return sopClassUIDs.includes(
coord.ReferencedSOPSequence.ReferencedSOPClassUID
);
});
});
/**
* New display set doesn't have measurements that references the correct sop class.
*/
if (measurements.length === 0) {
return;
}
const imageIds = images.map(i => i.getImageId());
const SOPInstanceUIDs = images.map(i => i.SOPInstanceUID);
measurements.forEach(measurement => {
const { coords } = measurement;
coords.forEach(coord => {
if (coord.ReferencedSOPSequence !== undefined) {
const imageIndex = SOPInstanceUIDs.findIndex(
SOPInstanceUID =>
SOPInstanceUID ===
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID
);
if (imageIndex > -1) {
const imageId = imageIds[imageIndex];
const imageMetadata = images[imageIndex].getData().metadata;
addMeasurement(
measurement,
imageId,
imageMetadata,
imageDisplaySet.displaySetInstanceUID
);
}
}
});
});
};
export default parseSCOORD3D;
@@ -0,0 +1,102 @@
import csTools from 'cornerstone-tools';
import OHIF from '../../../';
/** Internal imports */
import TOOL_NAMES from '../constants/toolNames';
import getRenderableData from './getRenderableData';
const globalImageIdSpecificToolStateManager =
csTools.globalImageIdSpecificToolStateManager;
/**
* Add a measurement to a display set.
*
* @param {*} measurement
* @param {*} imageId
* @param {*} displaySetInstanceUID
*/
export default function addMeasurement(
measurement,
imageId,
imageMetadata,
displaySetInstanceUID
) {
// TODO -> Render rotated ellipse .
const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
const measurementData = {
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
renderableData: {},
labels: measurement.labels,
};
measurement.coords.forEach(coord => {
const { GraphicType, GraphicData, ValueType } = coord;
if (measurementData.renderableData[GraphicType] === undefined) {
measurementData.renderableData[GraphicType] = [];
}
measurementData.renderableData[GraphicType].push(
getRenderableData(GraphicType, GraphicData, ValueType, imageMetadata)
);
});
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
if (toolState[imageId] === undefined) {
toolState[imageId] = {};
}
const imageIdToolState = toolState[imageId];
// If we don't have tool state for this type of tool, add an empty object
if (imageIdToolState[toolName] === undefined) {
imageIdToolState[toolName] = {
data: [],
};
}
const toolData = imageIdToolState[toolName];
measurementData.description = `Read-only annotation`;
measurementData.isReadOnly = true;
toolData.data.push(measurementData);
addToMeasurementApi({ measurementData, toolName, imageId });
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = displaySetInstanceUID;
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
// It'd be super werid if it didn't anyway as a SCOORD.
measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
return measurement;
}
const addToMeasurementApi = ({ measurementData, toolName, imageId }) => {
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const toolType = toolName;
const collection = measurementApi.tools[toolType];
if (!collection) return;
if (!measurementData || measurementData.cancelled) return;
const imageAttributes = OHIF.measurements.getImageAttributes(null, imageId);
const measurement = Object.assign({}, measurementData, imageAttributes, {
lesionNamingNumber: measurementData.lesionNamingNumber,
userId: OHIF.user.getUserId(),
toolType,
});
const addedMeasurement = measurementApi.addMeasurement(toolType, measurement);
Object.assign(measurementData, addedMeasurement);
const measurementLabel = OHIF.measurements.getLabel(measurementData);
if (measurementLabel) {
measurementData.labels = [measurementLabel];
}
};
@@ -0,0 +1,31 @@
const getCoordsFromSCOORDOrSCOORD3D = (graphicItem, displaySet) => {
const { ValueType, RelationshipType, GraphicType, GraphicData } = graphicItem;
// if (RelationshipType !== RELATIONSHIP_TYPE.INFERRED_FROM) {
// console.warn(
// `Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM."`
// );
// return;
// }
const coords = { ValueType, GraphicType, GraphicData };
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
if (ValueType === 'SCOORD') {
const { ReferencedSOPSequence } = graphicItem.ContentSequence;
coords.ReferencedSOPSequence = ReferencedSOPSequence;
} else if (ValueType === 'SCOORD3D') {
if (graphicItem.ReferencedFrameOfReferenceUID) {
coords.ReferencedFrameOfReferenceSequence = graphicItem.ReferencedFrameOfReferenceUID;
} else if (graphicItem.ContentSequence) {
const {
ReferencedFrameOfReferenceSequence,
} = graphicItem.ContentSequence;
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
}
}
return coords;
};
export default getCoordsFromSCOORDOrSCOORD3D;
@@ -0,0 +1,19 @@
const getLabelFromMeasuredValueSequence = (
ConceptNameCodeSequence,
MeasuredValueSequence
) => {
const { CodeMeaning } = ConceptNameCodeSequence;
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence;
const formatedNumericValue = NumericValue
? Number(NumericValue).toFixed(1)
: '';
return {
label: CodeMeaning,
value: `${formatedNumericValue} ${CodeValue}`,
}; // E.g. Long Axis: 31.0 mm
};
export default getLabelFromMeasuredValueSequence;
@@ -0,0 +1,47 @@
import { CodeNameCodeSequenceValues } from '../enums';
import getSequenceAsArray from './getSequenceAsArray';
import getMergedContentSequencesByTrackingUniqueIdentifiers from './getMergedContentSequencesByTrackingUniqueIdentifiers';
import processMeasurement from './processMeasurement';
const getMeasurements = (
ImagingMeasurementReportContentSequence,
displaySet
) => {
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.ImagingMeasurements
);
const MeasurementGroups = getSequenceAsArray(
ImagingMeasurements.ContentSequence
).filter(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.MeasurementGroup
);
const mergedContentSequencesByTrackingUniqueIdentifiers = getMergedContentSequencesByTrackingUniqueIdentifiers(
MeasurementGroups
);
let measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
trackingUniqueIdentifier => {
const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
];
const measurement = processMeasurement(mergedContentSequence, displaySet);
if (measurement) {
measurements.push(measurement);
}
}
);
return measurements;
};
export default getMeasurements;
@@ -0,0 +1,54 @@
import getSequenceAsArray from './getSequenceAsArray';
import { CodeNameCodeSequenceValues } from '../enums';
const getMergedContentSequencesByTrackingUniqueIdentifiers = MeasurementGroups => {
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
MeasurementGroups.forEach(MeasurementGroup => {
const ContentSequence = getSequenceAsArray(
MeasurementGroup.ContentSequence
);
const TrackingUniqueIdentifierItem = ContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
);
if (!TrackingUniqueIdentifierItem) {
console.warn(
'No Tracking Unique Identifier, skipping ambiguous measurement.'
);
}
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
if (
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
] === undefined
) {
// Add the full ContentSequence
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
] = [...ContentSequence];
} else {
// Add the ContentSequence minus the tracking identifier, as we have this
// Information in the merged ContentSequence anyway.
ContentSequence.forEach(item => {
if (
item.ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
) {
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
].push(item);
}
});
}
});
return mergedContentSequencesByTrackingUniqueIdentifiers;
};
export default getMergedContentSequencesByTrackingUniqueIdentifiers;
@@ -0,0 +1,43 @@
import getSequenceAsArray from './getSequenceAsArray';
import { CodeNameCodeSequenceValues } from '../enums';
const getReferencedImagesList = ImagingMeasurementReportContentSequence => {
const referencedImages = [];
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.ImageLibrary
);
if (!ImageLibrary.ContentSequence) {
return referencedImages;
}
const ImageLibraryGroup = getSequenceAsArray(
ImageLibrary.ContentSequence
).find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.ImageLibraryGroup
);
getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
const { ReferencedSOPSequence } = item;
if (ReferencedSOPSequence) {
const {
ReferencedSOPClassUID,
ReferencedSOPInstanceUID,
} = ReferencedSOPSequence;
referencedImages.push({
ReferencedSOPClassUID,
ReferencedSOPInstanceUID,
});
}
});
return referencedImages;
};
export default getReferencedImagesList;
@@ -0,0 +1,192 @@
import csMath from 'cornerstone-math';
import SCOORD_TYPES from '../constants/scoordTypes';
import { inv } from 'mathjs';
const getRenderableData = (
GraphicType,
GraphicData,
ValueType,
imageMetadata
) => {
let renderableData;
const orientation = imageMetadata.ImageOrientationPatient;
const position = imageMetadata.ImagePositionPatient;
const pixelSpacing = imageMetadata.PixelSpacing;
const sliceSpacing = imageMetadata.SliceThickness
? imageMetadata.SliceThickness
: 1;
// https://nipy.org/nibabel/dicom/dicom_orientation.html
const M = [
[
orientation[0] * pixelSpacing[0],
orientation[3] * pixelSpacing[1],
sliceSpacing,
position[0],
],
[
orientation[1] * pixelSpacing[0],
orientation[4] * pixelSpacing[1],
sliceSpacing,
position[1],
],
[
orientation[2] * pixelSpacing[0],
orientation[5] * pixelSpacing[1],
sliceSpacing,
position[2],
],
[0, 0, 0, 1],
];
// we need to go from 3D to pixel (cornerstone2D works in pixel coordinates),
// we take the inverse.
const M1 = inv(M);
const worldToIJK = (point, M1) => {
const worldPoint = {
x:
M1[0][0] * point.x + M1[0][1] * point.y + M1[0][2] * point.z + M1[0][3],
y:
M1[1][0] * point.x + M1[1][1] * point.y + M1[1][2] * point.z + M1[1][3],
z:
M1[2][0] * point.x + M1[2][1] * point.y + M1[2][2] * point.z + M1[2][3],
};
return worldPoint;
};
// https://dicom.innolitics.com/ciods/procedure-log/sr-document-content/00700023
switch (GraphicType) {
case SCOORD_TYPES.POINT:
renderableData = [];
if (ValueType === 'SCOORD3D') {
for (let i = 0; i < GraphicData.length; i += 3) {
const point = {
x: GraphicData[i],
y: GraphicData[i + 1],
z: GraphicData[i + 2],
};
renderableData.push(worldToIJK(point, M1));
}
} else {
for (let i = 0; i < GraphicData.length; i += 2) {
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
}
}
break;
case SCOORD_TYPES.MULTIPOINT:
renderableData = [];
if (ValueType === 'SCOORD3D') {
for (let i = 0; i < GraphicData.length; i += 3) {
const point = {
x: GraphicData[i],
y: GraphicData[i + 1],
z: GraphicData[i + 2],
};
renderableData.push(worldToIJK(point, M1));
}
} else {
for (let i = 0; i < GraphicData.length; i += 2) {
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
}
}
break;
case SCOORD_TYPES.POLYLINE:
renderableData = [];
if (ValueType === 'SCOORD3D') {
for (let i = 0; i < GraphicData.length; i += 3) {
const point = {
x: GraphicData[i],
y: GraphicData[i + 1],
z: GraphicData[i + 2],
};
renderableData.push(worldToIJK(point, M1));
}
} else {
for (let i = 0; i < GraphicData.length; i += 2) {
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
}
}
break;
case SCOORD_TYPES.POLYGON:
// this is only scoord3d
renderableData = [];
for (let i = 0; i < GraphicData.length; i += 3) {
const point = {
x: GraphicData[i],
y: GraphicData[i + 1],
z: GraphicData[i + 2],
};
renderableData.push(worldToIJK(point, M1));
}
break;
case SCOORD_TYPES.CIRCLE: {
// this is only scoord
const center = { x: GraphicData[0], y: GraphicData[1] };
const onPerimeter = { x: GraphicData[2], y: GraphicData[3] };
const radius = csMath.point.distance(center, onPerimeter);
renderableData = {
center,
radius,
};
break;
}
case SCOORD_TYPES.ELLIPSE: {
console.warn('ROTATED ELLIPSE NOT YET SUPPORTED!');
// To Do: scoord3d ellips, need data for testing
const majorAxis = [
{ x: GraphicData[0], y: GraphicData[1] },
{ x: GraphicData[2], y: GraphicData[3] },
];
const minorAxis = [
{ x: GraphicData[4], y: GraphicData[5] },
{ x: GraphicData[6], y: GraphicData[7] },
];
// Calculate two opposite corners of box defined by two axes.
const minorAxisLength = csMath.point.distance(minorAxis[0], minorAxis[1]);
const minorAxisDirection = {
x: (minorAxis[1].x - minorAxis[0].x) / minorAxisLength,
y: (minorAxis[1].y - minorAxis[0].y) / minorAxisLength,
};
const halfMinorAxisLength = minorAxisLength / 2;
// First end point of major axis + half minor axis vector
const corner1 = {
x: majorAxis[0].x + minorAxisDirection.x * halfMinorAxisLength,
y: majorAxis[0].y + minorAxisDirection.y * halfMinorAxisLength,
};
// Second end point of major axis - half of minor axis vector
const corner2 = {
x: majorAxis[1].x - minorAxisDirection.x * halfMinorAxisLength,
y: majorAxis[1].y - minorAxisDirection.y * halfMinorAxisLength,
};
renderableData = {
corner1,
corner2,
};
break;
}
}
return renderableData;
};
export default getRenderableData;
@@ -0,0 +1,4 @@
const getSequenceAsArray = sequence =>
Array.isArray(sequence) ? sequence : [sequence];
export default getSequenceAsArray;
@@ -0,0 +1,48 @@
import { adapters } from 'dcmjs';
const cornerstoneAdapters = adapters.Cornerstone;
/**
* Checks if the given `displaySet`can be rehydrated into the `MeasurementService`.
*
* @param {object} displaySet The SR `displaySet` to check.
* @param {object[]} mappings The CornerstoneTools 4 mappings to the `MeasurementService`.
* @returns {boolean} True if the SR can be rehydrated into the `MeasurementService`.
*/
export default function isRehydratable(displaySet, mappings) {
if (!mappings || !mappings.length) {
return false;
}
const mappingDefinitions = mappings.map(m => m.definition);
const { measurements } = displaySet;
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
adapterKey =>
typeof cornerstoneAdapters[adapterKey]
.isValidCornerstoneTrackingIdentifier === 'function'
);
const adapters = [];
adapterKeys.forEach(key => {
if (mappingDefinitions.includes(key)) {
// Must have both a dcmjs adapter and a MeasurementService
// Definition in order to be a candidate for import.
adapters.push(cornerstoneAdapters[key]);
}
});
for (let i = 0; i < measurements.length; i++) {
const TrackingIdentifier = measurements[i].TrackingIdentifier;
const hydratable = adapters.some(adapter =>
adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier)
);
if (hydratable) {
return true;
}
}
return false;
}
@@ -0,0 +1,16 @@
import processTID1410Measurement from './processTID1410Measurement';
import processNonGeometricallyDefinedMeasurement from './processNonGeometricallyDefinedMeasurement';
const processMeasurement = (mergedContentSequence, displaySet) => {
if (
mergedContentSequence.some(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
)
) {
return processTID1410Measurement(mergedContentSequence, displaySet);
}
return processNonGeometricallyDefinedMeasurement(mergedContentSequence);
};
export default processMeasurement;
@@ -0,0 +1,117 @@
import getLabelFromMeasuredValueSequence from './getLabelFromMeasuredValueSequence';
import getCoordsFromSCOORDOrSCOORD3D from './getCoordsFromSCOORDOrSCOORD3D';
import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from '../enums';
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
const processNonGeometricallyDefinedMeasurement = mergedContentSequence => {
const NUMContentItems = mergedContentSequence.filter(
group => group.ValueType === 'NUM'
);
const UIDREFContentItem = mergedContentSequence.find(
group => group.ValueType === 'UIDREF'
);
const TrackingIdentifierContentItem = mergedContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingIdentifier
);
const Finding = mergedContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.Finding
);
const FindingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.SRT &&
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite
);
const measurement = {
loaded: false,
labels: [],
coords: [],
TrackingUniqueIdentifier: UIDREFContentItem.UID,
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
};
if (
Finding &&
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.cornerstoneTools4 &&
Finding.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
value: Finding.ConceptCodeSequence.CodeMeaning,
});
}
// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
if (FindingSites.length) {
const cornerstoneFreeTextFindingSite = FindingSites.find(
FindingSite =>
FindingSite.ConceptCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.cornerstoneTools4 &&
FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
);
if (cornerstoneFreeTextFindingSite) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
});
}
}
NUMContentItems.forEach(item => {
const {
ConceptNameCodeSequence,
ContentSequence,
MeasuredValueSequence,
} = item;
if (!ContentSequence) {
console.warn(`Graphic ${ContentSequence} missing, skipping annotation.`);
return;
}
const { ValueType } = ContentSequence;
if (!ValueType === 'SCOORD' && !ValueType === 'SCOORD3D') {
console.warn(
`Graphic ${ValueType} not currently supported, skipping annotation.`
);
return;
}
const coords = getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
if (coords) {
measurement.coords.push(coords);
}
if (MeasuredValueSequence) {
measurement.labels.push(
getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
)
);
}
});
return measurement;
};
export default processNonGeometricallyDefinedMeasurement;
@@ -0,0 +1,88 @@
import getLabelFromMeasuredValueSequence from './getLabelFromMeasuredValueSequence';
import getCoordsFromSCOORDOrSCOORD3D from './getCoordsFromSCOORDOrSCOORD3D';
import { RELATIONSHIP_TYPE, CodeNameCodeSequenceValues } from '../enums';
/**
* TID 1410 Planar ROI Measurements and Qualitative Evaluations.
*
* @param {*} mergedContentSequence
* @returns
*/
const processTID1410Measurement = (mergedContentSequence, displaySet) => {
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
const graphicItem = mergedContentSequence.find(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
);
if (!graphicItem) {
console.warn(
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
);
return;
}
const UIDREFContentItem = mergedContentSequence.find(
group => group.ValueType === 'UIDREF'
);
const TrackingIdentifierContentItem = mergedContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingIdentifier
);
const NUMContentItems = mergedContentSequence.filter(
group => group.ValueType === 'NUM'
);
const measurement = {
loaded: false,
labels: [],
coords: [getCoordsFromSCOORDOrSCOORD3D(graphicItem, displaySet)],
TrackingUniqueIdentifier: UIDREFContentItem.UID,
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
};
NUMContentItems.forEach(item => {
const {
ConceptNameCodeSequence,
ContentSequence,
MeasuredValueSequence,
} = item;
if (
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.Score
) {
ContentSequence.forEach(item => {
if (
[
RELATIONSHIP_TYPE.SELECTED_FROM,
RELATIONSHIP_TYPE.INFERRED_FROM,
].includes(item.RelationshipType)
) {
if (item.ReferencedSOPSequence) {
measurement.coords.forEach(coord => {
coord.ReferencedSOPSequence = item.ReferencedSOPSequence;
});
}
}
});
}
if (MeasuredValueSequence) {
measurement.labels.push(
getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
)
);
}
});
return measurement;
};
export default processTID1410Measurement;
+3 -2
View File
@@ -19,9 +19,10 @@ import findMostRecentStructuredReport from './utils/findMostRecentStructuredRepo
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
*
* @param {serverType} server
* @param {object} external
* @returns {Promise} Should resolve with OHIF measurementData object
*/
const retrieveMeasurements = server => {
const retrieveMeasurements = (server, external = {}) => {
log.info('[DICOMSR] retrieveMeasurements');
if (!server || server.type !== 'dicomWeb') {
@@ -36,7 +37,7 @@ const retrieveMeasurements = server => {
if (!latestSeries) return Promise.resolve({});
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl, external);
};
/**
@@ -17,9 +17,15 @@ const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
* @param {Array} series - List of all series metaData loaded
* @param {Array} studies - List of all studies metaData loaded
* @param {string} serverUrl - Server URL to be used on request
* @param {object} external
* @returns {Object} MeasurementData
*/
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
const retrieveMeasurementFromSR = async (
series,
studies,
serverUrl,
external
) => {
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
@@ -40,7 +46,8 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
const displaySets = getAllDisplaySets(studies);
const measurementsData = parseDicomStructuredReport(
part10SRArrayBuffer,
displaySets
displaySets,
external
);
return measurementsData;
@@ -1,7 +1,11 @@
import dcmjs from 'dcmjs';
import classes from '../classes';
import parseSCOORD3D from './SCOORD3D/parseSCOORD3D';
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
const { LogManager } = classes;
/**
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
@@ -9,20 +13,40 @@ import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBy
*
* @param {ArrayBuffer} part10SRArrayBuffer
* @param {Array} displaySets
* @param {object} external
* @returns
*/
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
// Get the dicom data as an Object
const parseDicomStructuredReport = (
part10SRArrayBuffer,
displaySets,
external
) => {
if (external && external.servicesManager) {
parseSCOORD3D({ servicesManager: external.servicesManager, displaySets });
}
// Get the dicom data as an Object
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const storedMeasurementByToolType = MeasurementReport.generateToolState(
dataset
);
let storedMeasurementByToolType;
try {
storedMeasurementByToolType = MeasurementReport.generateToolState(dataset);
} catch (error) {
const seriesDescription = dataset.SeriesDescription || '';
LogManager.publish(LogManager.EVENTS.OnLog, {
title: `Failed to parse ${seriesDescription} measurement report`,
type: 'warning',
message: error.message || '',
notify: true,
});
return;
}
const measurementData = {};
let measurementNumber = 0;
@@ -41,6 +41,7 @@ const isStructuredReportSeries = series => {
const supportedSopClassUIDs = [
'1.2.840.10008.5.1.4.1.1.88.22',
'1.2.840.10008.5.1.4.1.1.11.1',
'1.2.840.10008.5.1.4.1.1.88.34', // COMPREHENSIVE_3D_SR
];
const firstInstance = series.getFirstInstance();
+18
View File
@@ -0,0 +1,18 @@
import PubSub from './PubSub';
/** Log Events */
export const LogEvents = Object.freeze({
OnLog: 'onLog',
});
/**
* Log manager that implements pub/sub.
* This manager can be used to send logs across different packages
* using previously registered events.
*/
class LogManager extends PubSub {
EVENTS = LogEvents;
}
/** Singleton */
export default new LogManager();
+75
View File
@@ -0,0 +1,75 @@
const _subscriptions = Symbol('subscriptions');
const _lastSubscriptionId = Symbol('lastSubscriptionId');
/**
* Class to implement publish/subscribe pattern
*
* @class
* @classdesc Pub/sub mechanism
*/
export default class PubSub {
constructor() {
this[_subscriptions] = {};
this[_lastSubscriptionId] = 0;
}
/**
* Subscribe to event
*
* @param {string} eventName Event name
* @param {Function} callback Callback function
* @returns {void}
*/
subscribe(eventName, callback) {
if (eventName === undefined) {
throw new Error('Event name is required');
}
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
if (!this[_subscriptions].hasOwnProperty(eventName)) {
this[_subscriptions][eventName] = {};
}
const subscriptionId = `sub${this[_lastSubscriptionId]++}`;
this[_subscriptions][eventName][subscriptionId] = callback;
}
/**
* Removes a subscription
*
* @param {string} eventName Event name
* @param {Function} [callback] Callback function
* @returns {void}
*/
unsubscribe(eventName, callback) {
const callbacks = this[_subscriptions][eventName] || {};
for (let subscriptionId in callbacks) {
if (!callback) {
delete callbacks[subscriptionId];
} else if (callbacks[subscriptionId] === callback) {
delete callbacks[subscriptionId];
}
}
}
/**
* Publish event to all subscriptions
*
* @param {String} eventName Event name
* @param {any} [payload] Data that will be published
* @returns {void}
*/
publish(eventName, ...payload) {
if (eventName === undefined) {
throw new Error('Event name is required');
}
const callbacks = this[_subscriptions][eventName] || {};
for (let subscriptionId in callbacks) {
callbacks[subscriptionId](...payload);
}
}
}
+118 -31
View File
@@ -1,16 +1,14 @@
import cornerstone from 'cornerstone-core';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import {
clearStudyLoadingProgress,
setStudyLoadingProgress,
} from '../redux/actions';
import debounce from 'lodash.debounce';
import StackManager from '../utils/StackManager';
import { StudyPrefetcher } from './StudyPrefetcher';
class BaseLoadingListener {
constructor(stack, options = {}) {
this.id = BaseLoadingListener.getNewId();
this.stack = stack;
this.startListening();
this.statsItemsLimit = options.statsItemsLimit || 2;
this.stats = {
items: [],
@@ -60,6 +58,9 @@ class BaseLoadingListener {
}
_getProgressId() {
/**
* TODO: The id key should be configurable.
*/
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
return 'StackProgress:' + displaySetInstanceUID;
}
@@ -96,11 +97,18 @@ class BaseLoadingListener {
class DICOMFileLoadingListener extends BaseLoadingListener {
constructor(stack, options) {
super(stack, options);
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandler.bind(
this
);
this._dataSetUrl = this._getDataSetUrl(stack);
this._lastLoaded = 0;
// Check how many instances has already been download (cached)
this._checkCachedData();
this.startListening();
}
_checkCachedData() {
@@ -127,10 +135,6 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
startListening() {
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandle.bind(
this
);
this.stopListening();
cornerstone.events.addEventListener(
@@ -200,17 +204,57 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
}
}
const StudyLoadingListenerEvents = {
OnProgress: 'StudyLoadingListenerEvents.OnProgress',
};
function promiseState(promise, callback) {
// Symbols and RegExps are never content-equal
var uniqueValue = window['Symbol'] ? Symbol('unique') : /unique/;
function notifyPendingOrResolved(value) {
if (value === uniqueValue) {
return callback('pending');
} else {
return callback('fulfilled');
}
}
function notifyRejected(reason) {
return callback('rejected');
}
var race = [promise, Promise.resolve(uniqueValue)];
Promise.race(race).then(notifyPendingOrResolved, notifyRejected);
}
class StackLoadingListener extends BaseLoadingListener {
constructor(stack, options = {}) {
options.statsItemsLimit = 20;
super(stack, options);
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
this
);
this.imageDataMap = this._convertImageIdsArrayToMap(stack.imageIds);
this.framesStatus = this._createArray(stack.imageIds.length, false);
this.loadedCount = 0;
// Check how many instances has already been download (cached)
this._checkCachedData();
this._debouncedSetProgressData = debounce((...args) => {
this._setProgressData(...args);
/** After checking cache, continue prefetch */
const studyPrefetcher = StudyPrefetcher.getInstance();
studyPrefetcher.prefetch(studyPrefetcher.getElement());
}, 300);
const debounced = true;
this._checkCachedData(debounced);
this.startListening();
}
_convertImageIdsArrayToMap(imageIds) {
@@ -238,18 +282,37 @@ class StackLoadingListener extends BaseLoadingListener {
return array;
}
_checkCachedData() {
// const imageIds = this.stack.imageIds;
// TODO: No way to check status of Promise.
/*for(let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
/**
* Check if image id is cached in cornerstone.
*
* @param {string} imageId
* @returns
*/
isImageCached(imageId) {
const image = cornerstone.imageCache.imageCache[imageId];
return image && image.sizeInBytes;
}
const imagePromise = cornerstone.imageCache.getImageLoadObject(imageId).promise;
_checkCachedData(debounced = false) {
const imageIds = this.stack.imageIds;
if (imagePromise && (imagePromise.state() === 'resolved')) {
this._updateFrameStatus(imageId, true);
}
}*/
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const imageObject = cornerstone.imageCache.getImageLoadObject(imageId);
if (this.isImageCached(imageId)) {
this._updateFrameStatus(imageId, true, debounced);
}
if (imageObject && imageObject.promise) {
promiseState(imageObject.promise, state => {
if (state === 'fulfilled') {
this._updateFrameStatus(imageId, true, debounced);
}
});
}
}
}
_getImageLoadedEventName() {
@@ -272,11 +335,6 @@ class StackLoadingListener extends BaseLoadingListener {
const imageLoadedEventName = this._getImageLoadedEventName();
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
this
);
this.stopListening();
cornerstone.events.addEventListener(
@@ -303,7 +361,7 @@ class StackLoadingListener extends BaseLoadingListener {
);
}
_updateFrameStatus(imageId, loaded) {
_updateFrameStatus(imageId, loaded, debounced) {
const imageData = this.imageDataMap.get(imageId);
if (!imageData || imageData.loaded === loaded) {
@@ -318,7 +376,7 @@ class StackLoadingListener extends BaseLoadingListener {
imageData.loaded = loaded;
this.framesStatus[imageData.index] = loaded;
this.loadedCount += loaded ? 1 : -1;
this._updateProgress();
this._updateProgress(debounced);
}
_setProgressData(progressId, progressData) {
@@ -339,7 +397,7 @@ class StackLoadingListener extends BaseLoadingListener {
);
}
_updateProgress() {
_updateProgress(debounced) {
const totalFramesCount = this.stack.imageIds.length;
const loadedFramesCount = this.loadedCount;
const loadingFramesCount = totalFramesCount - loadedFramesCount;
@@ -357,6 +415,11 @@ class StackLoadingListener extends BaseLoadingListener {
framesStatus: this.framesStatus,
};
if (debounced) {
this._debouncedSetProgressData(progressId, progressData);
return;
}
this._setProgressData(progressId, progressData);
}
@@ -371,11 +434,13 @@ class StackLoadingListener extends BaseLoadingListener {
}
progressBar += ']';
log.info(`${displaySetInstanceUID}: ${progressBar}`);
console.info(`${displaySetInstanceUID}: ${progressBar}`);
}
}
class StudyLoadingListener {
static events = StudyLoadingListenerEvents;
constructor(options) {
this.listeners = {};
this.options = options;
@@ -454,14 +519,36 @@ class StudyLoadingListener {
_getSchema(stack) {
const imageId = stack.imageIds[0];
if (!imageId) {
return;
}
const colonIndex = imageId.indexOf(':');
return imageId.substring(0, colonIndex);
}
// Singleton
static getInstance(options) {
/**
* TODO: Use a different alternative without the use of events.
*/
const DEFAULT_OPTIONS = {
_setProgressData: (progressId, progressData) => {
const event = new CustomEvent(StudyLoadingListenerEvents.OnProgress, {
detail: { progressId, progressData },
});
document.dispatchEvent(event);
},
_clearProgressById: progressId => {
const event = new CustomEvent(StudyLoadingListenerEvents.OnProgress, {
detail: { progressId, percentComplete: 0 },
});
document.dispatchEvent(event);
},
};
if (!StudyLoadingListener._instance) {
StudyLoadingListener._instance = new StudyLoadingListener(options);
StudyLoadingListener._instance = new StudyLoadingListener(
options || DEFAULT_OPTIONS
);
}
return StudyLoadingListener._instance;
+335 -56
View File
@@ -1,14 +1,27 @@
import log from '../log.js';
import OHIFError from './OHIFError';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import getImageId from '../utils/getImageId.js';
const noop = () => {};
export class StudyPrefetcher {
constructor(studies) {
options = {
order: 'closest',
displaySetCount: 1,
onImageCached: noop,
requestType: 'prefetch',
preventCache: false,
prefetchDisplaySetsTimeout: 300,
maxNumPrefetchRequests: 100,
includeActiveDisplaySet: false,
};
constructor(studies, options) {
this.studies = studies || [];
this.prefetchDisplaySetsTimeout = 300;
this.lastActiveViewportElement = null;
if (options) {
this.options = { ...this.options, ...options };
this.options.requestType = 'prefetch';
}
cornerstone.events.addEventListener(
'cornerstoneimagecachefull.StudyPrefetcher',
@@ -16,6 +29,9 @@ export class StudyPrefetcher {
);
}
/**
* Remove previously set event listeners and stop prefetching.
*/
destroy() {
this.stopPrefetching();
cornerstone.events.removeEventListener(
@@ -24,95 +40,180 @@ export class StudyPrefetcher {
);
}
static getInstance() {
/**
* Get StudyPrefetcher singleton instance.
*
* @param {array} studies
* @param {object} options
* @returns
*/
static getInstance(studies = [], options) {
if (!StudyPrefetcher.instance) {
StudyPrefetcher.instance = new StudyPrefetcher([]);
StudyPrefetcher.instance = new StudyPrefetcher(studies, options);
}
if (options) {
this.options = { ...this.options, ...options };
this.options.requestType = 'prefetch';
}
return StudyPrefetcher.instance;
}
/**
* OHIF study metadata instances.
*
* @param {array} studies
*/
setStudies(studies) {
this.stopPrefetching();
this.studies = studies;
}
prefetch() {
/**
* Get previously prefetched element.
*
* @returns {HTMLElement} Previously prefetched element.
*/
getElement() {
return this.element;
}
/**
* Prefetch related display sets based on cornerstone viewport element
* with previously set options.
*
* @param {*} element
* @param {string} displaySetInstanceUID the display set instance uid
* @returns
*/
prefetch(element, displaySetInstanceUID) {
if (!this.studies || !this.studies.length) {
return;
}
this.element = element;
try {
this.enabledElement = cornerstone.getEnabledElement(element);
} catch {
throw new Error('Failed to find the enabled element');
}
this.stopPrefetching();
this.prefetchDisplaySets();
this.prefetchDisplaySets(displaySetInstanceUID);
}
/**
* Stop prefetching images.
*/
stopPrefetching() {
cornerstoneTools.requestPoolManager.clearRequestStack('prefetch');
cornerstone.imageLoadPoolManager.clearRequestStack('prefetch');
}
prefetchDisplaySetsAsync(timeout) {
timeout = timeout || this.prefetchDisplaySetsTimeout;
/**
* Prefetch display sets async.
*
* @param {HTMLElement} element cornerstone viewport element
* @param {number} timeout
*/
prefetchDisplaySetsAsync(element, timeout) {
try {
this.enabledElement = cornerstone.getEnabledElement(element);
} catch {
throw new Error('Failed to find the enabled element');
}
timeout = timeout || this.options.prefetchDisplaySetsTimeout;
clearTimeout(this.prefetchDisplaySetsHandler);
this.prefetchDisplaySetsHandler = setTimeout(() => {
this.prefetchDisplaySets();
this.prefetchDisplaySets(element);
}, timeout);
}
prefetchDisplaySets() {
// TODO: Allow passing in config
let config = {
order: 'closest',
displaySetCount: 1,
};
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(config);
/**
* Extract all image ids from all display sets to be fetched and
* call method to add images to request pool manager.
*
* @param {string} displaySetInstanceUID the display set instance uid
*/
prefetchDisplaySets(displaySetInstanceUID) {
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(
displaySetInstanceUID
);
const imageIds = this.getImageIdsFromDisplaySets(displaySetsToPrefetch);
this.prefetchImageIds(imageIds);
}
/**
* Add image ids to request pool manager.
*
* @param {array} imageIds
*/
prefetchImageIds(imageIds) {
const nonCachedImageIds = this.filterCachedImageIds(imageIds);
const requestPoolManager = cornerstoneTools.requestPoolManager;
const requestType = 'prefetch';
const preventCache = false;
const noop = () => {};
const imageLoadPoolManager = cornerstone.imageLoadPoolManager;
imageLoadPoolManager.maxNumRequests = {
...imageLoadPoolManager.maxNumRequests,
prefetch: this.options.maxNumPrefetchRequests,
};
let requestFn;
if (this.options.preventCache) {
requestFn = id => cornerstone.loadImage(id);
} else {
requestFn = id => cornerstone.loadAndCacheImage(id);
}
nonCachedImageIds.forEach(imageId => {
requestPoolManager.addRequest(
{},
imageId,
requestType,
preventCache,
noop,
noop
imageLoadPoolManager.addRequest(
requestFn.bind(this, imageId),
this.options.requestType,
{
imageId,
}
);
});
requestPoolManager.startGrabbing();
}
/**
* Get study by cornerstone image instance.
*
* @param {object} image
* @returns
*/
getStudy(image) {
const StudyInstanceUID = cornerstone.metaData.get(
'StudyInstanceUID',
image.imageId
);
return OHIF.viewer.Studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
const studies = this.studies;
return studies.find(
study => study.getData().StudyInstanceUID === StudyInstanceUID
);
}
/**
* Get study series by cornerstone image instance.
*
* @param {object} study OHIF study instance
* @param {object} image cornerstone image instance object
* @returns
*/
getSeries(study, image) {
const SeriesInstanceUID = cornerstone.metaData.get(
'SeriesInstanceUID',
image.imageId
);
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
return studyMetadata.getSeriesByUID(SeriesInstanceUID);
return study.getSeriesByUID(SeriesInstanceUID);
}
/**
* Get sop instance by cornerstone image instance.
*
* @param {array} series
* @param {object} image
* @returns
*/
getInstance(series, image) {
const instanceMetadata = cornerstone.metaData.get(
'instance',
@@ -121,7 +222,33 @@ export class StudyPrefetcher {
return series.getInstanceByUID(instanceMetadata.SOPInstanceUID);
}
getActiveDisplaySet(displaySets, instance) {
/**
* Returns the display set with given uid.
*
* @param {string} displaySetInstanceUID the display set instance uid
* @returns {object} displaySet
*/
getDisplaySetByUID(displaySetInstanceUID) {
let displaySet;
this.studies.forEach(study => {
const ds = study.displaySets.find(
ds => ds.displaySetInstanceUID === displaySetInstanceUID
);
if (ds) {
displaySet = ds;
}
});
return displaySet;
}
/**
* Get display set by SOPInstanceUID.
*
* @param {array} displaySets
* @param {object} instance
* @returns
*/
getDisplaySetBySOPInstanceUID(displaySets, instance) {
return displaySets.find(displaySet => {
return displaySet.images.some(displaySetImage => {
return displaySetImage.SOPInstanceUID === instance.SOPInstanceUID;
@@ -129,25 +256,48 @@ export class StudyPrefetcher {
});
}
getDisplaySetsToPrefetch(config) {
/**
* Get active viewport image based on cornerstone viewport element.
* @returns
*/
getActiveViewportImage() {
if (!this.enabledElement) {
return;
}
return this.enabledElement.image;
}
/**
* Prefetch display sets based on cornerstone viewport element image.
*
* @param {string} displaySetInstanceUID the display set instance uid
* @returns {array} displaySets
*/
getDisplaySetsToPrefetch(displaySetInstanceUID) {
const image = this.getActiveViewportImage();
if (!image || !config || !config.displaySetCount) {
if (!image) {
return [];
}
/*const study = this.getStudy(image);
const study = this.getStudy(image);
const series = this.getSeries(study, image);
const instance = this.getInstance(series, image);*/
const instance = this.getInstance(series, image);
const displaySets = study.displaySets;
const activeDisplaySet = null; //this.getActiveDisplaySet(displaySets, instance);
const activeDisplaySet = displaySetInstanceUID
? this.getDisplaySetByUID(displaySetInstanceUID)
: this.getDisplaySetBySOPInstanceUID(displaySets, instance);
const prefetchMethodMap = {
topdown: 'getFirstDisplaySets',
downward: 'getNextDisplaySets',
upward: 'getPreviousDisplaySets',
closest: 'getClosestDisplaySets',
all: 'getAllDisplaySets',
};
const prefetchOrder = config.order;
const prefetchOrder = this.options.order;
const methodName = prefetchMethodMap[prefetchOrder];
const getDisplaySets = this[methodName];
@@ -163,18 +313,59 @@ export class StudyPrefetcher {
this,
displaySets,
activeDisplaySet,
config.displaySetCount
this.options.displaySetCount,
this.options.includeActiveDisplaySet
);
}
getFirstDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
/**
* Get all display sets.
*
* @param {array} displaySets
* @param {object} activeDisplaySet
* @param {number} displaySetCount
* @param {boolean} includeActiveDisplaySet
* @returns
*/
getAllDisplaySets(
displaySets,
activeDisplaySet,
displaySetCount,
includeActiveDisplaySet
) {
const length = displaySets.length;
const selectedDisplaySets = [];
for (let i = 0; i < length; i++) {
const displaySet = displaySets[i];
selectedDisplaySets.push(displaySet);
}
return selectedDisplaySets;
}
/**
* Get all display sets in order after the active display set.
*
* @param {array} displaySets
* @param {object} activeDisplaySet
* @param {number} displaySetCount
* @param {boolean} includeActiveDisplaySet
* @returns
*/
getFirstDisplaySets(
displaySets,
activeDisplaySet,
displaySetCount,
includeActiveDisplaySet
) {
const length = displaySets.length;
const selectedDisplaySets = [];
for (let i = 0; i < length && displaySetCount; i++) {
const displaySet = displaySets[i];
if (displaySet !== activeDisplaySet) {
if (includeActiveDisplaySet || displaySet !== activeDisplaySet) {
selectedDisplaySets.push(displaySet);
displaySetCount--;
}
@@ -183,21 +374,78 @@ export class StudyPrefetcher {
return selectedDisplaySets;
}
getNextDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
/**
* Get all display sets before the active display set.
*
* @param {array} displaySets
* @param {object} activeDisplaySet
* @param {number} displaySetCount
* @param {boolean} includeActiveDisplaySet
* @returns
*/
getPreviousDisplaySets(
displaySets,
activeDisplaySet,
displaySetCount,
includeActiveDisplaySet
) {
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
const begin = activeDisplaySetIndex + 1;
const end = Math.min(begin + displaySetCount, displaySets.length);
const end = includeActiveDisplaySet
? activeDisplaySetIndex + 1
: activeDisplaySetIndex;
const previousDisplaySets = displaySets.slice(0, end);
return previousDisplaySets.reverse().slice(0, displaySetCount);
}
/**
* Get all display sets after the active display set.
*
* @param {array} displaySets
* @param {object} activeDisplaySet
* @param {number} displaySetCount
* @param {boolean} includeActiveDisplaySet
* @returns
*/
getNextDisplaySets(
displaySets,
activeDisplaySet,
displaySetCount,
includeActiveDisplaySet
) {
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
const begin = includeActiveDisplaySet
? activeDisplaySetIndex
: activeDisplaySetIndex + 1;
const end = Math.min(begin + displaySetCount, displaySets.length);
return displaySets.slice(begin, end);
}
getClosestDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
/**
* Get all display set closest to the active display set.
*
* @param {array} displaySets
* @param {object} activeDisplaySet
* @param {number} displaySetCount
* @param {boolean} includeActiveDisplaySet
* @returns
*/
getClosestDisplaySets(
displaySets,
activeDisplaySet,
displaySetCount,
includeActiveDisplaySet
) {
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
const length = displaySets.length;
const selectedDisplaySets = [];
let left = activeDisplaySetIndex - 1;
let right = activeDisplaySetIndex + 1;
if (includeActiveDisplaySet) {
selectedDisplaySets.push(displaySets[activeDisplaySetIndex]);
displaySetCount--;
}
while ((left >= 0 || right < length) && displaySetCount) {
if (left >= 0) {
selectedDisplaySets.push(displaySets[left]);
@@ -215,6 +463,12 @@ export class StudyPrefetcher {
return selectedDisplaySets;
}
/**
* Get all image ids from display sets.
*
* @param {array} displaySets
* @returns {array} image ids
*/
getImageIdsFromDisplaySets(displaySets) {
let imageIds = [];
@@ -225,9 +479,19 @@ export class StudyPrefetcher {
return imageIds;
}
/**
* Get all image ids from a given display set.
*
* @param {array} displaySet
* @returns
*/
getImageIdsFromDisplaySet(displaySet) {
const imageIds = [];
if (!displaySet.images || displaySet.images.length < 1) {
return [];
}
// TODO: This duplicates work done by the stack manager
displaySet.images.forEach(image => {
const numFrames = image.numFrames;
@@ -245,15 +509,30 @@ export class StudyPrefetcher {
return imageIds;
}
/**
* Filter cached image ids from a set of image ids.
*
* @param {array} imageIds
* @returns {array} images not cached
*/
filterCachedImageIds(imageIds) {
return imageIds.filter(imageId => !this.isImageCached(imageId));
}
/**
* Check if image id is cached in cornerstone.
*
* @param {string} imageId
* @returns
*/
isImageCached(imageId) {
const image = cornerstone.imageCache.imageCache[imageId];
return image && image.sizeInBytes;
}
/**
* Warns that cache is full and stops prefetching.
*/
cacheFullHandler = () => {
log.warn('Cache full');
this.stopPrefetching();
+4
View File
@@ -4,6 +4,8 @@ import CommandsManager from './CommandsManager.js';
import { DICOMFileLoadingListener } from './StudyLoadingListener';
import HotkeysManager from './HotkeysManager.js';
import ImageSet from './ImageSet';
import LogManager from './LogManager';
import PubSub from './PubSub';
import MetadataProvider from './MetadataProvider';
import OHIFError from './OHIFError.js';
import { OHIFStudyMetadataSource } from './OHIFStudyMetadataSource';
@@ -36,7 +38,9 @@ const classes = {
MetadataProvider,
CommandsManager,
HotkeysManager,
LogManager,
ImageSet,
PubSub,
StudyPrefetcher,
StudyLoadingListener,
StackLoadingListener,
@@ -9,7 +9,10 @@ import { SeriesMetadata } from './SeriesMetadata';
import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from '../../utils/isImage';
import { isDisplaySetReconstructable, isSpacingUniform } from '../../utils/isDisplaySetReconstructable';
import {
isDisplaySetReconstructable,
isSpacingUniform,
} from '../../utils/isDisplaySetReconstructable';
import errorHandler from '../../errorHandler';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
import getXHRRetryRequestHook from '../../utils/xhrRetryRequestHook';
@@ -159,41 +162,64 @@ class StudyMetadata extends Metadata {
// series into another display set.
const stackableInstances = [];
series.forEachInstance(instance => {
let displaySet;
// All imaging modalities must have a valid value for SOPClassUID (x00080016) or Rows (x00280010)
if (
!isImage(instance.getTagValue('SOPClassUID')) &&
!instance.getTagValue('Rows')
) {
return;
}
let displaySet;
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
// we set an empty display and we add a isModalitySupported variable to
// print a warning that the modality is not supported in the thumbnail.
const displaySet = new ImageSet([]);
const seriesData = series.getData();
displaySet.setAttributes({
sopClassUIDs,
isClip: true,
displaySetInstanceUID: displaySet.uid,
SeriesDate: seriesData.SeriesDate,
SeriesTime: seriesData.SeriesTime,
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID for drag/drop purposes
numImageFrames: instance.getTagValue('NumberOfFrames'), // Override the default value of instances.length
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.Modality)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUIDs,
SeriesNumber: instance.getTagValue('SeriesNumber'),
SeriesDescription: instance.getTagValue('SeriesDescription'),
numImageFrames: instance.getTagValue('NumberOfFrames'),
frameRate: instance.getTagValue('FrameTime'),
Modality: instance.getTagValue('Modality'),
isMultiFrame: false,
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID
SeriesInstanceUID: series.getSeriesInstanceUID(),
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
isReconstructable: false,
isModalitySupported: false,
metadata: instance.getData().metadata,
});
displaySets.push(displaySet);
} else {
stackableInstances.push(instance);
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUIDs,
isClip: true,
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID for drag/drop purposes
numImageFrames: instance.getTagValue('NumberOfFrames'), // Override the default value of instances.length
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.Modality)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUIDs,
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID
SeriesInstanceUID: series.getSeriesInstanceUID(),
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else {
stackableInstances.push(instance);
}
}
});
@@ -227,7 +253,6 @@ class StudyMetadata extends Metadata {
displaySets.map(displaySet => this._derivedDisplaySets.push(displaySet));
}
/**
* Returns the source display set of the derivated display set.
* @param {object} derivatedDisplaySet
@@ -242,40 +267,45 @@ class StudyMetadata extends Metadata {
});
const otherDisplaySets = allDisplaySets.filter(
ds => ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID
ds =>
ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID
);
const { metadata } = derivatedDisplaySet;
let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence
(metadata, otherDisplaySets);
let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence(
metadata,
otherDisplaySets
);
let noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
let noReferencedSeriesAvailable =
!referencedSeriesInstanceUIDs ||
referencedSeriesInstanceUIDs.length === 0;
if (noReferencedSeriesAvailable) {
referencedSeriesInstanceUIDs =
_findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence
(metadata);
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence(
metadata
);
}
noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
noReferencedSeriesAvailable =
!referencedSeriesInstanceUIDs ||
referencedSeriesInstanceUIDs.length === 0;
if (noReferencedSeriesAvailable) {
referencedSeriesInstanceUIDs =
_findReferencedSeriesInstanceUIDsFromReferencedImageSequence
(metadata, otherDisplaySets);
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromReferencedImageSequence(
metadata,
otherDisplaySets
);
}
const referencedSeriesAvailable = referencedSeriesInstanceUIDs &&
referencedSeriesInstanceUIDs.length !== 0;
const referencedSeriesAvailable =
referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0;
if (referencedSeriesAvailable) {
const referencedDisplaySet = otherDisplaySets.find(ds =>
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
);
;
return referencedDisplaySet;
}
};
}
/**
* Returns a list of derived datasets in the study, filtered by the given filter.
@@ -303,7 +333,10 @@ class StudyMetadata extends Metadata {
if (referencedSeriesInstanceUID) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet => {
return StudyMetadata.getReferencedDisplaySet(displaySet, [this]).SeriesInstanceUID === referencedSeriesInstanceUID;
return (
StudyMetadata.getReferencedDisplaySet(displaySet, [this])
.SeriesInstanceUID === referencedSeriesInstanceUID
);
}
);
}
@@ -311,8 +344,7 @@ class StudyMetadata extends Metadata {
if (referencedFrameOfReferenceUID) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet =>
displaySet.FrameOfReferenceUID ===
referencedFrameOfReferenceUID
displaySet.FrameOfReferenceUID === referencedFrameOfReferenceUID
);
}
@@ -497,6 +529,14 @@ class StudyMetadata extends Metadata {
}
}
if (
this._displaySets.some(
ds => ds.displaySetInstanceUID === displaySet.displaySetInstanceUID
)
) {
return;
}
this._displaySets.splice(insertIndex, 0, displaySet);
this.displaySets = this._displaySets;
}
@@ -861,8 +901,9 @@ const makeDisplaySet = (series, instances) => {
imageSet.sortByImagePositionPatient();
// check if the spacing is uniform and update isReconstructable
const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find
(issue => issue === ReconstructionIssues.DATASET_4D);
const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find(
issue => issue === ReconstructionIssues.DATASET_4D
);
displaySpacingInfo = isSpacingUniform(imageSet.images, datasetIs4D);
imageSet.isReconstructable = displaySpacingInfo.isUniform;
@@ -875,11 +916,15 @@ const makeDisplaySet = (series, instances) => {
if (!imageSet.displayReconstructableInfo) {
// It is not reconstrabale Save type of warning
imageSet.reconstructionIssues = displaySpacingInfo ?
displayReconstructableInfo.reconstructionIssues.concat(displaySpacingInfo.reconstructionIssues) :
displayReconstructableInfo.reconstructionIssues;
imageSet.reconstructionIssues = displaySpacingInfo
? displayReconstructableInfo.reconstructionIssues.concat(
displaySpacingInfo.reconstructionIssues
)
: displayReconstructableInfo.reconstructionIssues;
}
imageSet.isModalitySupported = true;
return imageSet;
};
@@ -957,38 +1002,36 @@ function _getDisplaySetFromSopClassModule(
}
/**
* Returns the referenced series instance UIDs by searching the information in the
* ReferencedSeriesSequence.
* @param {object} derivatedDisplaySet.metadata
* @return {array[string]} referenced series instance UIDs.
*/
function _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence (
metadata,
* Returns the referenced series instance UIDs by searching the information in the
* ReferencedSeriesSequence.
* @param {object} derivatedDisplaySet.metadata
* @return {array[string]} referenced series instance UIDs.
*/
function _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence(
metadata
) {
if (!metadata.ReferencedSeriesSequence) {
return;
}
let referencedSeriesInstanceUIDs;
const ReferencedSeriesSequence = _toArray(
metadata.ReferencedSeriesSequence
);
const ReferencedSeriesSequence = _toArray(metadata.ReferencedSeriesSequence);
referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
);
return referencedSeriesInstanceUIDs;
};
}
/**
* Returns the referenced series instance UIDs by searching the information in the
* ReferencedImageSequence.
* @param {object} derivatedDisplaySet.metadata
* @param {array[object]} displaysets
* @return {array[string]} referenced series instance UIDs.
*/
function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence (
* Returns the referenced series instance UIDs by searching the information in the
* ReferencedImageSequence.
* @param {object} derivatedDisplaySet.metadata
* @param {array[object]} displaysets
* @return {array[string]} referenced series instance UIDs.
*/
function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence(
metadata,
displaySets
) {
@@ -1009,22 +1052,25 @@ function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence (
ReferencedSOPInstanceUID
);
if (referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0) {
if (
referencedSeriesInstanceUIDs &&
referencedSeriesInstanceUIDs.length !== 0
) {
break;
}
}
return referencedSeriesInstanceUIDs;
};
}
/**
* Returns the referenced series instance UIDs by searching the information in the
* SourceImageSequence.
* @param {object} derivatedDisplaySet.metadata
* @param {array[object]} displaysets
* @return {array[string]} referenced series instance UIDs.
*/
function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
* Returns the referenced series instance UIDs by searching the information in the
* SourceImageSequence.
* @param {object} derivatedDisplaySet.metadata
* @param {array[object]} displaysets
* @return {array[string]} referenced series instance UIDs.
*/
function _findReferencedSeriesInstanceUIDsFromSourceImageSequence(
metadata,
displaySets
) {
@@ -1034,9 +1080,7 @@ function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
SourceImageSequence = metadata.SourceImageSequence;
} else {
const { PerFrameFunctionalGroupsSequence } = metadata;
const firstFunctionalGroups = _toArray(
PerFrameFunctionalGroupsSequence
)[0];
const firstFunctionalGroups = _toArray(PerFrameFunctionalGroupsSequence)[0];
if (firstFunctionalGroups) {
const { DerivationImageSequence } = firstFunctionalGroups;
SourceImageSequence = DerivationImageSequence;
@@ -1056,22 +1100,25 @@ function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
displaySets,
ReferencedSOPInstanceUID
);
if (referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0) {
if (
referencedSeriesInstanceUIDs &&
referencedSeriesInstanceUIDs.length !== 0
) {
break;
}
}
return referencedSeriesInstanceUIDs;
};
}
/**
* Returns the referenced series instance UIDs by searching the information in the
* SOPInstanceUID of the displaySets.
* @param {array[object]} displaysets
* @param {string} SOPInstanceUID
* @return {array[string]} referenced series instance UIDs.
*/
function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID (
* Returns the referenced series instance UIDs by searching the information in the
* SOPInstanceUID of the displaySets.
* @param {array[object]} displaysets
* @param {string} SOPInstanceUID
* @return {array[string]} referenced series instance UIDs.
*/
function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
displaySets,
SOPInstanceUID
) {
@@ -1092,10 +1139,10 @@ function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID (
}
}
}
};
}
function _toArray(arrayOrObject) {
return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject];
}
export {StudyMetadata};
export { StudyMetadata };
+3
View File
@@ -20,6 +20,7 @@ import ui from './ui';
import user from './user.js';
import errorHandler from './errorHandler.js';
import utils, { hotkeys } from './utils/';
import str2ab from './utils/str2ab';
import {
UINotificationService,
@@ -96,6 +97,8 @@ export {
UIDialogService,
MeasurementService,
LoggerService,
str2ab,
};
export { OHIF };
+1
View File
@@ -26,6 +26,7 @@ describe('Top level exports', () => {
'cornerstone',
'default', //
'errorHandler',
'str2ab',
'string',
'ui',
'user',
@@ -802,6 +802,10 @@ export default class MeasurementApi {
collection.push(addedMeasurement);
}
if (measurement.isReadOnly) {
addedMeasurement.isReadOnly = measurement.isReadOnly;
}
if (!emptyItem) {
// Reflect the entry in the tool group collection
groupCollection.push({
@@ -1,9 +1,11 @@
import cornerstone from 'cornerstone-core';
export default function(element) {
// Get the Cornerstone imageId
const enabledElement = cornerstone.getEnabledElement(element);
const imageId = enabledElement.image.imageId;
export default function(element, imageId) {
if (!imageId) {
// Get the Cornerstone imageId
const enabledElement = cornerstone.getEnabledElement(element);
imageId = enabledElement.image.imageId;
}
// Get StudyInstanceUID & PatientID
const {
@@ -0,0 +1,22 @@
export const dicomSRDisplayTool = {
id: 'DICOMSRDisplayTool',
name: 'DICOMSRDisplayTool',
toolGroup: 'allTools',
cornerstoneToolType: 'DICOMSRDisplayTool',
options: {
measurementTable: {
displayFunction: data => {
return `(SR) ${data.lesionNamingNumber ||
data.measurementNumber ||
data.text ||
''}`;
},
},
caseProgress: {
include: true,
evaluate: true,
},
},
};
export default dicomSRDisplayTool;
@@ -10,6 +10,7 @@ import { angle } from './angle';
import { targetCR } from './targetCR';
import { targetNE } from './targetNE';
import { targetUN } from './targetUN';
import dicomSRDisplayTool from './dicomSRDisplayTool';
export {
arrowAnnotate,
@@ -24,4 +25,5 @@ export {
targetCR,
targetNE,
targetUN,
dicomSRDisplayTool,
};
-13
View File
@@ -78,17 +78,6 @@ export const setActiveViewportSpecificData = viewportSpecificData => ({
/**
* NOT-VIEWPORT
*/
export const setStudyLoadingProgress = (progressId, progressData) => ({
type: 'SET_STUDY_LOADING_PROGRESS',
progressId,
progressData,
});
export const clearStudyLoadingProgress = progressId => ({
type: 'CLEAR_STUDY_LOADING_PROGRESS',
progressId,
});
export const setUserPreferences = state => ({
type: SET_USER_PREFERENCES,
state,
@@ -134,8 +123,6 @@ const actions = {
/**
* NOT-VIEWPORT
*/
setStudyLoadingProgress,
clearStudyLoadingProgress,
setUserPreferences,
setExtensionData,
setTimepoints,
-2
View File
@@ -11,8 +11,6 @@ describe('actions', () => {
'setLayout',
'clearViewportSpecificData',
'setActiveViewportSpecificData',
'setStudyLoadingProgress',
'clearStudyLoadingProgress',
'setUserPreferences',
'setExtensionData',
'setTimepoints',
@@ -1,5 +1,4 @@
import extensions from './extensions';
import loading from './loading';
import preferences from './preferences';
import servers from './servers';
import studies from './studies';
@@ -8,7 +7,6 @@ import viewports from './viewports';
const reducers = {
extensions,
loading,
preferences,
servers,
studies,
@@ -1,33 +0,0 @@
import cloneDeep from 'lodash.clonedeep';
const defaultState = {
progress: {},
lastUpdated: null,
};
const loading = (state = defaultState, action) => {
let progress;
let lastUpdated;
switch (action.type) {
case 'SET_STUDY_LOADING_PROGRESS':
progress = cloneDeep(state).progress;
progress[action.progressId] = action.progressData;
// This is a workaround so we can easily identify changes
// to the progress object without doing deep comparison.
lastUpdated = new Date().getTime();
return Object.assign({}, state, { progress, lastUpdated });
case 'CLEAR_STUDY_LOADING_PROGRESS':
progress = cloneDeep(state).progress;
delete progress[action.progressId];
lastUpdated = new Date().getTime();
return Object.assign({}, state, { progress, lastUpdated });
default:
return state;
}
};
export default loading;
@@ -1,5 +1,7 @@
/** Internal imports */
import log from '../../log';
import guid from '../../utils/guid';
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
/**
* Measurement source schema
@@ -15,7 +17,7 @@ import guid from '../../utils/guid';
*
* @typedef {Object} Measurement
* @property {number} id -
* @property {string} sopInstanceUid -
* @property {string} SOPInstanceUID -
* @property {string} FrameOfReferenceUID -
* @property {string} referenceSeriesUID -
* @property {string} label -
@@ -32,24 +34,37 @@ const MEASUREMENT_SCHEMA_KEYS = [
'id',
'SOPInstanceUID',
'FrameOfReferenceUID',
'referenceStudyUID',
'referenceSeriesUID',
'label',
'description',
'type',
'unit',
'area', // TODO: Add concept names instead (descriptor)
'mean',
'stdDev',
'length',
'shortestDiameter',
'longestDiameter',
'text', // NOTE: There is nothing like this in SR.
'points',
'source',
];
const EVENTS = {
MEASUREMENT_UPDATED: 'event::measurement_updated',
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
MEASUREMENT_ADDED: 'event::measurement_added',
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
MEASUREMENT_REMOVED: 'event::measurement_removed',
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
};
const VALUE_TYPES = {
POLYLINE: 'value_type::polyline',
POINT: 'value_type::point',
BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here.
ELLIPSE: 'value_type::ellipse',
MULTIPOINT: 'value_type::multipoint',
CIRCLE: 'value_type::circle',
@@ -61,6 +76,7 @@ class MeasurementService {
this.mappings = {};
this.measurements = {};
this.listeners = {};
this._jumpToMeasurementCache = {};
Object.defineProperty(this, 'EVENTS', {
value: EVENTS,
writable: false,
@@ -73,6 +89,8 @@ class MeasurementService {
enumerable: true,
configurable: false,
});
Object.assign(this, pubSubServiceInterface);
}
/**
@@ -91,7 +109,7 @@ class MeasurementService {
/**
* Get specific measurement by its id.
*
* @param {string} id If of the measurement
* @param {string} id Id of the measurement
* @return {Measurement} Measurement instance
*/
getMeasurement(id) {
@@ -114,13 +132,11 @@ class MeasurementService {
*/
createSource(name, version) {
if (!name) {
log.warn('Source name not provided. Exiting early.');
return;
throw new Error('Source name not provided.');
}
if (!version) {
log.warn('Source version not provided. Exiting early.');
return;
throw new Error('Source version not provided.');
}
const id = guid();
@@ -132,6 +148,9 @@ class MeasurementService {
source.addOrUpdate = (definition, measurement) => {
return this.addOrUpdate(source, definition, measurement);
};
source.remove = id => {
return this.remove(id, source);
};
source.getAnnotation = (definition, measurementId) => {
return this.getAnnotation(source, definition, measurementId);
};
@@ -142,6 +161,32 @@ class MeasurementService {
return source;
}
getSource(name, version) {
const { sources } = this;
const id = this._getSourceId(name, version);
return sources[id];
}
getSourceMappings(name, version) {
const { mappings } = this;
const id = this._getSourceId(name, version);
return mappings[id];
}
_getSourceId(name, version) {
const { sources } = this;
const sourceId = Object.keys(sources).find(sourceId => {
const source = sources[sourceId];
return source.name === name && source.version === version;
});
return sourceId;
}
/**
* Add a new measurement matching criteria along with mapping functions.
*
@@ -160,28 +205,23 @@ class MeasurementService {
toMeasurementSchema
) {
if (!this._isValidSource(source)) {
log.warn('Invalid source. Exiting early.');
return;
throw new Error('Invalid source.');
}
if (!matchingCriteria) {
log.warn('Matching criteria not provided. Exiting early.');
return;
throw new Error('Matching criteria not provided.');
}
if (!definition) {
log.warn('Definition not provided. Exiting early.');
return;
throw new Error('Definition not provided.');
}
if (!toSourceSchema) {
log.warn('Source mapping function not provided. Exiting early.');
return;
throw new Error('Mapping function to source schema not provided.');
}
if (!toMeasurementSchema) {
log.warn('Measurement mapping function not provided. Exiting early.');
return;
throw new Error('Measurement mapping function not provided.');
}
const mapping = {
@@ -227,9 +267,9 @@ class MeasurementService {
measurementId,
definition
);
const measurement = this.getMeasurement(measurementId);
if (mapping) return mapping.toSourceSchema(measurement, definition);
const measurement = this.getMeasurement(measurementId);
const matchingMapping = this._getMatchingMapping(
source,
definition,
@@ -243,15 +283,50 @@ class MeasurementService {
}
}
update(id, measurement, notYetUpdatedAtSource = false) {
if (this.measurements[id]) {
const updatedMeasurement = {
...measurement,
modifiedTimestamp: Math.floor(Date.now() / 1000),
};
log.info(
`Updating internal measurement representation...`,
updatedMeasurement
);
this.measurements[id] = updatedMeasurement;
this.publish(
// Add an internal flag to say the measurement has not yet been updated at source.
this.EVENTS.MEASUREMENT_UPDATED,
{
source: measurement.source,
measurement: updatedMeasurement,
notYetUpdatedAtSource,
}
);
return updatedMeasurement.id;
}
}
/**
* Adds or update persisted measurements.
*
* @param {MeasurementSource} source The measurement source instance
* @param {string} definition The source definition
* @param {Measurement} measurement The source measurement
* @return {string} A measurement id
* Add a raw measurement into a source so that it may be
* Converted to/from annotation in the same way. E.g. import serialized data
* Of the same form as the measurement source.
* @param {MeasurementSource} source The measurement source instance.
* @param {string} definition The source definition you want to add the measurement to.
* @param {object} data The data you wish to add to the source.
* @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition.
*/
addOrUpdate(source, definition, sourceMeasurement) {
addRawMeasurement(
source,
definition,
data,
toMeasurementSchema,
dataSource = {}
) {
if (!this._isValidSource(source)) {
log.warn('Invalid source. Exiting early.');
return;
@@ -273,14 +348,8 @@ class MeasurementService {
let measurement = {};
try {
const sourceMappings = this.mappings[source.id];
const { toMeasurementSchema } = sourceMappings.find(
mapping => mapping.definition === definition
);
/* Convert measurement */
measurement = toMeasurementSchema(sourceMeasurement);
measurement = toMeasurementSchema(data);
/* Assign measurement source instance */
measurement.source = source;
} catch (error) {
@@ -298,7 +367,7 @@ class MeasurementService {
return;
}
let internalId = sourceMeasurement.id;
let internalId = data.id;
if (!internalId) {
internalId = guid();
log.warn(`Measurement ID not found. Generating UID: ${internalId}`);
@@ -316,49 +385,167 @@ class MeasurementService {
newMeasurement
);
this.measurements[internalId] = newMeasurement;
this._broadcastChange(
this.EVENTS.MEASUREMENT_UPDATED,
this.publish(this.EVENTS.MEASUREMENT_UPDATED, {
source,
newMeasurement
);
measurement: newMeasurement,
});
} else {
log.info(`Measurement added.`, newMeasurement);
this.measurements[internalId] = newMeasurement;
this._broadcastChange(
this.EVENTS.MEASUREMENT_ADDED,
this.publish(this.EVENTS.RAW_MEASUREMENT_ADDED, {
source,
newMeasurement
);
measurement: newMeasurement,
data,
dataSource,
});
}
return newMeasurement.id;
}
/**
* Subscribe to measurement updates.
* Adds or update persisted measurements.
*
* @param {string} eventName The name of the event
* @param {Function} callback Events callback
* @return {Object} Observable object with actions
* @param {MeasurementSource} source The measurement source instance
* @param {string} definition The source definition
* @param {Measurement} measurement The source measurement
* @return {string} A measurement id
*/
subscribe(eventName, callback) {
if (this._isValidEvent(eventName)) {
const listenerId = guid();
const subscription = { id: listenerId, callback };
console.info(`Subscribing to '${eventName}'.`);
if (Array.isArray(this.listeners[eventName])) {
this.listeners[eventName].push(subscription);
} else {
this.listeners[eventName] = [subscription];
}
return {
unsubscribe: () => this._unsubscribe(eventName, listenerId),
};
} else {
throw new Error(`Event ${eventName} not supported.`);
addOrUpdate(source, definition, sourceMeasurement) {
if (!this._isValidSource(source)) {
throw new Error('Invalid source.');
}
if (!definition) {
throw new Error('No source definition provided.');
}
const sourceInfo = this._getSourceInfo(source);
if (!this._sourceHasMappings(source)) {
throw new Error(
`No measurement mappings found for '${sourceInfo}' source. Exiting early.`
);
}
let measurement = {};
try {
const sourceMappings = this.mappings[source.id];
const { toMeasurementSchema } = sourceMappings.find(
mapping => mapping.definition === definition
);
/* Convert measurement */
measurement = toMeasurementSchema(sourceMeasurement);
/* Assign measurement source instance */
measurement.source = source;
} catch (error) {
throw new Error(
`Failed to map '${sourceInfo}' measurement for definition ${definition}:`,
error.message
);
}
if (!this._isValidMeasurement(measurement)) {
throw new Error(
`Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.`
);
}
let internalId = sourceMeasurement.id;
if (!internalId) {
internalId = guid();
log.info(`Measurement ID not found. Generating UID: ${internalId}`);
}
const newMeasurement = {
...measurement,
modifiedTimestamp: Math.floor(Date.now() / 1000),
id: internalId,
};
if (this.measurements[internalId]) {
log.info(
`Measurement already defined. Updating measurement.`,
newMeasurement
);
this.measurements[internalId] = newMeasurement;
this.publish(this.EVENTS.MEASUREMENT_UPDATED, {
source,
measurement: newMeasurement,
notYetUpdatedAtSource: false,
});
} else {
log.info('Measurement added.', newMeasurement);
this.measurements[internalId] = newMeasurement;
this.publish(this.EVENTS.MEASUREMENT_ADDED, {
source,
measurement: newMeasurement,
});
}
return newMeasurement.id;
}
/**
* Removes a measurement and broadcasts the removed event.
*
* @param {string} id The measurement id
* @param {MeasurementSource} source The measurement source instance
* @return {string} The removed measurement id
*/
remove(id, source) {
if (!id || !this.measurements[id]) {
log.warn(`No id provided, or unable to find measurement by id.`);
return;
}
delete this.measurements[id];
this.publish(this.EVENTS.MEASUREMENT_REMOVED, {
source,
measurement: id, // This is weird :shrug:
});
}
clearMeasurements() {
this.measurements = {};
this._jumpToMeasurementCache = {};
this.publish(this.EVENTS.MEASUREMENTS_CLEARED);
}
jumpToMeasurement(viewportIndex, id) {
const measurement = this.measurements[id];
if (!measurement) {
log.warn(`No id provided, or unable to find measurement by id.`);
return;
}
this._addJumpToMeasurement(viewportIndex, id);
const eventName = this.EVENTS.JUMP_TO_MEASUREMENT;
const hasListeners = Object.keys(this.listeners).length > 0;
const hasCallbacks = Array.isArray(this.listeners[eventName]);
if (hasListeners && hasCallbacks) {
this.listeners[eventName].forEach(listener => {
listener.callback({ viewportIndex, measurement });
});
}
}
_addJumpToMeasurement(viewportIndex, id) {
this._jumpToMeasurementCache[viewportIndex] = id;
}
getJumpToMeasurement(viewportIndex) {
return this._jumpToMeasurementCache[viewportIndex];
}
removeJumpToMeasurement(viewportIndex) {
delete this._jumpToMeasurementCache[viewportIndex];
}
_getMappingByMeasurementSource(measurementId, definition) {
@@ -370,12 +557,20 @@ class MeasurementService {
}
}
/**
* Clear all measurements and broadcasts cleared event.
*/
clear() {
this.measurements = {};
this.publish(this.EVENTS.MEASUREMENTS_CLEARED);
}
/**
* Get measurement mapping function if matching criteria.
*
* @param {MeasurementSource} source Measurement source instance
* @param {string} definition The source definition
* @param {string} measurement The measurement serice measurement
* @param {Measurement} measurement The measurement service measurement
* @return {Object} The mapping based on matched criteria
*/
_getMatchingMapping(source, definition, measurement) {
@@ -387,10 +582,27 @@ class MeasurementService {
/* Criteria Matching */
return sourceMappingsByDefinition.find(({ matchingCriteria }) => {
return (
if (matchingCriteria.type !== measurement.type) {
return false;
}
if (
matchingCriteria.properties &&
matchingCriteria.properties.every(name =>
measurement.hasOwnProperty(name)
)
) {
return true;
}
if (
measurement.points &&
measurement.points.length === matchingCriteria.points
);
) {
return true;
}
return false;
});
}
@@ -426,47 +638,6 @@ class MeasurementService {
);
}
/**
* Broadcasts measurement changes.
*
* @param {string} measurementId The measurement id
* @param {MeasurementSource} source The measurement source
* @param {string} eventName The event name
* @return void
*/
_broadcastChange(eventName, source, measurement) {
const hasListeners = Object.keys(this.listeners).length > 0;
const hasCallbacks = Array.isArray(this.listeners[eventName]);
if (hasListeners && hasCallbacks) {
this.listeners[eventName].forEach(listener => {
listener.callback({ source, measurement });
});
}
}
/**
* Unsubscribe to measurement updates.
*
* @param {string} eventName The name of the event
* @param {string} listenerId The listeners id
* @return void
*/
_unsubscribe(eventName, listenerId) {
if (!this.listeners[eventName]) {
return;
}
const listeners = this.listeners[eventName];
if (Array.isArray(listeners)) {
this.listeners[eventName] = listeners.filter(
({ id }) => id !== listenerId
);
} else {
this.listeners[eventName] = undefined;
}
}
/**
* Check if a given measurement data is valid.
*
@@ -489,10 +660,10 @@ class MeasurementService {
*
* @param {string} eventName The name of the event
* @return {boolean} Event name validation
*/
_isValidEvent(eventName) {
return Object.values(this.EVENTS).includes(eventName);
}
// */
// _isValidEvent(eventName) {
// return Object.values(this.EVENTS).includes(eventName);
// }
/**
* Converts object of objects to array.
@@ -13,7 +13,7 @@ describe('MeasurementService.js', () => {
let source;
let definition;
let matchingCriteria;
let toAnnotation;
let toSourceSchema;
let toMeasurement;
let annotation;
@@ -34,11 +34,20 @@ describe('MeasurementService.js', () => {
unit: 'mm',
area: 123,
type: measurementService.VALUE_TYPES.POLYLINE,
points: [{ x: 1, y: 2 }, { x: 1, y: 2 }],
points: [
{ x: 1, y: 2 },
{ x: 1, y: 2 },
],
source: source,
};
toAnnotation = () => annotation;
toMeasurement = () => measurement;
toSourceSchema = () => annotation;
toMeasurement = () => {
if (Object.keys(measurement).includes('invalidProperty')) {
throw new Error('Measurement does not match schema');
}
return measurement;
}
matchingCriteria = {
valueType: measurementService.VALUE_TYPES.POLYLINE,
points: 2,
@@ -52,16 +61,16 @@ describe('MeasurementService.js', () => {
measurementService.createSource('Testing', '1');
});
it('logs warning and return early if no name provided', () => {
measurementService.createSource(null, '1');
expect(log.warn.mock.calls.length).toBe(1);
it('throws Error if no name provided', () => {
expect(() => {
measurementService.createSource(null, '1')
}).toThrow(new Error('Source name not provided.'));
});
it('logs warning and return early if no version provided', () => {
measurementService.createSource('Testing', null);
expect(log.warn.mock.calls.length).toBe(1);
it('throws Error if no version provided', () => {
expect(() => {
measurementService.createSource('Testing', null)
}).toThrow(new Error('Source version not provided.'));
});
});
@@ -71,83 +80,84 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
});
it('logs warning and return early if no matching criteria provided', () => {
measurementService.addMapping(
source,
definition,
null,
toAnnotation,
toMeasurement
);
it('throws Error if invalid source provided', () => {
expect(() => {
const invalidSource = {};
expect(log.warn.mock.calls.length).toBe(1);
measurementService.addMapping(
invalidSource,
definition,
matchingCriteria,
toSourceSchema,
toMeasurement
);
}).toThrow(new Error('Invalid source.'));
});
it('logs warning and return early if invalid source provided', () => {
const invalidSoure = {};
measurementService.addMapping(
invalidSoure,
definition,
matchingCriteria,
toAnnotation,
toMeasurement
);
expect(log.warn.mock.calls.length).toBe(1);
it('throws Error if no matching criteria provided', () => {
expect(() => {
measurementService.addMapping(
source,
definition,
null,
toSourceSchema,
toMeasurement
);
}).toThrow(new Error('Matching criteria not provided.'));
});
it('logs warning and return early if no source provided', () => {
measurementService.addMapping(
null /* source */,
definition,
matchingCriteria,
toAnnotation,
toMeasurement
);
expect(log.warn.mock.calls.length).toBe(1);
it('throws Error if no source provided', () => {
expect(() => {
measurementService.addMapping(
null /* source */,
definition,
matchingCriteria,
toSourceSchema,
toMeasurement
);
}).toThrow(new Error('Invalid source.'));
});
it('logs warning and return early if no definition provided', () => {
measurementService.addMapping(
source,
null /* definition */,
matchingCriteria,
toAnnotation,
toMeasurement
);
expect(log.warn.mock.calls.length).toBe(1);
expect(() => {
measurementService.addMapping(
source,
null /* definition */,
matchingCriteria,
toSourceSchema,
toMeasurement
);
}).toThrow(new Error('Definition not provided.'));
});
it('logs warning and return early if no measurement mapping function provided', () => {
measurementService.addMapping(
source,
definition,
matchingCriteria,
null /* toAnnotation */,
toMeasurement
);
expect(log.warn.mock.calls.length).toBe(1);
it('throws Error if no measurement mapping function provided', () => {
expect(() => {
measurementService.addMapping(
source,
definition,
matchingCriteria,
null /* toSourceSchema */,
toMeasurement
);
}).toThrow(new Error('Mapping function to source schema not provided.'));
});
it('logs warning and return early if no annotation mapping function provided', () => {
measurementService.addMapping(
source,
definition,
matchingCriteria,
toAnnotation,
null /* toMeasurement */
);
expect(log.warn.mock.calls.length).toBe(1);
it('throws Error if no annotation mapping function provided', () => {
expect(() => {
measurementService.addMapping(
source,
definition,
matchingCriteria,
toSourceSchema,
null /* toMeasurement */
);
}).toThrow(new Error('Measurement mapping function not provided.'));
});
});
@@ -157,7 +167,7 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
const measurementId = source.addOrUpdate(definition, annotation);
@@ -171,7 +181,7 @@ describe('MeasurementService.js', () => {
source,
definition,
{},
toAnnotation,
toSourceSchema,
toMeasurement
);
const measurementId = source.addOrUpdate(definition, annotation);
@@ -193,7 +203,7 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
@@ -212,7 +222,7 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
@@ -232,7 +242,7 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
@@ -245,9 +255,9 @@ describe('MeasurementService.js', () => {
});
it('fails to add new measurements when no mapping', () => {
source.addOrUpdate(definition, measurement);
expect(log.warn.mock.calls.length).toBe(1);
expect(() => {
source.addOrUpdate(definition, measurement);
}).toThrow()
});
it('fails to add new measurements when invalid mapping function', () => {
@@ -255,13 +265,13 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
1 /* Invalid */
);
source.addOrUpdate(definition, measurement);
expect(log.warn.mock.calls.length).toBe(1);
expect(() => {
source.addOrUpdate(definition, measurement);
}).toThrow()
});
it('adds new measurement with custom id', () => {
@@ -271,7 +281,7 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
@@ -288,28 +298,28 @@ describe('MeasurementService.js', () => {
expect(newMeasurement).toEqual(savedMeasurement);
});
it('logs warning and return if adding invalid measurement', () => {
it('throws Error if adding invalid measurement', () => {
measurement.invalidProperty = {};
measurementService.addMapping(
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
source.addOrUpdate(definition, measurement);
expect(log.warn.mock.calls.length).toBe(2);
expect(() => {
source.addOrUpdate(definition, measurement);
}).toThrow()
});
it('updates existent measurement', () => {
it('updates existing measurement', () => {
measurementService.addMapping(
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
@@ -330,7 +340,7 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
@@ -354,7 +364,7 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
@@ -381,7 +391,7 @@ describe('MeasurementService.js', () => {
source,
definition,
matchingCriteria,
toAnnotation,
toSourceSchema,
toMeasurement
);
@@ -0,0 +1,3 @@
import pubSubServiceInterface from './pubSubServiceInterface';
export { pubSubServiceInterface };
@@ -0,0 +1,88 @@
import guid from '../../utils/guid';
/**
* Consumer must implement:
* this.listeners = {}
* this.EVENTS = { "EVENT_KEY": "EVENT_VALUE" }
*/
export default {
subscribe,
publish,
_unsubscribe,
_isValidEvent,
};
/**
* Subscribe to updates.
*
* @param {string} eventName The name of the event
* @param {Function} callback Events callback
* @return {Object} Observable object with actions
*/
function subscribe(eventName, callback) {
if (this._isValidEvent(eventName)) {
const listenerId = guid();
const subscription = { id: listenerId, callback };
console.info(`Subscribing to '${eventName}'.`);
if (Array.isArray(this.listeners[eventName])) {
this.listeners[eventName].push(subscription);
} else {
this.listeners[eventName] = [subscription];
}
return {
unsubscribe: () => this._unsubscribe(eventName, listenerId),
};
} else {
throw new Error(`Event ${eventName} not supported.`);
}
}
/**
* Unsubscribe to measurement updates.
*
* @param {string} eventName The name of the event
* @param {string} listenerId The listeners id
* @return void
*/
function _unsubscribe(eventName, listenerId) {
if (!this.listeners[eventName]) {
return;
}
const listeners = this.listeners[eventName];
if (Array.isArray(listeners)) {
this.listeners[eventName] = listeners.filter(({ id }) => id !== listenerId);
} else {
this.listeners[eventName] = undefined;
}
}
/**
* Check if a given event is valid.
*
* @param {string} eventName The name of the event
* @return {boolean} Event name validation
*/
function _isValidEvent(eventName) {
return Object.values(this.EVENTS).includes(eventName);
}
/**
* Broadcasts changes.
*
* @param {string} eventName - The event name
* @param {func} callbackProps - Properties to pass callback
* @return void
*/
function publish(eventName, callbackProps) {
const hasListeners = Object.keys(this.listeners).length > 0;
const hasCallbacks = Array.isArray(this.listeners[eventName]);
if (hasListeners && hasCallbacks) {
this.listeners[eventName].forEach(listener => {
listener.callback(callbackProps);
});
}
}
@@ -0,0 +1,115 @@
import { api } from 'dicomweb-client';
/**
* An implementation of the static wado client, that fetches data from
* a static response rather than actually doing real queries. This allows
* fast encoding of test data, but because it is static, anything actually
* performing searches doesn't work. This version fixes the query issue
* by manually implementing a query option.
*/
export default class StaticWadoClient extends api.DICOMwebClient {
static filterKeys = {
"StudyInstanceUID": "0020000D",
"PatientName": "00100010",
"00100020": "mrn",
"PatientID": "00100020",
"StudyDescription": "00081030",
"StudyDate": "00080020",
"ModalitiesInStudy": "00080061",
AccessionNumber: "00080050",
};
constructor(qidoConfig) {
super(qidoConfig);
this.staticWado = qidoConfig.staticWado;
}
/**
* Replace the search for studies remote query with a local version which
* retrieves a complete query list and then sub-selects from it locally.
* @param {*} options
* @returns
*/
async searchForStudies(options) {
if (!this.staticWado) return super.searchForStudies(options);
let searchResult = await super.searchForStudies(options);
const { queryParams } = options;
if (!queryParams) return searchResult;
const filtered = searchResult.filter(study => {
for (const key of Object.keys(StaticWadoClient.filterKeys)) {
if (!this.filterItem(key, queryParams, study)) return false;
}
return true;
});
return filtered;
}
/**
* Compares values, matching any instance of desired to any instance of
* actual by recursively go through the paired set of values. That is,
* this is O(m*n) where m is how many items in desired and n is the length of actual
* Then, at the individual item node, compares the Alphabetic name if present,
* and does a sub-string matching on string values, and otherwise does an
* exact match comparison.
*
* @param {*} desired
* @param {*} actual
* @returns true if the values match
*/
compareValues(desired, actual) {
if (Array.isArray(desired)) {
return desired.find(item => this.compareValues(item, actual));
}
if (Array.isArray(actual)) {
return actual.find(actualItem => this.compareValues(desired, actualItem));
}
if (actual && actual.Alphabetic) {
actual = actual.Alphabetic;
}
if (typeof (actual) == 'string') {
if (actual.length === 0) return true;
if (desired.length === 0 || desired === '*') return true;
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`)
return actual.indexOf(desired.substring(1, desired.length - 1)) != -1;
} else if (desired[desired.length - 1] === '*') {
return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
} else if (desired[0] === '*') {
return actual.indexOf(desired.substring(1)) === actual.length - desired.length + 1;
}
}
return desired === actual;
}
/** Compares a pair of dates to see if the value is within the range */
compareDateRange(range, value) {
if (!value) return true;
const dash = range.indexOf('-');
if (dash === -1) return this.compareValues(range, value);
const start = range.substring(0, dash);
const end = range.substring(dash + 1);
return (!start || value >= start) &&
(!end || value <= end);
}
/**
* Filters the return list by the query parameters.
*
* @param {*} key
* @param {*} queryParams
* @param {*} study
* @returns
*/
filterItem(key, queryParams, study) {
const altKey = StaticWadoClient.filterKeys[key] || key;
if (!queryParams) return true;
const testValue = queryParams[key] || queryParams[altKey];
if (!testValue) return true;
const valueElem = study[key] || study[altKey];
if (!valueElem) return false;
if (valueElem.vr == 'DA') return this.compareDateRange(testValue, valueElem.Value[0]);
const value = valueElem.Value;
return this.compareValues(testValue, value) && true;
}
}
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff. Show more