Compare commits

...
Author SHA1 Message Date
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
ohif-bot ea0963d75f chore(release): publish [skip ci]
- @ohif/extension-dicom-rt@0.6.14
 - @ohif/extension-dicom-segmentation@0.6.11
 - @ohif/extension-vtk@1.11.16
 - @ohif/core@2.13.15
 - @ohif/ui@1.9.3
 - @ohif/viewer@4.9.24
2021-09-03 21:01:01 +00:00
Igor Octaviano 46d5718011 Merge pull request #2491 from OHIF/feat/ohif-2482
OHIF-2482: Add badge numbers for seg and rtstruct
2021-09-03 17:57:42 -03:00
Davide Punzo f8740e04f3 Merge branch 'master' into feat/ohif-2482 2021-09-02 16:08:37 +02:00
ohif-bot 669717d27b chore(release): publish [skip ci]
- @ohif/extension-vtk@1.11.15
 - @ohif/core@2.13.14
 - @ohif/viewer@4.9.23
2021-08-30 13:04:58 +00:00
Alireza 715dfc556a fix: MPR metadata parsing for spacings (#2517)
* fix: MPR metadata parsing for spacings

* fix: OHIF Metadata provider for validNumbers for imagePlaneModule
2021-08-30 09:01:46 -04:00
ohif-bot cafb7d3f7b chore(release): publish [skip ci]
- @ohif/viewer@4.9.22
2021-08-19 13:05:04 +00:00
matthiasg 5f92f6ec7f fix: Remove internet dependency for workbox (addressing #2109) (#2151)
* Ignoring *.js.map files as generated in the extensions directories for example

* Fix for #2109. Removed internet dependency to google workbox
2021-08-19 15:00:20 +02:00
Matthias SchmiederandMatthias Schmieder eea610dfe8 fix CVE-2019-5021 caused by nginx-apline (#2514)
Co-authored-by: Matthias Schmieder <matthias.schmieder@stryker.com>
2021-08-19 13:42:04 +02:00
Erik Ziegler e8733a9bd9 Update CITATION.cff (#2504) 2021-08-04 17:16:35 +02:00
Erik Ziegler d292931073 Create CITATION.cff (#2503) 2021-08-04 15:43:10 +02:00
igoroctaviano f6a0db753a Merge branch 'master' of github.com:OHIF/Viewers into feat/ohif-2482 2021-07-29 09:19:42 -03:00
igoroctaviano 8518e179ee Extract event 2021-07-29 09:18:31 -03:00
igoroctaviano 3f40cdced7 Add badge numbers for seg and rtstruct 2021-07-28 22:15:05 -03:00
ohif-bot 4cf17185da chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.10.10
 - @ohif/viewer@4.9.21
2021-07-26 11:44:06 +00:00
b8ae92c2d4 fix: changed new image handler conditional for 0 edge case (#2338)
Co-authored-by: Michael Andersen <Michael.Andersen@rmp.uhn.ca>
Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
2021-07-26 13:35:37 +02:00
Woonchan Cho bad67020cc chore(#2483) make client-side code debuggable right from the source code (#2484) 2021-07-26 13:34:55 +02:00
ohif-bot 2afcd498f3 chore(release): publish [skip ci]
- @ohif/extension-debugging@0.2.2
 - @ohif/extension-dicom-tag-browser@0.2.11
 - @ohif/extension-vtk@1.11.14
 - @ohif/core@2.13.13
 - @ohif/viewer@4.9.20
2021-06-03 20:56:43 +00:00
Igor Octaviano ce4c3d5360 Merge pull request #2408 from OHIF/feat/idc-2374
IDC-2374: Exponential backoff and retry after 500 error
2021-06-03 17:52:49 -03:00
igoroctaviano d575e36879 Expose retryable option 2021-06-03 17:24:39 -03:00
igoroctaviano 98fcabfcb5 Resolve opening connection before previsouly setting headers 2021-06-03 17:05:45 -03:00
Davide Punzo 1e2773345a Merge branch 'feat/idc-2374' of https://github.com/OHIF/Viewers into feat/idc-2374 2021-06-03 16:25:29 +02:00
Davide Punzo f847633cf3 update dicomweb-client to version 0.8.1 2021-06-03 16:25:06 +02:00
igoroctaviano bd2ddd7702 Remove metadata check 2021-06-03 07:20:49 -03:00
igoroctaviano 26b7b34bd4 Add check for metadata 2021-06-03 07:10:33 -03:00
Davide Punzo 6f3e446a79 Merge branch 'master' into feat/idc-2374 2021-06-03 10:11:45 +02:00
ohif-bot f0d7f2e6c7 chore(release): publish [skip ci]
- @ohif/viewer@4.9.19
2021-06-03 08:07:04 +00:00
Davide Punzo c8f7abfd53 Merge branch 'master' into feat/idc-2374 2021-06-03 10:04:02 +02:00
Davide Punzo 2b29ff1820 clean back default config (#2438) 2021-06-03 10:03:49 +02:00
Davide Punzo 5627e9c919 update dicomweb-client to version 0.8.0 2021-06-03 09:37:57 +02:00
Davide Punzo 582b713b84 Merge branch 'master' into feat/idc-2374 2021-06-03 09:25:50 +02:00
ohif-bot 5eb8e58b65 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.10.9
 - @ohif/extension-dicom-html@1.3.10
 - @ohif/extension-dicom-rt@0.6.13
 - @ohif/extension-dicom-segmentation@0.6.10
 - @ohif/extension-dicom-tag-browser@0.2.10
 - @ohif/extension-vtk@1.11.13
 - @ohif/core@2.13.12
 - @ohif/viewer@4.9.18
2021-06-02 14:01:32 +00:00
Igor Octaviano 403688b18c fix(dicom-html): Add parsed dicom meta info section (#2419)
* Add dicom meta info section to dicom sr html
* Update dependency: dcmjs@0.18.8
2021-06-02 15:57:56 +02:00
ohif-bot f0d13a7acd chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.10.8
 - @ohif/extension-dicom-rt@0.6.12
 - @ohif/extension-dicom-segmentation@0.6.9
 - @ohif/extension-vtk@1.11.12
 - @ohif/core@2.13.11
 - @ohif/ui@1.9.2
 - @ohif/viewer@4.9.17
2021-05-17 16:51:58 +00:00
Davide Punzo 78f1a7f3e5 Re #2259: UI refinements for warnings (#2397)
* Re #2259: highlight series thumbnails border for active series (series in the active viewport)
* Re #2259: add the warning icon also on the active viewport as an overlay
2021-05-17 18:48:32 +02:00
ohif-bot a57ab4d12a chore(release): publish [skip ci]
- @ohif/viewer@4.9.16
2021-05-14 01:18:42 +00:00
Igor Octaviano 3b6c6374e3 Merge pull request #2409 from OHIF/feat/add-option-to-disable-servers-cache
IDC: Add option to disable local storage of servers
2021-05-13 22:15:59 -03:00
igoroctaviano 4b508b0c25 Update config property 2021-05-13 21:56:19 -03:00
igoroctaviano 1cbef867ce Add option to disable local storage cache of servers 2021-05-13 21:42:46 -03:00
igoroctaviano 05444f48d4 Improve docs remove env variables 2021-05-13 09:30:52 -03:00
igoroctaviano 2ea227ca06 Extract hook and add config and documentation 2021-05-13 09:06:11 -03:00
igoroctaviano 35aaad175c Update naming 2021-05-13 08:11:09 -03:00
igoroctaviano 863fe05907 Update naming 2021-05-13 08:11:03 -03:00
igoroctaviano 1c9560044f Implement handlers to retry metadata requests 2021-05-13 08:10:53 -03:00
ohif-bot 343f06c57d chore(release): publish [skip ci]
- @ohif/extension-dicom-html@1.3.9
 - @ohif/extension-dicom-rt@0.6.11
 - @ohif/extension-dicom-segmentation@0.6.8
 - @ohif/extension-dicom-tag-browser@0.2.9
 - @ohif/extension-vtk@1.11.11
 - @ohif/core@2.13.10
 - @ohif/viewer@4.9.15
2021-04-22 03:42:37 +00:00
dannyrb e2bb647386 Merge branch 'master' of github.com:OHIF/Viewers into ups-master 2021-04-21 23:29:54 -04:00
dannyrb a3cb9dab57 remove sitemap dep from older version 2021-04-21 23:29:41 -04:00
ohif-bot 410ab028c7 chore(release): publish [skip ci]
- @ohif/extension-dicom-html@1.3.8
 - @ohif/extension-dicom-rt@0.6.10
 - @ohif/extension-dicom-segmentation@0.6.7
 - @ohif/extension-dicom-tag-browser@0.2.8
 - @ohif/extension-vtk@1.11.10
 - @ohif/core@2.13.9
 - @ohif/viewer@4.9.14
2021-04-22 03:27:52 +00:00
dannyrb 54dd738f61 Merge branch 'master' of github.com:OHIF/Viewers into ups-master 2021-04-21 23:24:39 -04:00
dannyrb 6f16c7e48d Remove broken plugin? Seems to have broken child dep 2021-04-21 23:24:28 -04:00
ohif-bot 82ddd95cf0 chore(release): publish [skip ci]
- @ohif/extension-dicom-html@1.3.7
 - @ohif/extension-dicom-rt@0.6.9
 - @ohif/extension-dicom-segmentation@0.6.6
 - @ohif/extension-dicom-tag-browser@0.2.7
 - @ohif/extension-vtk@1.11.9
 - @ohif/core@2.13.8
 - @ohif/viewer@4.9.13
2021-04-21 14:14:00 +00:00
Davide Punzo 2b658fae7e IDC2345: update dcmjs to version 0.18.5 (#2378) 2021-04-21 16:10:49 +02:00
ohif-bot a822151e92 chore(release): publish [skip ci]
- @ohif/extension-dicom-html@1.3.6
 - @ohif/extension-vtk@1.11.8
 - @ohif/core@2.13.7
 - @ohif/viewer@4.9.12
2021-04-21 11:17:09 +00:00
Igor Octaviano 0ae49c6fdc IDC-2348: Add acquisition storage sr sopclass to sr html ext (#2352)
* Add acquisition context storage sr sop to html ext
2021-04-21 13:12:54 +02:00
ohif-bot 1d069d2da3 chore(release): publish [skip ci]
- @ohif/extension-dicom-segmentation@0.6.5
 - @ohif/extension-vtk@1.11.7
 - @ohif/core@2.13.6
 - @ohif/viewer@4.9.11
2021-04-21 10:22:55 +00:00
Igor Octaviano 274342a5e1 Merge pull request #2369 from Punzo/IDC2361
Re #2361: fix seg combobox filling
2021-04-21 07:19:32 -03:00
Davide Punzo 21a5d1bfc4 update 2021-04-21 10:40:02 +02:00
Davide Punzo e421bc7df9 update 2021-04-20 16:39:04 +02:00
Davide Punzo e41ba2bfde IDC2361: fix seg combobox filling 2021-04-19 19:42:30 +02:00
ohif-bot d2e75b0f20 chore(release): publish [skip ci]
- @ohif/extension-dicom-segmentation@0.6.4
 - @ohif/extension-vtk@1.11.6
 - @ohif/core@2.13.5
 - @ohif/viewer@4.9.10
2021-04-16 18:18:27 +00:00
Davide Punzo 07b272e024 IDC2345: implement thumbnails footnotes UI for segmentation series inconsistencies warning (#2364)
* IDC2345: implement thumbnails footnotes UI for segmentation series inconsistencies warning
2021-04-16 20:14:04 +02:00
ohif-bot a08caa9bdb chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.10.7
 - @ohif/extension-dicom-html@1.3.5
 - @ohif/extension-dicom-rt@0.6.8
 - @ohif/extension-dicom-segmentation@0.6.3
 - @ohif/extension-dicom-tag-browser@0.2.6
 - @ohif/extension-vtk@1.11.5
 - @ohif/core@2.13.4
 - @ohif/viewer@4.9.9
2021-04-15 13:13:50 +00:00
Igor Octaviano 29afddb3a6 Merge pull request #2363 from OHIF/fix/window-level-issues
fix(MetadataProvider): Update metadata provider to resolve issues after dicomweb update/new return value
2021-04-15 10:10:48 -03:00
igoroctaviano f85f856734 Comment cache 2021-04-15 09:40:37 -03:00
igoroctaviano c6bc19c002 Rollback circle ci test 2021-04-15 09:29:50 -03:00
igoroctaviano 538555f000 Merge branch 'master' of github.com:OHIF/Viewers into fix/window-level-issues 2021-04-15 09:17:54 -03:00
igoroctaviano 8681036b21 fix(wl): fix metadata provider window level issues after dicomweb update (string vs number) 2021-04-15 09:15:42 -03:00
Davide Punzo e55b909ca6 IDC2345: Update dcmjs to version 0.18.4 (#2362) 2021-04-15 13:33:13 +02:00
Davide Punzo 4bb36704ee IDC2346: fix images sorting (#2358) 2021-04-14 19:43:58 +02:00
Davide Punzo 9bbdb35d88 IDC2259: do not check uniform spacing in the 4th dimension for 4D datasets (#2356) 2021-04-13 12:57:28 +02:00
qqalexqq 2f443fa2ef fix: Update Dockerfile node version - fix missing dependencies (#2349)
There were some utilities missing: python, make, g++. Also node version was too old - current node builds without problems.
2021-04-05 09:47:57 +02:00
ohif-bot 6d7d7da287 chore(release): publish [skip ci]
- @ohif/extension-dicom-segmentation@0.6.2
 - @ohif/viewer@4.9.8
2021-03-31 10:24:08 +00:00
Davide Punzo 1f6751f95b IDC2332 (#2341)
1) fix the loading of the segmentations when filtering study with ?seriesInstanceUID= syntax
2) add time in the dates of the items of the segmentation combobox list
3) order segmentation combobox list in reverse time order
2021-03-31 12:20:34 +02:00
ohif-bot 92081a097d chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.10.6
 - @ohif/viewer@4.9.7
2021-03-26 18:38:58 +00:00
Igor Octaviano ba1a33c3ac Merge pull request #2335 from OHIF/idc-2334
IDC-2334: Resolve choppy scrolling in 2D mode
2021-03-26 15:36:07 -03:00
igoroctaviano 193ff12b6e Fix scroll speed 2021-03-26 13:27:51 -03:00
ohif-bot 3296ffaa07 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.11.4
 - @ohif/core@2.13.3
 - @ohif/ui@1.9.1
 - @ohif/viewer@4.9.6
2021-03-26 10:35:50 +00:00
Davide Punzo 0456d92462 IDC 2259: add warning UI for series inconsistencies (#2331)
IDC 2259: add warning UI for series inconsistencies

remove VTKMPRToolbarButton component memoization (it is not anymore necessary)
2021-03-26 11:32:10 +01:00
ohif-bot db3b15e537 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.11.3
 - @ohif/core@2.13.2
 - @ohif/viewer@4.9.5
2021-03-25 16:16:14 +00:00
Igor OctavianoandDavide Punzo 6e723ebced fix: IDC-2288: Update toolbar tabs after derived displayset loaded (#2307)
* Update toolbar tabs after derived displayset loaded

* trigger CI

Co-authored-by: Davide Punzo <punzodavide@hotmail.it>
2021-03-25 17:12:20 +01:00
174 changed files with 15046 additions and 930 deletions

No files matched your search

+35 -27
View File
@@ -48,12 +48,12 @@ jobs:
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
# - restore_cache:
# name: Restore Yarn and Cypress Package Cache
# keys:
# # when lock file changes, use increasingly general patterns to restore cache
# - yarn-packages-{{ checksum "yarn.lock" }}
# - yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
@@ -107,12 +107,12 @@ jobs:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
# - restore_cache:
# name: Restore Yarn and Cypress Package Cache
# keys:
# # when lock file changes, use increasingly general patterns to restore cache
# - yarn-packages-{{ checksum "yarn.lock" }}
# - yarn-packages-
- run:
name: Install Dependencies
@@ -143,12 +143,12 @@ jobs:
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
# - restore_cache:
# name: Restore Yarn and Cypress Package Cache
# keys:
# # when lock file changes, use increasingly general patterns to restore cache
# - yarn-packages-{{ checksum "yarn.lock" }}
# - yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
@@ -237,11 +237,11 @@ jobs:
post:
- git fetch --all
# Use increasingly general patterns to restore cache
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
# - restore_cache:
# name: Restore Yarn and Cypress Package Cache
# keys:
# - yarn-packages-{{ checksum "yarn.lock" }}
# - yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
@@ -346,7 +346,9 @@ workflows:
record: true
store_artifacts: true
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
yarn run build
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
wait-on: 'http://localhost:3000'
@@ -372,7 +374,9 @@ workflows:
record: true
store_artifacts: true
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build:package
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
wait-on: 'http://localhost:3000'
@@ -403,7 +407,9 @@ workflows:
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
@@ -480,7 +486,9 @@ workflows:
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build
build:
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
+5
View File
@@ -9,6 +9,10 @@ src/version.js
junit.xml
coverage/
.docz/
*.js.map
.yarn
platform/docs/.docusaurus
platform/ui/storybook-static
# YALC (for Erik)
.yalc
@@ -34,3 +38,4 @@ screenshots/
# Locize settings
.locize
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
+24
View File
@@ -0,0 +1,24 @@
cff-version: 1.2.0
message: "If you use this software, please cite it as below."
authors:
- family-names: "Ziegler"
given-names: "Erik"
orcid: "https://orcid.org/0000-0003-1857-8129"
- family-names: "Urban"
given-names: "Trinity"
- family-names: "Brown"
given-names: "Danny"
- family-names: "Petts"
given-names: "James"
- family-names: "Pieper"
given-names: "Steve D"
- family-names: "Lewis"
given-names: "Rob"
- family-names: "Hafey"
given-names: "Chris"
- family-names: "Harris"
given-names: "Gordon J"
title: "Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework for Building Web-Based Imaging Applications to Support Cancer Research"
doi: 10.1200/CCI.19.00131
version: 1.0.0
url: "https://github.com/OHIF/Viewers"
+3 -2
View File
@@ -21,7 +21,7 @@
# Stage 1: Build the application
# docker build -t ohif/viewer:latest .
FROM node:10.16.3-slim as builder
FROM node:15.13.0-slim as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
@@ -39,6 +39,7 @@ COPY package.json /usr/src/app/package.json
COPY postcss.config.js /usr/src/app/postcss.config.js
COPY yarn.lock /usr/src/app/yarn.lock
RUN apt-get update && apt-get install -y python make g++
# Run the install before copying the rest of the files
RUN yarn config set workspaces-experimental true
RUN yarn install
@@ -52,7 +53,7 @@ RUN yarn run build
# Stage 2: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux
FROM nginx:1.15.5-alpine
FROM nginx:1.21.1-alpine
RUN apk add --no-cache bash
RUN rm -rf /etc/nginx/conf.d
COPY .docker/Viewer-v2.x /etc/nginx/conf.d
+2 -2
View File
@@ -8,7 +8,6 @@
"-fontsettings",
"github",
"ga",
"sitemap",
"anchors",
"versions"
],
@@ -24,7 +23,8 @@
"token": "UA-110573590-2"
},
"sitemap": {
"hostname": "https://docs.ohif.org"
"hostname": "https://docs.ohif.org",
"todo": "sitemap plugin removed; causing build errors"
},
"versions": {
"gitbookConfigURL": "https://raw.githubusercontent.com/OHIF/Viewers/master/docs/book.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,
},
};
```
+2 -2
View File
@@ -8,7 +8,6 @@
"-fontsettings",
"github",
"ga",
"sitemap",
"anchors",
"versions"
],
@@ -24,7 +23,8 @@
"token": "UA-110573590-2"
},
"sitemap": {
"hostname": "https://docs.ohif.org"
"hostname": "https://docs.ohif.org",
"todo": "sitemap plugin removed; causing build errors"
},
"versions": {
"gitbookConfigURL": "https://raw.githubusercontent.com/OHIF/Viewers/master/docs/book.json",
+68
View File
@@ -3,6 +3,74 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.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)
### Bug Fixes
* changed new image handler conditional for 0 edge case ([#2338](https://github.com/OHIF/Viewers/issues/2338)) ([b8ae92c](https://github.com/OHIF/Viewers/commit/b8ae92c2d4f733b3ca89826118029685650b396a))
## [2.10.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.8...@ohif/extension-cornerstone@2.10.9) (2021-06-02)
### Bug Fixes
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
## [2.10.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.7...@ohif/extension-cornerstone@2.10.8) (2021-05-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.10.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.6...@ohif/extension-cornerstone@2.10.7) (2021-04-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.10.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.5...@ohif/extension-cornerstone@2.10.6) (2021-03-26)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.10.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.4...@ohif/extension-cornerstone@2.10.5) (2021-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
+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.5",
"version": "2.12.0",
"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.3",
"dicom-parser": "^1.8.3",
"cornerstone-core": "^2.5.0",
"cornerstone-math": "^0.1.9",
"cornerstone-tools": "^6.0.0",
"cornerstone-wado-image-loader": "^4.0.3",
"dcmjs": "0.18.8",
"dicom-parser": "^1.8.9",
"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"
}
}
@@ -53,7 +53,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,
@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay';
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
import OHIF from '@ohif/core';
import PropTypes from 'prop-types';
@@ -14,6 +15,7 @@ class OHIFCornerstoneViewport extends Component {
static defaultProps = {
customProps: {},
isStackPrefetchEnabled: true,
};
static propTypes = {
@@ -22,6 +24,8 @@ class OHIFCornerstoneViewport extends Component {
viewportIndex: PropTypes.number,
children: PropTypes.node,
customProps: PropTypes.object,
stackPrefetch: PropTypes.object,
isStackPrefetchEnabled: PropTypes.bool,
};
static id = 'OHIFCornerstoneViewport';
@@ -179,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
if (
displaySet.displaySetInstanceUID !==
prevDisplaySet.displaySetInstanceUID ||
prevDisplaySet.displaySetInstanceUID ||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
displaySet.frameIndex !== prevDisplaySet.frameIndex
) {
@@ -194,6 +198,7 @@ class OHIFCornerstoneViewport extends Component {
return null;
}
const { viewportIndex } = this.props;
const { inconsistencyWarnings } = this.props.viewportData.displaySet;
const {
imageIds,
currentImageIdIndex,
@@ -219,7 +224,7 @@ class OHIFCornerstoneViewport extends Component {
const { displaySet } = this.props.viewportData;
const { StudyInstanceUID } = displaySet;
if (currentImageIdIndex > 0) {
if (currentImageIdIndex >= 0) {
this.props.onNewImage({
StudyInstanceUID,
SOPInstanceUID: sopInstanceUid,
@@ -229,18 +234,29 @@ class OHIFCornerstoneViewport extends Component {
}
};
const warningsOverlay = props => {
return (
<OHIFCornerstoneViewportOverlay
{...props}
inconsistencyWarnings={inconsistencyWarnings}
/>
);
};
return (
<>
<ConnectedCornerstoneViewport
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={currentImageIdIndex}
onNewImage={newImageHandler}
onNewImageDebounceTime={700}
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}
+8 -1
View File
@@ -268,6 +268,7 @@ const commandsModule = ({ servicesManager }) => {
SOPInstanceUID,
frameIndex,
activeViewportIndex,
refreshViewports = true,
}) => {
const study = studyMetadataManager.get(StudyInstanceUID);
@@ -278,6 +279,10 @@ const commandsModule = ({ servicesManager }) => {
);
});
if (!displaySet) {
return;
}
displaySet.SOPInstanceUID = SOPInstanceUID;
displaySet.frameIndex = frameIndex;
@@ -285,7 +290,9 @@ const commandsModule = ({ servicesManager }) => {
setViewportSpecificData(activeViewportIndex, displaySet)
);
refreshCornerstoneViewports();
if (refreshViewports) {
refreshCornerstoneViewports();
}
},
};
@@ -0,0 +1,69 @@
.imageViewerViewport.empty ~ .OHIFCornerstoneViewportOverlay {
display: none;
}
.OHIFCornerstoneViewportOverlay {
color: #9ccef9;
}
.OHIFCornerstoneViewportOverlay .overlay-element {
position: absolute;
font-weight: 400;
text-shadow: 1px 1px #000;
pointer-events: none;
}
.OHIFCornerstoneViewportOverlay .top-left {
top: 20px;
left: 20px;
}
.OHIFCornerstoneViewportOverlay .top-center {
top: 20px;
padding-top: 20px;
width: 100%;
text-align: center;
}
.OHIFCornerstoneViewportOverlay .top-right {
top: 20px;
right: 20px;
text-align: right;
}
.OHIFCornerstoneViewportOverlay .bottom-left {
bottom: 20px;
left: 20px;
}
.OHIFCornerstoneViewportOverlay .bottom-left2 {
bottom: 140px;
left: 20px;
}
.OHIFCornerstoneViewportOverlay .bottom-right {
bottom: 20px;
right: 20px;
text-align: right;
}
.OHIFCornerstoneViewportOverlay.controlsVisible .topright,
.OHIFCornerstoneViewportOverlay.controlsVisible .bottomright {
right: calc(20px + 19px);
}
.OHIFCornerstoneViewportOverlay .warning {
position: absolute;
font-weight: 400;
text-shadow: 1px 1px #000;
pointer-events: inherit;
}
.OHIFCornerstoneViewportOverlay .warning svg {
opacity: 1;
color: #e29e4a;
fill: #e29e4a;
stroke: #e29e4a;
width: 32px;
height: 28px;
}
.OHIFCornerstoneViewportOverlay svg {
color: #9ccef9;
fill: #9ccef9;
stroke: #9ccef9;
background-color: transparent;
margin: 2px;
width: 18px;
height: 18px;
}
@@ -0,0 +1,167 @@
import { PureComponent } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import './OHIFCornerstoneViewportOverlay.css';
import {
isValidNumber,
formatNumberPrecision,
formatDICOMDate,
formatDICOMTime,
formatPN,
getCompression
} from '../utils/formatStudy';
import classNames from 'classnames';
import { Icon } from '@ohif/ui/src/elements/Icon';
import { Tooltip } from '@ohif/ui/src/components/tooltip';
import { OverlayTrigger } from '@ohif/ui/src/components/overlayTrigger';
class OHIFCornerstoneViewportOverlay extends PureComponent {
static propTypes = {
scale: PropTypes.number.isRequired,
windowWidth: PropTypes.oneOfType([
PropTypes.number.isRequired,
PropTypes.string.isRequired,
]),
windowCenter: PropTypes.oneOfType([
PropTypes.number.isRequired,
PropTypes.string.isRequired,
]),
imageId: PropTypes.string.isRequired,
imageIndex: PropTypes.number.isRequired,
stackSize: PropTypes.number.isRequired,
inconsistencyWarnings: PropTypes.array.isRequired
};
render() {
const { imageId, scale, windowWidth, windowCenter, inconsistencyWarnings } = this.props;
if (!imageId) {
return null;
}
const zoomPercentage = formatNumberPrecision(scale * 100, 0);
const seriesMetadata =
cornerstone.metaData.get('generalSeriesModule', imageId) || {};
const imagePlaneModule =
cornerstone.metaData.get('imagePlaneModule', imageId) || {};
const { rows, columns, sliceThickness, sliceLocation } = imagePlaneModule;
const { seriesNumber, seriesDescription } = seriesMetadata;
const generalStudyModule =
cornerstone.metaData.get('generalStudyModule', imageId) || {};
const { studyDate, studyTime, studyDescription } = generalStudyModule;
const patientModule =
cornerstone.metaData.get('patientModule', imageId) || {};
const { patientId, patientName } = patientModule;
const generalImageModule =
cornerstone.metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
const cineModule = cornerstone.metaData.get('cineModule', imageId) || {};
const { frameTime } = cineModule;
const frameRate = formatNumberPrecision(1000 / frameTime, 1);
const compression = getCompression(imageId);
const wwwc = `W: ${
windowWidth.toFixed ? windowWidth.toFixed(0) : windowWidth
} L: ${windowWidth.toFixed ? windowCenter.toFixed(0) : windowCenter}`;
const imageDimensions = `${columns} x ${rows}`;
const { imageIndex, stackSize } = this.props;
const inconsistencyWarningsOn = inconsistencyWarnings && inconsistencyWarnings.length !== 0 ? true : false;
const getWarningContent = (warningList) => {
if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => {
return <li key={index}>{warn}</li>;
});
return <ol>{listedWarnings}</ol>;
} else {
return <React.Fragment>{warningList}</React.Fragment>;
}
};
const getWarningInfo = (seriesNumber, inconsistencyWarnings) => {
return(
<React.Fragment>
{inconsistencyWarnings.length != 0 ? (
<OverlayTrigger
key={seriesNumber}
placement="left"
overlay={
<Tooltip
placement="left"
className="in tooltip-warning"
id="tooltip-left"
>
<div className="warningTitle">Series Inconsistencies</div>
<div className="warningContent">{getWarningContent(inconsistencyWarnings)}</div>
</Tooltip>
}
>
<div className={classNames('warning')}>
<span className="warning-icon">
<Icon name="exclamation-triangle" />
</span>
</div>
</OverlayTrigger>
) : (
<React.Fragment></React.Fragment>
)}
</React.Fragment>
);
};
const normal = (
<React.Fragment>
<div className="top-left overlay-element">
<div>{formatPN(patientName)}</div>
<div>{patientId}</div>
</div>
<div className="top-right overlay-element">
<div>{studyDescription}</div>
<div>
{formatDICOMDate(studyDate)} {formatDICOMTime(studyTime)}
</div>
</div>
<div className="bottom-right overlay-element">
<div>Zoom: {zoomPercentage}%</div>
<div>{wwwc}</div>
<div className="compressionIndicator">{compression}</div>
</div>
<div className="bottom-left2 warning">
<div>{inconsistencyWarningsOn ? getWarningInfo(seriesNumber, inconsistencyWarnings) : ''}</div>
</div>
<div className="bottom-left overlay-element">
<div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div>
<div>
{stackSize > 1
? `Img: ${instanceNumber} ${imageIndex}/${stackSize}`
: ''}
</div>
<div>
{frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''}
<div>{imageDimensions}</div>
<div>
{isValidNumber(sliceLocation)
? `Loc: ${formatNumberPrecision(sliceLocation, 2)} mm `
: ''}
{sliceThickness
? `Thick: ${formatNumberPrecision(sliceThickness, 2)} mm`
: ''}
</div>
<div>{seriesDescription}</div>
</div>
</div>
</React.Fragment>
);
return <div className="OHIFCornerstoneViewportOverlay">{normal}</div>;
}
}
export default OHIFCornerstoneViewportOverlay;
+18 -2
View File
@@ -36,13 +36,29 @@ 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
* the viewport when new images are loaded. It's very ugly
* and we should remove it.
*/
const onNewImageHandler = jumpData => {
/** Do not trigger all viewports to render unnecessarily */
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}
/>
);
};
+9 -2
View File
@@ -37,7 +37,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 +59,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,109 @@
import moment from 'moment';
/**
* Checks if value is valid.
*
* @param {number} value
* @returns {boolean} is valid.
*/
function isValidNumber(value) {
return typeof value === 'number' && !isNaN(value);
}
/**
* Formats number precision.
*
* @param {number} number
* @param {number} precision
* @returns {number} formatted number.
*/
function formatNumberPrecision(number, precision) {
if (number !== null) {
return parseFloat(number).toFixed(precision);
}
}
/**
* Formats DICOM date.
*
* @param {string} date
* @param {string} strFormat
* @returns {string} formatted date.
*/
function formatDICOMDate(date, strFormat = 'MMM D, YYYY') {
return moment(date, 'YYYYMMDD').format(strFormat);
}
/**
* DICOM Time is stored as HHmmss.SSS, where:
* HH 24 hour time:
* m mm 0..59 Minutes
* s ss 0..59 Seconds
* S SS SSS 0..999 Fractional seconds
*
* Goal: '24:12:12'
*
* @param {*} time
* @param {string} strFormat
* @returns {string} formatted name.
*/
function formatDICOMTime(time, strFormat = 'HH:mm:ss') {
return moment(time, 'HH:mm:ss').format(strFormat);
}
/**
* Formats a patient name for display purposes
*
* @param {string} name
* @returns {string} formatted name.
*/
function formatPN(name) {
if (!name) {
return;
}
// Convert the first ^ to a ', '. String.replace() only affects
// the first appearance of the character.
const commaBetweenFirstAndLast = name.replace('^', ', ');
// Replace any remaining '^' characters with spaces
const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' ');
// Trim any extraneous whitespace
return cleaned.trim();
}
/**
* Gets compression type
*
* @param {number} imageId
* @returns {string} comrpession type.
*/
function getCompression(imageId) {
const generalImageModule =
cornerstone.metaData.get('generalImageModule', imageId) || {};
const {
lossyImageCompression,
lossyImageCompressionRatio,
lossyImageCompressionMethod,
} = generalImageModule;
if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') {
const compressionMethod = lossyImageCompressionMethod || 'Lossy: ';
const compressionRatio = formatNumberPrecision(
lossyImageCompressionRatio,
2
);
return compressionMethod + compressionRatio + ' : 1';
}
return 'Lossless / Uncompressed';
}
export { isValidNumber,
formatNumberPrecision,
formatDICOMDate,
formatDICOMTime,
formatPN,
getCompression
};
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-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
## [0.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.0...@ohif/extension-debugging@0.2.1) (2021-03-19)
**Note:** Version bump only for package @ohif/extension-debugging
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-debugging",
"version": "0.2.1",
"version": "0.3.0",
"description": "OHIF extension for debugging.",
"author": "OHIF",
"license": "MIT",
@@ -28,13 +28,12 @@
},
"peerDependencies": {
"@ohif/core": "^2.6.0",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.5.2"
"dicom-parser": "^1.8.9"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"detect-browser": "5.1.1",
"dicomweb-client": "^0.6.0",
"dicomweb-client": "^0.8.1",
"file-saver": "^2.0.2",
"jszip": "^3.2.2"
}
+51
View File
@@ -3,6 +3,57 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.8...@ohif/extension-dicom-html@1.3.9) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.7...@ohif/extension-dicom-html@1.3.8) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.6...@ohif/extension-dicom-html@1.3.7) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.5...@ohif/extension-dicom-html@1.3.6) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.4...@ohif/extension-dicom-html@1.3.5) (2021-04-15)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.3.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.3...@ohif/extension-dicom-html@1.3.4) (2021-03-09)
**Note:** Version bump only for package @ohif/extension-dicom-html
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-html",
"version": "1.3.4",
"version": "1.3.10",
"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.3",
"dcmjs": "0.18.8",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
@@ -168,6 +168,16 @@ const getContentSequence = (data, level = 1) => {
Object.keys(data).forEach(key => {
const value = data[key];
if (key === '_meta') {
const HeaderDynamicLevel = `h3`;
root.push(<hr />);
root.push(
<HeaderDynamicLevel key="Metadata">
DICOM File Meta Information
</HeaderDynamicLevel>
);
}
let content;
if (value instanceof Object) {
content = getContentSequence(value, level + 1);
@@ -215,6 +225,7 @@ class DicomHtmlViewport extends Component {
const dicomData = DicomMessage.readFile(arrayBuffer);
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
dataset._meta = DicomMetaDictionary.naturalizeDataset(dataset._meta);
const mainData = getMainData(dataset);
const contentSequence = getContentSequence(dataset);
@@ -9,6 +9,7 @@ const SOP_CLASS_UIDS = {
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',
};
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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.0",
"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.9",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
+70
View File
@@ -3,6 +3,76 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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
## [0.6.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.12...@ohif/extension-dicom-rt@0.6.13) (2021-06-02)
### Bug Fixes
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
## [0.6.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.11...@ohif/extension-dicom-rt@0.6.12) (2021-05-17)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.6.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.10...@ohif/extension-dicom-rt@0.6.11) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.6.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.9...@ohif/extension-dicom-rt@0.6.10) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.6.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.8...@ohif/extension-dicom-rt@0.6.9) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.6.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.7...@ohif/extension-dicom-rt@0.6.8) (2021-04-15)
**Note:** Version bump only for package @ohif/extension-dicom-rt
## [0.6.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.6...@ohif/extension-dicom-rt@0.6.7) (2021-03-09)
**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.7",
"version": "0.7.0",
"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.3",
"cornerstone-core": "^2.5.0",
"cornerstone-tools": "^6.0.0",
"dcmjs": "0.18.8",
"gl-matrix": "^3.3.0",
"prop-types": "^15.6.2",
"react": "^16.8.6",
@@ -71,8 +71,8 @@ const OHIFDicomRTStructSopClassHandler = {
}
}
rtStructDisplaySet.getSourceDisplaySet = function (studies) {
return getSourceDisplaySet(studies, rtStructDisplaySet);
rtStructDisplaySet.getSourceDisplaySet = function (studies, activateLabelMap = true) {
return getSourceDisplaySet(studies, rtStructDisplaySet, activateLabelMap);
};
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
+7 -31
View File
@@ -1,38 +1,14 @@
export default function getSourceDisplaySet(studies, rtStructDisplaySet) {
const referencedDisplaySet = _getReferencedDisplaySet(
import { metadata } from '@ohif/core';
export default function getSourceDisplaySet(studies, rtStructDisplaySet, activateLabelMap = true) {
const referencedDisplaySet = metadata.StudyMetadata.getReferencedDisplaySet(
rtStructDisplaySet,
studies
);
rtStructDisplaySet.load(referencedDisplaySet, studies);
if (activateLabelMap) {
rtStructDisplaySet.load(referencedDisplaySet, studies);
}
return referencedDisplaySet;
}
const _getReferencedDisplaySet = (rtStructDisplaySet, studies) => {
let allDisplaySets = [];
studies.forEach(study => {
allDisplaySets = allDisplaySets.concat(study.displaySets);
});
const otherDisplaySets = allDisplaySets.filter(
ds => ds.displaySetInstanceUID !== rtStructDisplaySet.displaySetInstanceUID
);
const ReferencedSeriesSequence = Array.isArray(
rtStructDisplaySet.metadata.ReferencedSeriesSequence
)
? rtStructDisplaySet.metadata.ReferencedSeriesSequence
: [rtStructDisplaySet.metadata.ReferencedSeriesSequence];
const referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
);
const referencedDisplaySet = otherDisplaySets.find(ds =>
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
);
return referencedDisplaySet;
};
+38 -1
View File
@@ -1,11 +1,12 @@
import React from 'react';
import { utils } from '@ohif/core';
import init from './init.js';
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
import id from './id.js';
import RTPanel from './components/RTPanel/RTPanel';
import { version } from '../package.json';
import { utils } from '@ohif/core';
const { studyMetadataManager } = utils;
export default {
@@ -42,12 +43,48 @@ export default {
);
};
const RTPanelTabChangedEvent = 'rt-panel-tab-updated';
/**
* Trigger's an event to update the state of the panel's RoundedButtonGroup.
*
* This is required to avoid extension state
* coupling with the viewer's ToolbarRow component.
*
* @param {object} data
*/
const triggerRTPanelUpdatedEvent = data => {
const event = new CustomEvent(RTPanelTabChangedEvent, {
detail: data,
});
document.dispatchEvent(event);
};
const onRTStructsLoaded = ({ detail }) => {
const { rtStructDisplaySet, referencedDisplaySet } = detail;
const studyMetadata = studyMetadataManager.get(
rtStructDisplaySet.StudyInstanceUID
);
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: referencedDisplaySet.SeriesInstanceUID,
Modality: 'RTSTRUCT',
});
triggerRTPanelUpdatedEvent({
badgeNumber: referencedDisplaysets.length,
target: 'rt-panel',
});
};
document.addEventListener('extensiondicomrtrtloaded', onRTStructsLoaded);
return {
menuOptions: [
{
icon: 'list',
label: 'RTSTRUCT',
target: 'rt-panel',
stateEvent: RTPanelTabChangedEvent,
isDisabled: (studies, activeViewport) => {
if (!studies) {
return true;
+15 -7
View File
@@ -90,11 +90,11 @@ export default async function loadRTStruct(
const sopInstanceUID = ContourImageSequence
? ContourImageSequence.ReferencedSOPInstanceUID
: _getClosestSOPInstanceUID(
ContourData,
ContourGeometricType,
NumberOfContourPoints,
imageIdSopInstanceUidPairs
);
ContourData,
ContourGeometricType,
NumberOfContourPoints,
imageIdSopInstanceUidPairs
);
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
if (!imageId) {
@@ -171,7 +171,13 @@ export default async function loadRTStruct(
* allows us to easily watch the module or the rtstruct loading process in any other component
* without subscribing to external events.
*/
const event = new CustomEvent('extensiondicomrtrtloaded');
const event = new CustomEvent('extensiondicomrtrtloaded', {
detail: {
rtStructDisplaySet,
referencedDisplaySet,
studies,
},
});
document.dispatchEvent(event);
}
@@ -299,7 +305,9 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
return imageIdSopInstanceUidPairsEntry
? imageIdSopInstanceUidPairsEntry.imageId
: null;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(
@@ -3,6 +3,100 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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
## [0.6.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.9...@ohif/extension-dicom-segmentation@0.6.10) (2021-06-02)
### Bug Fixes
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
## [0.6.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.8...@ohif/extension-dicom-segmentation@0.6.9) (2021-05-17)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.6.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.7...@ohif/extension-dicom-segmentation@0.6.8) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.6.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.6...@ohif/extension-dicom-segmentation@0.6.7) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.6.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.5...@ohif/extension-dicom-segmentation@0.6.6) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.6.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.4...@ohif/extension-dicom-segmentation@0.6.5) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.6.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.3...@ohif/extension-dicom-segmentation@0.6.4) (2021-04-16)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.6.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.2...@ohif/extension-dicom-segmentation@0.6.3) (2021-04-15)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.6.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.1...@ohif/extension-dicom-segmentation@0.6.2) (2021-03-31)
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
## [0.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.0...@ohif/extension-dicom-segmentation@0.6.1) (2021-03-09)
**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.1",
"version": "0.7.0",
"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.3",
"cornerstone-core": "^2.5.0",
"cornerstone-tools": "^6.0.0",
"dcmjs": "0.18.8",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
@@ -155,7 +155,9 @@ const SegmentationPanel = ({
const brushStackState = getBrushStackState();
brushStackState.activeLabelmapIndex = newLabelmapIndex;
setState(state => ({ ...state, selectedSegmentation }));
if (selectedSegmentation) {
setState(state => ({ ...state, selectedSegmentation }));
}
refreshViewports();
@@ -189,6 +191,10 @@ const SegmentationPanel = ({
'extensiondicomsegmentationsegloaded',
refreshSegmentations
);
document.addEventListener(
'extensiondicomsegmentationsegselected',
updateSegmentationComboBox
);
/*
* These are specific to each element;
@@ -207,6 +213,10 @@ const SegmentationPanel = ({
'extensiondicomsegmentationsegloaded',
refreshSegmentations
);
document.removeEventListener(
'extensiondicomsegmentationsegselected',
updateSegmentationComboBox
);
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
enabledElement.removeEventListener(
'cornerstonetoolslabelmapmodified',
@@ -214,7 +224,32 @@ const SegmentationPanel = ({
)
);
};
}, [activeIndex, viewports]);
}, [
activeIndex,
updateSegmentationComboBox,
viewports,
]);
const updateSegmentationComboBox = e => {
const index = e.detail.activatedLabelmapIndex;
if (index !== -1) {
setState(state => ({ ...state, selectedSegmentation: index }));
} else {
cleanSegmentationComboBox();
}
};
const cleanSegmentationComboBox = () => {
setState(state => ({
...state,
segmentsHidden: [],
segmentNumbers: [],
labelMapList: [],
segmentList: [],
isDisabled: true,
selectedSegmentation: -1,
}));
};
const refreshSegmentations = () => {
const activeViewport = getActiveViewport();
@@ -277,7 +312,11 @@ const SegmentationPanel = ({
activeViewport.SeriesInstanceUID
);
return referencedSegDisplaysets.map((displaySet, index) => {
const filteredReferencedSegDisplaysets = referencedSegDisplaysets.filter(
segDisplay => segDisplay.loadError !== true
);
return filteredReferencedSegDisplaysets.map((displaySet, index) => {
const {
labelmapIndex,
originLabelMapIndex,
@@ -289,7 +328,7 @@ const SegmentationPanel = ({
/* Map to display representation */
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
const displayDate = date.format('ddd, MMM Do YYYY');
const displayDate = date.format('ddd, MMM Do YYYY, h:mm:ss a');
const displayDescription = displaySet.SeriesDescription;
return {
@@ -709,7 +748,7 @@ const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
referencedDisplaysets.sort((a, b) => {
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
return aNumber - bNumber;
return bNumber - aNumber;
});
return referencedDisplaysets;
@@ -3,7 +3,7 @@ import Select from 'react-select';
const SegmentationSelect = ({ value, formatOptionLabel, options }) => (
<Select
value={value}
value={value === undefined ? null : value}
formatOptionLabel={formatOptionLabel}
styles={segmentationSelectStyles}
options={options}
@@ -55,6 +55,7 @@ export default function getSopClassHandlerModule({ servicesManager }) {
referencedDisplaySetUID: null, // Assigned when loaded.
labelmapIndex: null, // Assigned when loaded.
isLoaded: false,
loadError: false,
hasOverlapping: false,
SeriesDate,
SeriesTime,
@@ -63,8 +64,8 @@ export default function getSopClassHandlerModule({ servicesManager }) {
metadata,
};
segDisplaySet.getSourceDisplaySet = function(studies) {
return getSourceDisplaySet(studies, segDisplaySet);
segDisplaySet.getSourceDisplaySet = function(studies, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
return getSourceDisplaySet(studies, segDisplaySet, activateLabelMap, onDisplaySetLoadFailureHandler);
};
segDisplaySet.load = async function(referencedDisplaySet, studies) {
@@ -82,57 +83,50 @@ export default function getSopClassHandlerModule({ servicesManager }) {
StudyInstanceUID,
referencedDisplaySet.SeriesInstanceUID
);
return new Promise(async (resolve, reject) => {
let results;
try {
results = _parseSeg(segArrayBuffer, imageIds);
} catch (error) {
segDisplaySet.isLoaded = false;
segDisplaySet.loadError = true;
reject(error);
}
const {
labelmapBufferArray,
segMetadata,
segmentsOnFrame,
segmentsOnFrameArray,
} = results;
let labelmapIndex;
if (labelmapBufferArray.length > 1) {
let labelmapIndexes = [];
for (let i = 0; i < labelmapBufferArray.length; ++i) {
labelmapIndexes.push(
await loadSegmentation(
imageIds,
segDisplaySet,
labelmapBufferArray[i],
segMetadata,
segmentsOnFrame,
segmentsOnFrameArray[i]
)
);
}
/**
* Since overlapping segmentations have virtual labelmaps,
* originLabelMapIndex is used in the panel to select the correct dropdown value.
*/
segDisplaySet.hasOverlapping = true;
segDisplaySet.originLabelMapIndex = labelmapIndexes[0];
labelmapIndex = labelmapIndexes[0];
console.warn('Overlapping segmentations!');
} else {
labelmapIndex = await loadSegmentation(
imageIds,
segDisplaySet,
labelmapBufferArray[0],
segMetadata,
segmentsOnFrame,
[]
const results = await _parseSeg(segArrayBuffer, imageIds);
if (results === undefined) {
return;
}
const {
labelmapBufferArray,
segMetadata,
segmentsOnFrame,
segmentsOnFrameArray,
} = results;
let labelmapIndex;
if (labelmapBufferArray.length > 1) {
let labelmapIndexes = [];
for (let i = 0; i < labelmapBufferArray.length; ++i) {
labelmapIndexes.push(
await loadSegmentation(
imageIds,
segDisplaySet,
labelmapBufferArray[i],
segMetadata,
segmentsOnFrame,
segmentsOnFrameArray[i]
)
);
}
resolve(labelmapIndex);
});
/**
* Since overlapping segments have virtual labelmaps,
* originLabelMapIndex is used in the panel to select the correct dropdown value.
*/
segDisplaySet.hasOverlapping = true;
segDisplaySet.originLabelMapIndex = labelmapIndexes[0];
labelmapIndex = labelmapIndexes[0];
console.warn('Overlapping segments!');
} else {
labelmapIndex = await loadSegmentation(
imageIds,
segDisplaySet,
labelmapBufferArray[0],
segMetadata,
segmentsOnFrame,
[]
);
}
};
return segDisplaySet;
@@ -1,88 +1,16 @@
import setActiveLabelmap from './utils/setActiveLabelMap';
import { classes } from '@ohif/core';
import { metadata } from '@ohif/core';
const { ImageSet } = classes;
export default function getSourceDisplaySet(studies, segDisplaySet, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
const referencedDisplaySet = metadata.StudyMetadata.getReferencedDisplaySet(segDisplaySet, studies);
export default function getSourceDisplaySet(studies, segDisplaySet) {
const referencedDisplaySet = _getReferencedDisplaySet(segDisplaySet, studies);
setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet);
return referencedDisplaySet;
}
const _getReferencedDisplaySet = (segDisplaySet, studies) => {
let allDisplaySets = [];
studies.forEach(study => {
allDisplaySets = allDisplaySets.concat(study.displaySets);
});
const otherDisplaySets = allDisplaySets.filter(
ds => ds.displaySetInstanceUID !== segDisplaySet.displaySetInstanceUID
);
const { metadata } = segDisplaySet;
let referencedSeriesInstanceUIDs;
if (metadata.ReferencedSeriesSequence) {
const ReferencedSeriesSequence = _toArray(
metadata.ReferencedSeriesSequence
);
referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
);
} else {
const { PerFrameFunctionalGroupsSequence } = metadata;
let SourceImageSequence;
if (metadata.SourceImageSequence) {
SourceImageSequence = metadata.SourceImageSequence;
} else {
const firstFunctionalGroups = _toArray(
PerFrameFunctionalGroupsSequence
)[0];
const { DerivationImageSequence } = firstFunctionalGroups;
SourceImageSequence = DerivationImageSequence;
}
const firstSourceImage = _toArray(SourceImageSequence)[0];
const { ReferencedSOPInstanceUID } = firstSourceImage;
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
otherDisplaySets,
ReferencedSOPInstanceUID
);
let activatedLabelmapPromise;
if (activateLabelMap) {
activatedLabelmapPromise = setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet, undefined, onDisplaySetLoadFailureHandler);
}
const referencedDisplaySet = otherDisplaySets.find(ds =>
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
);
return referencedDisplaySet;
};
const _findReferencedSeriesInstanceUIDsFromSOPInstanceUID = (
displaySets,
SOPInstanceUID
) => {
const imageSets = displaySets.filter(ds => ds instanceof ImageSet);
for (let i = 0; i < imageSets.length; i++) {
const { images } = imageSets[i];
for (let j = 0; j < images.length; j++) {
if (images[j].SOPInstanceUID === SOPInstanceUID) {
return [images[j].getData().metadata.SeriesInstanceUID];
}
}
return {
referencedDisplaySet : referencedDisplaySet,
activatedLabelmapPromise : activatedLabelmapPromise
}
};
function _toArray(arrayOrObject) {
return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject];
}
@@ -1,9 +1,12 @@
import React from 'react';
import OHIF from '@ohif/core';
import init from './init.js';
import toolbarModule from './toolbarModule.js';
import getSopClassHandlerModule from './getOHIFDicomSegSopClassHandler.js';
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
import { version } from '../package.json';
const { studyMetadataManager } = OHIF.utils;
export default {
/**
@@ -79,12 +82,50 @@ export default {
);
};
const SegmentationPanelTabUpdatedEvent = 'segmentation-panel-tab-updated';
/**
* Trigger's an event to update the state of the panel's RoundedButtonGroup.
*
* This is required to avoid extension state
* coupling with the viewer's ToolbarRow component.
*
* @param {object} data
*/
const triggerSegmentationPanelTabUpdatedEvent = data => {
const event = new CustomEvent(SegmentationPanelTabUpdatedEvent, {
detail: data,
});
document.dispatchEvent(event);
};
const onSegmentationsLoaded = ({ detail }) => {
const { segDisplaySet, segMetadata } = detail;
const studyMetadata = studyMetadataManager.get(
segDisplaySet.StudyInstanceUID
);
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: segMetadata.seriesInstanceUid,
Modality: 'SEG',
});
triggerSegmentationPanelTabUpdatedEvent({
badgeNumber: referencedDisplaysets.length,
target: 'segmentation-panel',
});
};
document.addEventListener(
'extensiondicomsegmentationsegloaded',
onSegmentationsLoaded
);
return {
menuOptions: [
{
icon: 'list',
label: 'Segmentations',
target: 'segmentation-panel',
stateEvent: SegmentationPanelTabUpdatedEvent,
isDisabled: studies => {
if (!studies) {
return true;
@@ -59,7 +59,16 @@ export default async function loadSegmentation(
* without subscribing to external events.
*/
console.log('Segmentation loaded.');
const event = new CustomEvent('extensiondicomsegmentationsegloaded');
const event = new CustomEvent('extensiondicomsegmentationsegloaded', {
detail: {
imageIds,
segDisplaySet,
labelmapBuffer,
segMetadata,
segmentsOnFrame,
labelmapSegments,
},
});
document.dispatchEvent(event);
return labelmapIndex;
@@ -20,7 +20,7 @@ export default async function setActiveLabelmap(
displaySet,
callback = () => {},
onDisplaySetLoadFailure = err => {
throw new Error(err.message);
console.error(err.message);
}
) {
const studyMetadata = studyMetadataManager.get(
@@ -48,16 +48,15 @@ export default async function setActiveLabelmap(
}
if (!displaySet.isLoaded) {
const loadPromise = displaySet.load(referencedDisplaySet, studies);
loadPromise.catch(error => {
try {
await displaySet.load(referencedDisplaySet, studies);
} catch (error) {
displaySet.isLoaded = false;
displaySet.loadError = true;
onDisplaySetLoadFailure(error);
// Return old index.
return activeLabelmapIndex;
});
await loadPromise;
return -1;
}
}
labelmapIndex =
@@ -67,8 +66,11 @@ export default async function setActiveLabelmap(
// This might have just been created, so need to use the non-cached value.
state = cornerstoneTools.getModule('segmentation').state;
brushStackState = state.series[firstImageId];
brushStackState.activeLabelmapIndex = labelmapIndex;
if (brushStackState) {
brushStackState.activeLabelmapIndex = labelmapIndex;
}
refreshViewports();
callback();
+51
View File
@@ -3,6 +3,57 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [0.2.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.9...@ohif/extension-dicom-tag-browser@0.2.10) (2021-06-02)
### Bug Fixes
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
## [0.2.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.8...@ohif/extension-dicom-tag-browser@0.2.9) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.7...@ohif/extension-dicom-tag-browser@0.2.8) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.6...@ohif/extension-dicom-tag-browser@0.2.7) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.5...@ohif/extension-dicom-tag-browser@0.2.6) (2021-04-15)
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
## [0.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.4...@ohif/extension-dicom-tag-browser@0.2.5) (2021-03-09)
**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.5",
"version": "0.2.11",
"description": "OHIF extension for checking DICOM headers.",
"author": "OHIF",
"license": "MIT",
@@ -28,12 +28,12 @@
},
"peerDependencies": {
"@ohif/core": "^2.6.0",
"dcmjs": "0.18.3",
"dcmjs": "0.18.8",
"react": "^16.8.6"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"dicomweb-client": "^0.6.0",
"dicomweb-client": "^0.8.1",
"moment": "2.24.0",
"react-select": "^3.0.8"
}
+145
View File
@@ -3,6 +3,151 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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
## [1.11.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.14...@ohif/extension-vtk@1.11.15) (2021-08-30)
### Bug Fixes
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
## [1.11.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.13...@ohif/extension-vtk@1.11.14) (2021-06-03)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.12...@ohif/extension-vtk@1.11.13) (2021-06-02)
### Bug Fixes
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
## [1.11.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.11...@ohif/extension-vtk@1.11.12) (2021-05-17)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.10...@ohif/extension-vtk@1.11.11) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.9...@ohif/extension-vtk@1.11.10) (2021-04-22)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.8...@ohif/extension-vtk@1.11.9) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.7...@ohif/extension-vtk@1.11.8) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.6...@ohif/extension-vtk@1.11.7) (2021-04-21)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.5...@ohif/extension-vtk@1.11.6) (2021-04-16)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.4...@ohif/extension-vtk@1.11.5) (2021-04-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.3...@ohif/extension-vtk@1.11.4) (2021-03-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.2...@ohif/extension-vtk@1.11.3) (2021-03-25)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.11.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.1...@ohif/extension-vtk@1.11.2) (2021-03-09)
**Note:** Version bump only for package @ohif/extension-vtk
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.11.2",
"version": "1.12.0",
"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.3",
"dicom-parser": "^1.8.3",
"cornerstone-core": "^2.5.0",
"cornerstone-tools": "^6.0.0",
"cornerstone-wado-image-loader": "^4.0.3",
"dcmjs": "0.18.8",
"dicom-parser": "^1.8.9",
"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.1",
"@ohif/ui": "^1.9.0",
"cornerstone-tools": "^4.20.1",
"cornerstone-wado-image-loader": "^3.1.0",
"dicom-parser": "^1.8.3",
"@ohif/core": "^2.15.0",
"@ohif/ui": "^1.10.1",
"cornerstone-tools": "^6.0.0",
"cornerstone-wado-image-loader": "^4.0.3",
"dicom-parser": "^1.8.9",
"gh-pages": "^2.0.1",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
+3 -3
View File
@@ -72,7 +72,7 @@ class OHIFVTKViewport extends Component {
};
static defaultProps = {
onScroll: () => {},
onScroll: () => { },
};
static id = 'OHIFVTKViewport';
@@ -271,7 +271,7 @@ class OHIFVTKViewport extends Component {
volumeMapper.setSampleDistance(sampleDistance);
// Be generous to surpress warnings, as the logging really hurts performance.
// Be generous to suppress warnings, as the logging really hurts performance.
// TODO: maybe we should auto adjust samples to 1000.
volumeMapper.setMaximumSamplesPerRay(4000);
@@ -398,7 +398,7 @@ class OHIFVTKViewport extends Component {
if (
displaySet.displaySetInstanceUID !==
prevDisplaySet.displaySetInstanceUID ||
prevDisplaySet.displaySetInstanceUID ||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
displaySet.frameIndex !== prevDisplaySet.frameIndex
) {
@@ -1,18 +1,21 @@
import React, { useMemo } from 'react';
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { ToolbarButton } from '@ohif/ui';
import { utils } from '@ohif/core';
import { createSelector } from 'reselect';
const { studyMetadataManager } = utils;
const _isDisplaySetReconstructable = (
displaySetInstanceUID = '',
StudyInstanceUID = ''
) => {
if (displaySetInstanceUID == '' || StudyInstanceUID == '') {
let isVisible = true;
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
if (!viewportSpecificData[activeViewportIndex]) {
return false;
}
};
const { displaySetInstanceUID, StudyInstanceUID } = viewportSpecificData[
activeViewportIndex
];
const studies = studyMetadataManager.all();
@@ -24,132 +27,38 @@ const _isDisplaySetReconstructable = (
return false;
}
const displaySet = study._displaySets.find(
set => set.displaySetInstanceUID === displaySetInstanceUID
);
const displaySet = study._displaySets.find(set => set.displaySetInstanceUID === displaySetInstanceUID);
if (!displaySet) {
return false;
}
// 2D MPR is not currently available for 4D datasets.
// Assuming that slices at different time have the same position, here we just check if
// there are multiple slices for the same ImagePositionPatient and disable MPR.
// A better heuristic would be checking 4D tags, e.g. the presence of multiple TemporalPositionIdentifier values.
// However, some studies (e.g. https://github.com/OHIF/Viewers/issues/2113) do not have such tags.
for (let ii = 0; ii < displaySet.numImageFrames; ++ii) {
const image = displaySet.images[ii];
if (!image) continue;
const imageIdControl = image.getImageId();
const instanceMetadataControl = cornerstone.metaData.get(
'instance',
imageIdControl
);
if (
!instanceMetadataControl ||
instanceMetadataControl === undefined ||
!instanceMetadataControl.ImagePositionPatient ||
instanceMetadataControl.ImagePositionPatient === undefined
) {
// if ImagePositionPatient is missing, skip the 4D datasets check.
// do not return false, because it could be a 3D dataset.
continue;
}
let xImagePositionPatientControl =
instanceMetadataControl.ImagePositionPatient[0];
let yImagePositionPatientControl =
instanceMetadataControl.ImagePositionPatient[1];
let zImagePositionPatientControl =
instanceMetadataControl.ImagePositionPatient[2];
for (let jj = ii + 1; jj < displaySet.numImageFrames; ++jj) {
const image = displaySet.images[jj];
if (!image) continue;
const imageId = image.getImageId();
const instanceMetadata = cornerstone.metaData.get('instance', imageId);
if (
!instanceMetadata ||
instanceMetadata === undefined ||
!instanceMetadata.ImagePositionPatient ||
instanceMetadata.ImagePositionPatient === undefined
) {
// if ImagePositionPatient is missing, skip the 4D datasets check.
// do not return false, because it could be a 3D dataset.
continue;
}
let xImagePositionPatient = instanceMetadata.ImagePositionPatient[0];
let yImagePositionPatient = instanceMetadata.ImagePositionPatient[1];
let zImagePositionPatient = instanceMetadata.ImagePositionPatient[2];
// ImagePositionPatient is float
if (
Math.abs( xImagePositionPatientControl - xImagePositionPatient ) < 1e-6 &&
Math.abs( yImagePositionPatientControl - yImagePositionPatient ) < 1e-6 &&
Math.abs( zImagePositionPatientControl - zImagePositionPatient ) < 1e-6
) {
return false;
}
}
}
};
return displaySet.isReconstructable;
};
const selectDisplaySetInstanceUID = state => {
if (
state.viewports.viewportSpecificData[state.viewports.activeViewportIndex]
) {
return state.viewports.viewportSpecificData[
state.viewports.activeViewportIndex
].displaySetInstanceUID;
} else {
return '';
}
};
const selectStudyInstanceUID = state => {
if (
state.viewports.viewportSpecificData[state.viewports.activeViewportIndex]
) {
return state.viewports.viewportSpecificData[
state.viewports.activeViewportIndex
].StudyInstanceUID;
} else {
return '';
}
};
const stateSelector = createSelector(
[selectDisplaySetInstanceUID, selectStudyInstanceUID],
(displaySetInstanceUID, StudyInstanceUID) => {
return {
displaySetInstanceUID,
StudyInstanceUID,
};
}
);
function VTKMPRToolbarButton({ toolbarClickCallback, button, isActive }) {
function VTKMPRToolbarButton({
parentContext,
toolbarClickCallback,
button,
activeButtons,
isActive,
className,
}) {
const { id, label, icon } = button;
const { displaySetInstanceUID, StudyInstanceUID } = useSelector(
stateSelector
);
const { viewportSpecificData, activeViewportIndex } = useSelector(state => {
const { viewports = {} } = state;
const { viewportSpecificData, activeViewportIndex } = viewports;
const isVisible = useMemo(() => {
return _isDisplaySetReconstructable(
displaySetInstanceUID,
StudyInstanceUID
);
}, [displaySetInstanceUID, StudyInstanceUID]);
return {
viewportSpecificData,
activeViewportIndex,
}
});
isVisible = _isDisplaySetReconstructable(
viewportSpecificData,
activeViewportIndex,
);
return (
<React.Fragment>
+154
View File
@@ -3,6 +3,160 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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
## [2.13.14](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.13...@ohif/core@2.13.14) (2021-08-30)
### Bug Fixes
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
## [2.13.13](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.12...@ohif/core@2.13.13) (2021-06-03)
**Note:** Version bump only for package @ohif/core
## [2.13.12](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.11...@ohif/core@2.13.12) (2021-06-02)
### Bug Fixes
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
## [2.13.11](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.10...@ohif/core@2.13.11) (2021-05-17)
**Note:** Version bump only for package @ohif/core
## [2.13.10](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.9...@ohif/core@2.13.10) (2021-04-22)
**Note:** Version bump only for package @ohif/core
## [2.13.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.8...@ohif/core@2.13.9) (2021-04-22)
**Note:** Version bump only for package @ohif/core
## [2.13.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.7...@ohif/core@2.13.8) (2021-04-21)
**Note:** Version bump only for package @ohif/core
## [2.13.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.6...@ohif/core@2.13.7) (2021-04-21)
**Note:** Version bump only for package @ohif/core
## [2.13.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.5...@ohif/core@2.13.6) (2021-04-21)
**Note:** Version bump only for package @ohif/core
## [2.13.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.4...@ohif/core@2.13.5) (2021-04-16)
**Note:** Version bump only for package @ohif/core
## [2.13.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.3...@ohif/core@2.13.4) (2021-04-15)
### Bug Fixes
* **wl:** fix metadata provider window level issues after dicomweb update (string vs number) ([8681036](https://github.com/OHIF/Viewers/commit/8681036b2133cfc325658691a256a86b5e36705f))
## [2.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.2...@ohif/core@2.13.3) (2021-03-26)
**Note:** Version bump only for package @ohif/core
## [2.13.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.1...@ohif/core@2.13.2) (2021-03-25)
### Bug Fixes
* IDC-2288: Update toolbar tabs after derived displayset loaded ([#2307](https://github.com/OHIF/Viewers/issues/2307)) ([6e723eb](https://github.com/OHIF/Viewers/commit/6e723ebced58852007ca553792c740976cfc4201))
## [2.13.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.0...@ohif/core@2.13.1) (2021-03-09)
**Note:** Version bump only for package @ohif/core
+8 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.13.1",
"version": "2.15.0",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -31,21 +31,22 @@
"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.5.0",
"cornerstone-tools": "^6.0.0",
"cornerstone-wado-image-loader": "^4.0.3",
"dicom-parser": "^1.8.9"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"ajv": "^6.10.0",
"dcmjs": "0.18.3",
"dicomweb-client": "^0.6.0",
"dcmjs": "0.18.8",
"dicomweb-client": "^0.8.1",
"immer": "6.0.2",
"isomorphic-base64": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
"lodash.merge": "^4.6.1",
"mousetrap": "^1.6.3",
"retry": "^0.12.0",
"validate.js": "^0.12.0"
}
}
@@ -6,6 +6,7 @@ import parseDicomStructuredReport from './parseDicomStructuredReport';
import parseMeasurementsData from './parseMeasurementsData';
import getAllDisplaySets from './utils/getAllDisplaySets';
import errorHandler from '../errorHandler';
import getXHRRetryRequestHook from '../utils/xhrRetryRequestHook';
const VERSION_NAME = 'dcmjs-0.0';
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
@@ -23,6 +24,7 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
requestHooks: [getXHRRetryRequestHook()],
};
const dicomWeb = new api.DICOMwebClient(config);
@@ -74,6 +76,7 @@ const stowSRFromMeasurements = async (measurements, serverUrl) => {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
requestHooks: [getXHRRetryRequestHook()],
};
const dicomWeb = new api.DICOMwebClient(config);
@@ -1,7 +1,10 @@
import dcmjs from 'dcmjs';
import classes from '../classes';
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
@@ -20,9 +23,21 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
);
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;
@@ -77,7 +92,6 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
/**
* Function to create imagePath with all imageData related
*
* @param {string} StudyInstanceUID
* @param {string} SeriesInstanceUID
* @param {string} SOPInstanceUID
+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();
+17 -14
View File
@@ -4,6 +4,7 @@ import dicomParser from 'dicom-parser';
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData';
import validNumber from '../utils/metadataProvider/validNumber';
class MetadataProvider {
constructor() {
@@ -230,26 +231,26 @@ class MetadataProvider {
let columnCosines;
if (PixelSpacing) {
rowPixelSpacing = PixelSpacing[0];
columnPixelSpacing = PixelSpacing[1];
rowPixelSpacing = validNumber(PixelSpacing[0]);
columnPixelSpacing = validNumber(PixelSpacing[1]);
}
if (ImageOrientationPatient) {
rowCosines = ImageOrientationPatient.slice(0, 3);
columnCosines = ImageOrientationPatient.slice(3, 6);
rowCosines = validNumber(ImageOrientationPatient.slice(0, 3));
columnCosines = validNumber(ImageOrientationPatient.slice(3, 6));
}
metadata = {
frameOfReferenceUID: instance.FrameOfReferenceUID,
rows: instance.Rows,
columns: instance.Columns,
imageOrientationPatient: ImageOrientationPatient,
imageOrientationPatient: validNumber(ImageOrientationPatient),
rowCosines,
columnCosines,
imagePositionPatient: instance.ImagePositionPatient,
sliceThickness: instance.SliceThickness,
sliceLocation: instance.SliceLocation,
pixelSpacing: PixelSpacing,
imagePositionPatient: validNumber(instance.ImagePositionPatient),
sliceThickness: validNumber(instance.SliceThickness),
sliceLocation: validNumber(instance.SliceLocation),
pixelSpacing: validNumber(PixelSpacing),
rowPixelSpacing,
columnPixelSpacing,
};
@@ -284,7 +285,7 @@ class MetadataProvider {
break;
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
const { WindowCenter, WindowWidth } = instance;
let { WindowCenter, WindowWidth } = instance;
const windowCenter = Array.isArray(WindowCenter)
? WindowCenter
@@ -294,15 +295,17 @@ class MetadataProvider {
: [WindowWidth];
metadata = {
windowCenter,
windowWidth,
windowCenter: validNumber(windowCenter),
windowWidth: validNumber(windowWidth),
};
break;
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
const rescaleSlope = validNumber(instance.RescaleSlope);
const rescaleIntercept = validNumber(instance.RescaleIntercept);
metadata = {
rescaleIntercept: instance.RescaleIntercept,
rescaleSlope: instance.RescaleSlope,
rescaleIntercept,
rescaleSlope,
rescaleType: instance.RescaleType,
};
break;
+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._imageLoadProgressEventHandle.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;
+322 -55
View File
@@ -1,14 +1,28 @@
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,
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 +30,9 @@ export class StudyPrefetcher {
);
}
/**
* Remove previously set event listeners and stop prefetching.
*/
destroy() {
this.stopPrefetching();
cornerstone.events.removeEventListener(
@@ -24,95 +41,167 @@ 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;
this.enabledElement = cornerstone.getEnabledElement(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) {
this.enabledElement = cornerstone.getEnabledElement(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;
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 +210,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 +244,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 +301,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 +362,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 +451,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 +467,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 +497,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,11 +9,12 @@ import { SeriesMetadata } from './SeriesMetadata';
import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from '../../utils/isImage';
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
import { isDisplaySetReconstructable, isSpacingUniform } from '../../utils/isDisplaySetReconstructable';
import errorHandler from '../../errorHandler';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
import getXHRRetryRequestHook from '../../utils/xhrRetryRequestHook';
export class StudyMetadata extends Metadata {
class StudyMetadata extends Metadata {
constructor(data, uid) {
super(data, uid);
// Initialize Private Properties
@@ -226,6 +227,56 @@ export class StudyMetadata extends Metadata {
displaySets.map(displaySet => this._derivedDisplaySets.push(displaySet));
}
/**
* Returns the source display set of the derivated display set.
* @param {object} derivatedDisplaySet
* @param {array[StudyMetadata]} studies
* @return {object} source display set.
*/
static getReferencedDisplaySet(derivatedDisplaySet, studies) {
let allDisplaySets = [];
studies.forEach(study => {
allDisplaySets = allDisplaySets.concat(study.displaySets);
});
const otherDisplaySets = allDisplaySets.filter(
ds => ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID
);
const { metadata } = derivatedDisplaySet;
let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence
(metadata, otherDisplaySets);
let noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
referencedSeriesInstanceUIDs.length === 0;
if (noReferencedSeriesAvailable) {
referencedSeriesInstanceUIDs =
_findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence
(metadata);
}
noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
referencedSeriesInstanceUIDs.length === 0;
if (noReferencedSeriesAvailable) {
referencedSeriesInstanceUIDs =
_findReferencedSeriesInstanceUIDsFromReferencedImageSequence
(metadata, otherDisplaySets);
}
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.
* @param {object} filter An object containing search filters
@@ -252,20 +303,7 @@ export class StudyMetadata extends Metadata {
if (referencedSeriesInstanceUID) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet => {
if (!displaySet.metadata.ReferencedSeriesSequence) {
return false;
}
const ReferencedSeriesSequence = Array.isArray(
displaySet.metadata.ReferencedSeriesSequence
)
? displaySet.metadata.ReferencedSeriesSequence
: [displaySet.metadata.ReferencedSeriesSequence];
return ReferencedSeriesSequence.some(
ReferencedSeries =>
ReferencedSeries.SeriesInstanceUID === referencedSeriesInstanceUID
);
return StudyMetadata.getReferencedDisplaySet(displaySet, [this]).SeriesInstanceUID === referencedSeriesInstanceUID;
}
);
}
@@ -273,7 +311,7 @@ export class StudyMetadata extends Metadata {
if (referencedFrameOfReferenceUID) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet =>
displaySet.ReferencedFrameOfReferenceUID ===
displaySet.FrameOfReferenceUID ===
referencedFrameOfReferenceUID
);
}
@@ -459,6 +497,12 @@ export class StudyMetadata extends Metadata {
}
}
if (this._displaySets.some(ds =>
ds.displaySetInstanceUID === displaySet.displaySetInstanceUID)
) {
return;
}
this._displaySets.splice(insertIndex, 0, displaySet);
this.displaySets = this._displaySets;
}
@@ -767,6 +811,16 @@ const isMultiFrame = instance => {
return instance.getTagValue('NumberOfFrames') > 1;
};
/**
* Creates a display set for a series.
* Checks if a series is reconstructable to a 3D volume.
* If reconstructable, the frames are sorted.
*
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
*
* @returns {Object} imageSet.
*/
const makeDisplaySet = (series, instances) => {
const instance = instances[0];
const imageSet = new ImageSet(instances);
@@ -786,7 +840,7 @@ const makeDisplaySet = (series, instances) => {
isMultiFrame: isMultiFrame(instance),
});
// Sort the images in this series if needed
// Sort the images in this series by instanceNumber
const shallSort = true; //!OHIF.utils.ObjectPath.get(Meteor, 'settings.public.ui.sortSeriesByIncomingOrder');
if (shallSort) {
imageSet.sortBy((a, b) => {
@@ -804,18 +858,32 @@ const makeDisplaySet = (series, instances) => {
imageSet.getImage(0).getTagValue('InstanceNumber')
);
const isReconstructable = isDisplaySetReconstructable(instances);
imageSet.isReconstructable = isReconstructable.value;
const displayReconstructableInfo = isDisplaySetReconstructable(instances);
imageSet.isReconstructable = displayReconstructableInfo.value;
let displaySpacingInfo = undefined;
if (shallSort && imageSet.isReconstructable) {
// sort images by image position
imageSet.sortByImagePositionPatient();
// check if the spacing is uniform and update isReconstructable
const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find
(issue => issue === ReconstructionIssues.DATASET_4D);
displaySpacingInfo = isSpacingUniform(imageSet.images, datasetIs4D);
imageSet.isReconstructable = displaySpacingInfo.isUniform;
if (displaySpacingInfo.missingFrames) {
// TODO -> This is currently unused, but may be used for reconstructing
// Volumes with gaps later on.
imageSet.missingFrames = displaySpacingInfo.missingFrames;
}
}
if (isReconstructable.missingFrames) {
// TODO -> This is currently unused, but may be used for reconstructing
// Volumes with gaps later on.
imageSet.missingFrames = isReconstructable.missingFrames;
if (!imageSet.displayReconstructableInfo) {
// It is not reconstrabale Save type of warning
imageSet.reconstructionIssues = displaySpacingInfo ?
displayReconstructableInfo.reconstructionIssues.concat(displaySpacingInfo.reconstructionIssues) :
displayReconstructableInfo.reconstructionIssues;
}
return imageSet;
@@ -878,6 +946,7 @@ function _getDisplaySetFromSopClassModule(
url: study.getData().wadoRoot,
headers,
errorInterceptor,
requestHooks: [getXHRRetryRequestHook()],
});
let displaySet = plugin.getDisplaySetFromSeries(
@@ -892,3 +961,147 @@ function _getDisplaySetFromSopClassModule(
}
return displaySet;
}
/**
* 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
);
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 (
metadata,
displaySets
) {
if (!metadata.ReferencedImageSequence) {
return;
}
let referencedSeriesInstanceUIDs;
const referencedImageArray = _toArray(metadata.ReferencedImageSequence);
for (let i = 0; i < referencedImageArray.length; i++) {
const { ReferencedSOPInstanceUID } = referencedImageArray[i];
if (!ReferencedSOPInstanceUID) {
continue;
}
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
displaySets,
ReferencedSOPInstanceUID
);
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 (
metadata,
displaySets
) {
let SourceImageSequence;
if (metadata.SourceImageSequence) {
SourceImageSequence = metadata.SourceImageSequence;
} else {
const { PerFrameFunctionalGroupsSequence } = metadata;
const firstFunctionalGroups = _toArray(
PerFrameFunctionalGroupsSequence
)[0];
if (firstFunctionalGroups) {
const { DerivationImageSequence } = firstFunctionalGroups;
SourceImageSequence = DerivationImageSequence;
}
}
if (!SourceImageSequence) {
return;
}
const sourceImageArray = _toArray(SourceImageSequence);
let referencedSeriesInstanceUIDs;
for (let i = 0; i < sourceImageArray.length; i++) {
const { ReferencedSOPInstanceUID } = sourceImageArray[i];
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
displaySets,
ReferencedSOPInstanceUID
);
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 (
displaySets,
SOPInstanceUID
) {
const imageSets = displaySets.filter(ds => ds instanceof ImageSet);
for (let i = 0; i < imageSets.length; i++) {
const { images } = imageSets[i];
if (!images) {
continue;
}
for (let j = 0; j < images.length; j++) {
const image = images[j];
if (!image) {
continue;
}
if (image.SOPInstanceUID === SOPInstanceUID) {
return [image.getData().metadata.SeriesInstanceUID];
}
}
}
};
function _toArray(arrayOrObject) {
return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject];
}
export {StudyMetadata};
+11
View File
@@ -0,0 +1,11 @@
const ReconstructionIssues = {
DATASET_4D: 'datasetis4D',
VARYING_IMAGESDIMENSIONS: 'imagesdimensionsvarying',
VARYING_IMAGESCOMPONENTS: 'imagescomponentsvarying',
VARYING_IMAGESORIENTATION: 'imagesorientationvarying',
MISSING_FRAMES: 'missingframes',
IRREGULAR_SPACING: 'irregularspacing',
MULTIFFRAMES: 'multiframe',
};
export {ReconstructionIssues};
-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;
@@ -260,7 +260,6 @@ class MeasurementService {
const sourceInfo = this._getSourceInfo(source);
if (!definition) {
console.log('TEST');
log.warn('No source definition provided. Exiting early.');
return;
}
@@ -2,6 +2,7 @@ import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import errorHandler from '../../../errorHandler';
import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook';
/**
* Creates a QIDO date string for a date range query
@@ -118,6 +119,7 @@ export default function Studies(server, filter) {
url: server.qidoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
requestHooks: [getXHRRetryRequestHook()],
};
const dicomWeb = new api.DICOMwebClient(config);
@@ -10,6 +10,7 @@ import {
} from './studyInstanceHelpers';
import errorHandler from '../../../errorHandler';
import { getXHRRetryRequestHook } from '../../../utils/xhrRetryRequestHook';
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
@@ -76,6 +77,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
url: server.qidoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
requestHooks: [getXHRRetryRequestHook()],
});
this.client = client;
@@ -4,6 +4,7 @@ import { createStudyFromSOPInstanceList } from './studyInstanceHelpers';
import RetrieveMetadataLoader from './retrieveMetadataLoader';
import errorHandler from '../../../errorHandler';
import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook';
/**
* Class for sync load of study metadata.
@@ -61,6 +62,7 @@ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader {
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
requestHooks: [getXHRRetryRequestHook()],
});
this.client = client;
@@ -4,6 +4,7 @@ import { api } from 'dicomweb-client';
import DICOMWeb from '../DICOMWeb';
import errorHandler from '../errorHandler';
import getXHRRetryRequestHook from './xhrRetryRequestHook';
const getImageId = imageObj => {
if (!imageObj) {
@@ -66,6 +67,7 @@ const wadorsRetriever = (
url,
headers,
errorInterceptor,
requestHooks: [getXHRRetryRequestHook()],
};
const dicomWeb = new api.DICOMwebClient(config);
+3
View File
@@ -18,6 +18,7 @@ import isDicomUid from './isDicomUid';
import resolveObjectPath from './resolveObjectPath';
import * as hierarchicalListUtils from './hierarchicalListUtils';
import * as progressTrackingUtils from './progressTrackingUtils';
import xhrRetryRequestHook from './xhrRetryRequestHook';
const utils = {
guid,
@@ -40,6 +41,7 @@ const utils = {
resolveObjectPath,
hierarchicalListUtils,
progressTrackingUtils,
xhrRetryRequestHook,
};
export {
@@ -63,6 +65,7 @@ export {
resolveObjectPath,
hierarchicalListUtils,
progressTrackingUtils,
xhrRetryRequestHook,
};
export default utils;
+1
View File
@@ -23,6 +23,7 @@ describe('Top level exports', () => {
'resolveObjectPath',
'hierarchicalListUtils',
'progressTrackingUtils',
'xhrRetryRequestHook',
].sort();
const exports = Object.keys(utils.default).sort();
@@ -1,9 +1,14 @@
import { ReconstructionIssues } from './../enums.js';
/**
* Checks if a series is reconstructable to a 3D volume.
*
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
*
* @returns {Object} value, reconstructionIssues.
*/
export default function isDisplaySetReconstructable(instances) {
function isDisplaySetReconstructable(instances) {
if (!instances.length) {
return { value: false };
}
@@ -23,31 +28,45 @@ export default function isDisplaySetReconstructable(instances) {
}
if (isMultiframe) {
return processMultiframe(instances[0]);
return processMultiframe();
} else {
return processSingleframe(instances);
}
}
function processMultiframe(instance) {
//TODO: deal with multriframe checks! return false for now as can't reconstruct.
return { value: false };
/**
* Process reconstructable multiframes checks
* TODO: deal with multriframe checks! return false for now as can't reconstruct.
* *
* @returns {Object} value and reconstructionIssues.
*/
function processMultiframe() {
const reconstructionIssues = [ReconstructionIssues.MULTIFRAMES];
return { value: false, reconstructionIssues };
}
/**
* Process reconstructable single frame checks
*
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
*
* @returns {Object} value and reconstructionIssues.
*/
function processSingleframe(instances) {
const n = instances.length;
const firstImage = instances[0].getData().metadata;
const firstImageRows = firstImage.Rows;
const firstImageColumns = firstImage.Columns;
const firstImageSamplesPerPixel = firstImage.SamplesPerPixel;
const firstImageOrientationPatient = firstImage.ImageOrientationPatient;
const firstImagePositionPatient = firstImage.ImagePositionPatient;
const reconstructionIssues = [];
// Can't reconstruct if we:
// -- Have a different dimensions within a displaySet.
// -- Have a different number of components within a displaySet.
// -- Have different orientations within a displaySet.
for (let i = 1; i < instances.length; i++) {
const instance = instances[i].getData().metadata;
for (let ii = 1; ii < n; ++ii) {
const instance = instances[ii].getData().metadata;
const {
Rows,
Columns,
@@ -55,67 +74,146 @@ function processSingleframe(instances) {
ImageOrientationPatient,
} = instance;
if (
Rows !== firstImageRows ||
Columns !== firstImageColumns ||
SamplesPerPixel !== firstImageSamplesPerPixel ||
!_isSameOrientation(ImageOrientationPatient, firstImageOrientationPatient)
) {
return { value: false };
if (Rows !== firstImageRows || Columns !== firstImageColumns) {
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS);
} else if (SamplesPerPixel !== firstImageSamplesPerPixel) {
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS);
} else if (!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)) {
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION);
}
if (reconstructionIssues.length !== 0) {
break;
}
}
// check if dataset is 4D
if (_isDataset4D(instances)) {
reconstructionIssues.push(ReconstructionIssues.DATASET_4D);
}
return { value: reconstructionIssues.length === 0 ? true : false, reconstructionIssues };
}
/**
* Check is the spacing is uniform.
* The input metadata array has to be ordered by image position.
*
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
* @param {boolean} is the dataset 4D.
*
* @returns {Object} isUniform, reconstructionIssues and missingFrames
*/
function isSpacingUniform(instances, datasetIs4D) {
const n = instances.length;
const firstImage = instances[0].getData().metadata;
const firstImagePositionPatient = firstImage.ImagePositionPatient;
const reconstructionIssues = [];
let missingFrames = 0;
// Check if frame spacing is approximately equal within a spacingTolerance.
// If spacing is on a uniform grid but we are missing frames,
// Allow reconstruction, but pass back the number of missing frames.
if (instances.length > 2) {
const lastIpp = instances[instances.length - 1].getData().metadata
if (n > 2) {
const lastIpp = instances[n - 1].getData().metadata
.ImagePositionPatient;
// We can't reconstruct if we are missing ImagePositionPatient values
if (!firstImagePositionPatient || !lastIpp) {
return { value: false };
}
if (firstImagePositionPatient && lastIpp) {
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
(n - 1);
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
(instances.length - 1);
let previousImagePositionPatient = firstImagePositionPatient;
let previousImagePositionPatient = firstImagePositionPatient;
for (let ii = 1; ii < n; ++ii) {
const instance = instances[ii].getData().metadata;
const { ImagePositionPatient } = instance;
for (let i = 1; i < instances.length; i++) {
const instance = instances[i].getData().metadata;
const { ImagePositionPatient } = instance;
const spacingBetweenFrames = _getPerpendicularDistance(
ImagePositionPatient,
previousImagePositionPatient
);
const spacingBetweenFrames = _getPerpendicularDistance(
ImagePositionPatient,
previousImagePositionPatient
);
const spacingIssue = _getSpacingIssue(
spacingBetweenFrames,
averageSpacingBetweenFrames
);
if (spacingIssue) {
const issue = spacingIssue.issue;
if (issue === reconstructionIssues.MISSING_FRAMES) {
missingFrames += spacingIssue.missingFrames;
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
return { value: false };
if (datasetIs4D && spacingBetweenFrames < 1.e-3) {
// the dataset is 4D, if the distance is zero, means that we are
// checking the 4th dimension. Do not return, since we want still to
// check the 3rd dimension spacing.
continue;
}
}
previousImagePositionPatient = ImagePositionPatient;
const spacingIssue = _getSpacingIssue(
spacingBetweenFrames,
averageSpacingBetweenFrames
);
if (spacingIssue) {
const issue = spacingIssue.issue;
if (issue === ReconstructionIssues.MISSING_FRAMES) {
missingFrames += spacingIssue.missingFrames;
} else if (issue === ReconstructionIssues.IRREGULAR_SPACING) {
reconstructionIssues.push(issue);
break;
}
}
previousImagePositionPatient = ImagePositionPatient;
}
}
}
return { value: true, missingFrames };
return { isUniform: reconstructionIssues.length === 0 ? true : false, missingFrames, reconstructionIssues };
}
function _isSameOrientation(iop1, iop2) {
/**
* Check if 4D dataset.
*
* Assuming that slices at different time have the same position, here we just check if
* there are multiple slices for the same ImagePositionPatient and disable MPR.
*
* A better heuristic would be checking 4D tags, e.g. the presence of multiple TemporalPositionIdentifier values.
* However, some studies (e.g. https://github.com/OHIF/Viewers/issues/2113) do not have such tags.
*
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
*
* @returns {boolean} dataset4D value.
*/
function _isDataset4D(instances) {
const n = instances.length;
for (let ii = 0; ii < n; ++ii) {
const instanceMetadataControl = instances[ii].getData().metadata;
if (
!instanceMetadataControl ||
instanceMetadataControl === undefined ||
!instanceMetadataControl.ImagePositionPatient ||
instanceMetadataControl.ImagePositionPatient === undefined
) {
continue;
}
for (let jj = ii + 1; jj < n; ++jj) {
const instanceMetadata = instances[jj].getData().metadata;
if (
!instanceMetadata ||
instanceMetadata === undefined ||
!instanceMetadata.ImagePositionPatient ||
instanceMetadata.ImagePositionPatient === undefined
) {
continue;
}
if (_isSameArray(instanceMetadataControl.ImagePositionPatient, instanceMetadata.ImagePositionPatient)) {
return true;
}
}
}
return false;
}
function _isSameArray(iop1, iop2) {
if (iop1 === undefined || !iop2 === undefined) {
return;
}
@@ -156,12 +254,12 @@ function _getSpacingIssue(spacing, averageSpacing) {
if (errorForEachSpacing < spacingTolerance * averageSpacing) {
return {
issue: reconstructionIssues.MISSING_FRAMES,
issue: ReconstructionIssues.MISSING_FRAMES,
missingFrames: numberOfSpacings - 1,
};
}
return { issue: reconstructionIssues.IRREGULAR_SPACING };
return { issue: ReconstructionIssues.IRREGULAR_SPACING };
}
function _getPerpendicularDistance(a, b) {
@@ -173,7 +271,5 @@ function _getPerpendicularDistance(a, b) {
}
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
const reconstructionIssues = {
MISSING_FRAMES: 'missingframes',
IRREGULAR_SPACING: 'irregularspacing',
};
export {isDisplaySetReconstructable, isSpacingUniform};
@@ -45,13 +45,13 @@ import studyMetadataManager from './studyMetadataManager';
* @param {string} referencedDisplaySet.studyInstanceUid
* @param {Array} referencedDisplaySet.sopClassUids
* @param {Study[]} studies Collection of studies
* @param {object} logger
* @param {object} snackbar
* @returns void
*/
const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
async function loadAndCacheDerivedDisplaySets(referencedDisplaySet, studies, logger, snackbar) {
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
const promises = [];
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
if (!studyMetadata) {
@@ -80,11 +80,10 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
});
// For each type, see if any are loaded, if not load the most recent.
Object.keys(displaySetsPerModality).forEach(key => {
await Promise.all(Object.keys(displaySetsPerModality).map(async (key) => {
const displaySets = displaySetsPerModality[key];
const isLoaded = displaySets.some(displaySet => displaySet.isLoaded);
if (isLoaded) {
return;
}
@@ -107,12 +106,34 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
}
});
recentDisplaySet.isLoading = true;
try {
if (recentDisplaySet.hasOwnProperty('getSourceDisplaySet') &&
typeof recentDisplaySet.getSourceDisplaySet === 'function') {
await recentDisplaySet.getSourceDisplaySet(studies);
} else {
await recentDisplaySet.load(referencedDisplaySet, studies);
}
} catch (error) {
recentDisplaySet.isLoaded = false;
recentDisplaySet.loadError = true;
logger.error({ error, message: error.message });
snackbar.show({
title: 'Error loading derived display set:',
message: error.message,
type: 'error',
error,
autoClose: false,
});
}
}));
promises.push(recentDisplaySet.load(referencedDisplaySet, studies));
});
return promises;
/*
* TODO: Improve the way we notify parts of the app
* that depends on derived display sets to be loaded.
* (Implement pubsub for better tracking of derived display sets)
*/
const event = new CustomEvent('deriveddisplaysetsloadedandcached');
document.dispatchEvent(event);
};
export default loadAndCacheDerivedDisplaySets;
@@ -4,6 +4,7 @@ import str2ab from '../str2ab';
import unpackOverlay from './unpackOverlay';
import errorHandler from '../../errorHandler';
import getXHRRetryRequestHook from '../xhrRetryRequestHook';
export default async function fetchOverlayData(instance, server) {
const OverlayDataPromises = [];
@@ -69,6 +70,7 @@ async function _getOverlayData(tag, server) {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
requestHooks: [getXHRRetryRequestHook()],
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
@@ -3,6 +3,7 @@ import DICOMWeb from '../../DICOMWeb';
import str2ab from '../str2ab';
import errorHandler from '../../errorHandler';
import getXHRRetryRequestHook from '../xhrRetryRequestHook';
export default async function fetchPaletteColorLookupTableData(
instance,
@@ -141,6 +142,7 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
requestHooks: [getXHRRetryRequestHook()],
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
@@ -0,0 +1,9 @@
const validNumber = val => {
if (Array.isArray(val)) {
return val.map(v => (v !== undefined ? Number(v) : v));
} else {
return val !== undefined ? Number(val) : val;
}
};
export default validNumber;
@@ -0,0 +1,103 @@
import retry from 'retry';
const defaultRetryOptions = {
retries: 5,
factor: 3,
minTimeout: 1 * 1000,
maxTimeout: 60 * 1000,
randomize: true,
retryableStatusCodes: [429, 500],
};
let retryOptions = { ...defaultRetryOptions };
/**
* Request hook used to add retry functionality to XHR requests.
*
* @param {XMLHttpRequest} request XHR request instance
* @param {object} metadata Metadata about the request
* @param {object} metadata.url URL
* @param {object} metadata.method HTTP method
* @returns {XMLHttpRequest} request instance optionally modified
*/
const xhrRetryRequestHook = (request, metadata) => {
const { url, method } = metadata;
function faultTolerantRequestSend(...args) {
const operation = retry.operation(retryOptions);
operation.attempt(function operationAttempt(currentAttempt) {
const originalOnReadyStateChange = request.onreadystatechange;
/** Overriding/extending XHR function */
request.onreadystatechange = function onReadyStateChange(...args) {
originalOnReadyStateChange.apply(request, args);
if (retryOptions.retryableStatusCodes.includes(request.status)) {
const errorMessage = `Attempt to request ${url} failed.`;
const attemptFailedError = new Error(errorMessage);
operation.retry(attemptFailedError);
}
};
/** Call open only on retry (after headers and other things were set in the xhr instance) */
if (currentAttempt > 1) {
console.warn(`Requesting ${url}... (attempt: ${currentAttempt})`);
request.open(method, url, true);
}
});
originalRequestSend.apply(request, args);
}
/** Overriding/extending XHR function */
const originalRequestSend = request.send;
request.send = faultTolerantRequestSend;
return request;
};
/**
* Returns a configured retry request hook function
* that can be used to add retry functionality to XHR request.
*
* Default options:
* retries: 5
* factor: 3
* minTimeout: 1 * 1000
* maxTimeout: 60 * 1000
* randomize: true
*
* @param {object} options
* @param {number} options.retires number of retries
* @param {number} options.factor factor
* @param {number} options.minTimeout the min timeout
* @param {number} options.maxTimeout the max timeout
* @param {boolean} options.randomize randomize
* @param {array} options.retryableStatusCodes status codes that can trigger retry
* @returns {function} the configured retry request function
*/
export const getXHRRetryRequestHook = (options = {}) => {
retryOptions = { ...defaultRetryOptions };
if ('retries' in options) {
retryOptions.retries = options.retries;
}
if ('factor' in options) {
retryOptions.factor = options.factor;
}
if ('minTimeout' in options) {
retryOptions.minTimeout = options.minTimeout;
}
if ('maxTimeout' in options) {
retryOptions.maxTimeout = options.maxTimeout;
}
if ('randomize' in options) {
retryOptions.randomize = options.randomize;
}
if ('retryableStatusCodes' in options) {
retryOptions.retryableStatusCodes = options.retryableStatusCodes;
}
return xhrRetryRequestHook;
};
export default getXHRRetryRequestHook;
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.0...@ohif/ui@1.10.1) (2021-09-13)
**Note:** Version bump only for package @ohif/ui
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.3...@ohif/ui@1.10.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))
## [1.9.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.2...@ohif/ui@1.9.3) (2021-09-03)
**Note:** Version bump only for package @ohif/ui
## [1.9.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.1...@ohif/ui@1.9.2) (2021-05-17)
**Note:** Version bump only for package @ohif/ui
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.0...@ohif/ui@1.9.1) (2021-03-26)
**Note:** Version bump only for package @ohif/ui
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.8.2...@ohif/ui@1.9.0) (2021-03-04)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "1.9.0",
"version": "1.10.1",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -4,6 +4,29 @@
z-index: 0;
}
.RoundedButtonGroup .badgeNumber-container {
background-color: var(--ui-border-color-dark);
width: 21px;
height: 21px;
justify-content: center;
align-items: center;
display: flex;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
bottom: 10px;
left: 5px;
color: white;
position: absolute;
}
.RoundedButtonGroup .badgeNumber {
font-size: 10px;
font-weight: bold;
display: block;
width: 25px;
}
.roundedButtonWrapper {
cursor: pointer;
display: inline-block;
@@ -36,7 +59,12 @@
z-index: 1;
}
.roundedButtonWrapper .roundedButton svg .roundedButtonWrapper .roundedButton span {
.roundedButtonWrapper
.roundedButton
svg
.roundedButtonWrapper
.roundedButton
span {
margin: 0 2px;
}
@@ -13,6 +13,7 @@ class RoundedButtonGroup extends Component {
PropTypes.shape({
value: PropTypes.any,
label: PropTypes.string,
stateEvent: PropTypes.string,
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
@@ -30,6 +31,15 @@ class RoundedButtonGroup extends Component {
value: null,
};
state = {
badgeNumbers: [],
};
constructor() {
super();
this.onStateEvent = this.onStateEvent.bind(this);
}
onClickOption = value => {
let newValue = value;
if (this.props.value === value) {
@@ -41,6 +51,48 @@ class RoundedButtonGroup extends Component {
}
};
onStateEvent(event) {
const optionIndex = this.props.options.findIndex(
o => o.value === event.detail.target
);
if (optionIndex > -1) {
const badgeNumbers = this.state.badgeNumbers;
badgeNumbers[optionIndex] = event.detail.badgeNumber;
this.setState({ badgeNumbers });
}
}
componentDidMount() {
this.props.options.forEach(option => {
if (option.stateEvent) {
document.addEventListener(option.stateEvent, this.onStateEvent);
}
});
}
componentDidUpdate(prevProps, prevState) {
this.props.options.forEach((option, index) => {
if (
option.stateEvent &&
option.stateEvent !==
(prevProps.options[index]
? prevProps.options[index].stateEvent
: null)
) {
document.removeEventListener(option.stateEvent, this.onStateEvent);
document.addEventListener(option.stateEvent, this.onStateEvent);
}
});
}
componentWillUnmount() {
this.props.options.forEach(option => {
if (option.stateEvent) {
document.removeEventListener(option.stateEvent, this.onStateEvent);
}
});
}
render() {
let className = classnames(
RoundedButtonGroup.className,
@@ -62,6 +114,14 @@ class RoundedButtonGroup extends Component {
<div className="bottomLabel">{option.bottomLabel}</div>
);
let badgeNumber = this.state.badgeNumbers[index];
const badgeNumberOverflow = String(badgeNumber).length > 2;
badgeNumber = badgeNumber
? badgeNumberOverflow
? 99
: badgeNumber
: null;
return (
<div
key={index}
@@ -70,6 +130,14 @@ class RoundedButtonGroup extends Component {
>
<div className="roundedButton">
{optionText}
{badgeNumber && (
<div className="badgeNumber-container">
<span className="badgeNumber">
{badgeNumber}
{badgeNumberOverflow && '+'}
</span>
</div>
)}
{iconProps && <Icon {...iconProps} />}
</div>
{bottomLabel}
@@ -2,7 +2,8 @@
import './ImageThumbnail.styl';
import { utils } from '@ohif/core';
import React, { useState, useEffect, createRef } from 'react';
import React, { useState, useEffect, createRef, useCallback } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator';
@@ -15,12 +16,14 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
//import cornerstone from 'cornerstone-core';
function ImageThumbnail(props) {
const {
active,
width,
height,
imageSrc,
imageId,
stackPercentComplete,
error: propsError,
showProgressBar,
} = props;
const [isLoading, setLoading] = useState(false);
@@ -37,7 +40,8 @@ function ImageThumbnail(props) {
loadingOrError = <ViewportLoadingIndicator />;
}
const showStackLoadingProgressBar = stackPercentComplete !== undefined;
const showStackLoadingProgressBar =
showProgressBar && stackPercentComplete !== undefined;
const shouldRenderToCanvas = () => {
return imageId && !imageSrc;
@@ -69,11 +73,11 @@ function ImageThumbnail(props) {
}
};
const purgeCancelablePromise = () => {
const purgeCancelablePromise = useCallback(() => {
if (cancelablePromise) {
cancelablePromise.cancel();
}
};
});
useEffect(() => {
return () => {
@@ -94,22 +98,28 @@ function ImageThumbnail(props) {
setImagePromise();
fetchImagePromise();
}
}, [fetchImagePromise, image.imageId, imageId, purgeCancelablePromise, setImagePromise]);
}, [
fetchImagePromise,
image.imageId,
imageId,
purgeCancelablePromise,
setImagePromise,
]);
return (
<div className="ImageThumbnail">
<div className={classNames('ImageThumbnail', { active: active })}>
<div className="image-thumbnail-canvas">
{shouldRenderToCanvas() ? (
<canvas ref={canvasRef} width={width} height={height} />
) : (
<img
className="static-image"
src={imageSrc}
//width={this.props.width}
height={height}
alt={''}
/>
)}
<img
className="static-image"
src={imageSrc}
//width={this.props.width}
height={height}
alt={''}
/>
)}
</div>
{loadingOrError}
{showStackLoadingProgressBar && (
@@ -126,19 +136,23 @@ function ImageThumbnail(props) {
}
ImageThumbnail.propTypes = {
active: PropTypes.bool,
imageSrc: PropTypes.string,
imageId: PropTypes.string,
error: PropTypes.bool,
width: PropTypes.number,
height: PropTypes.number,
stackPercentComplete: PropTypes.number.isRequired,
showProgressBar: PropTypes.bool,
};
ImageThumbnail.defaultProps = {
active: false,
error: false,
stackPercentComplete: 0,
width: 217,
height: 123,
showProgressBar: true,
};
export default ImageThumbnail;
@@ -6,7 +6,7 @@
--sidebar-transition: all 0.3s ease;
}
.ThumbnailEntry.active .ImageThumbnail
.ImageThumbnail.active
border-color: var(--active-color);
box-shadow: none
transition: var(--sidebar-transition);
@@ -14,7 +14,7 @@
.ImageThumbnail
background-color: var(--primary-background-color);
box-shadow: inset 0 0 0 1px var(--ui-border-color-dark);
border: 5px solid transparent
border: 2px solid transparent
border-radius: 12px
height: 135px
margin: 0 auto
@@ -9,6 +9,7 @@ function StudyBrowser(props) {
onThumbnailClick,
onThumbnailDoubleClick,
supportsDrag,
showThumbnailProgressBar,
} = props;
return (
@@ -20,6 +21,7 @@ function StudyBrowser(props) {
return study.thumbnails.map((thumb, thumbIndex) => {
// TODO: Thumb has more props than we care about?
const {
active,
altImageText,
displaySetInstanceUID,
imageId,
@@ -27,7 +29,7 @@ function StudyBrowser(props) {
numImageFrames,
SeriesDescription,
SeriesNumber,
stackPercentComplete,
hasWarnings,
} = thumb;
return (
@@ -37,6 +39,7 @@ function StudyBrowser(props) {
data-cy="thumbnail-list"
>
<Thumbnail
active={active}
supportsDrag={supportsDrag}
key={`${studyIndex}_${thumbIndex}`}
id={`${studyIndex}_${thumbIndex}`} // Unused?
@@ -50,13 +53,14 @@ function StudyBrowser(props) {
numImageFrames={numImageFrames}
SeriesDescription={SeriesDescription}
SeriesNumber={SeriesNumber}
stackPercentComplete={stackPercentComplete}
hasWarnings={hasWarnings}
// Events
onClick={onThumbnailClick.bind(
undefined,
displaySetInstanceUID
)}
onDoubleClick={onThumbnailDoubleClick}
showProgressBar={showThumbnailProgressBar}
/>
</div>
);
@@ -91,6 +95,7 @@ StudyBrowser.propTypes = {
supportsDrag: PropTypes.bool,
onThumbnailClick: PropTypes.func,
onThumbnailDoubleClick: PropTypes.func,
showThumbnailProgressBar: PropTypes.bool,
};
StudyBrowser.defaultProps = {
@@ -98,6 +103,7 @@ StudyBrowser.defaultProps = {
supportsDrag: true,
onThumbnailClick: noop,
onThumbnailDoubleClick: noop,
showThumbnailProgressBar: true,
};
export { StudyBrowser };
@@ -1,17 +1,37 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useDrag } from 'react-dnd';
import { classes } from '@ohif/core';
import ImageThumbnail from './ImageThumbnail';
import classNames from 'classnames';
import { Icon } from './../../elements/Icon';
import { Tooltip } from './../tooltip';
import { OverlayTrigger } from './../overlayTrigger';
import './Thumbnail.styl';
const StudyLoadingListener = classes.StudyLoadingListener;
function ThumbnailFooter({
SeriesDescription,
SeriesNumber,
InstanceNumber,
numImageFrames,
hasWarnings,
}) {
const [inconsistencyWarnings, inconsistencyWarningsSet] = useState([]);
useEffect(() => {
let unmounted = false;
hasWarnings.then(response => {
if (!unmounted) {
inconsistencyWarningsSet(response);
}
});
return () => {
unmounted = true;
};
}, [hasWarnings]);
const infoOnly = !SeriesDescription;
const getInfo = (value, icon, className = '') => {
@@ -22,28 +42,81 @@ function ThumbnailFooter({
</div>
);
};
const getWarningContent = inconsistencyWarnings => {
if (Array.isArray(inconsistencyWarnings)) {
const listedWarnings = inconsistencyWarnings.map((warn, index) => {
return <li key={index}>{warn}</li>;
});
return <ol>{listedWarnings}</ol>;
} else {
return <React.Fragment>{inconsistencyWarnings}</React.Fragment>;
}
};
const getWarningInfo = (SeriesNumber, inconsistencyWarnings) => {
return (
<React.Fragment>
{inconsistencyWarnings && inconsistencyWarnings.length != 0 ? (
<OverlayTrigger
key={SeriesNumber}
placement="left"
overlay={
<Tooltip
placement="left"
className="in tooltip-warning"
id="tooltip-left"
>
<div className="warningTitle">Series Inconsistencies</div>
<div className="warningContent">
{getWarningContent(inconsistencyWarnings)}
</div>
</Tooltip>
}
>
<div className={classNames('warning')}>
<span className="warning-icon">
<Icon name="exclamation-triangle" />
</span>
</div>
</OverlayTrigger>
) : (
<React.Fragment></React.Fragment>
)}
</React.Fragment>
);
};
const getSeriesInformation = (
SeriesNumber,
InstanceNumber,
numImageFrames
numImageFrames,
inconsistencyWarnings
) => {
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
return;
}
return (
const seriesInformation = (
<div className="series-information">
{getInfo(SeriesNumber, 'S:')}
{getInfo(InstanceNumber, 'I:')}
{getInfo(numImageFrames, '', 'image-frames')}
{getWarningInfo(SeriesNumber, inconsistencyWarnings)}
</div>
);
return seriesInformation;
};
return (
<div className={classNames('series-details', { 'info-only': infoOnly })}>
<div className="series-description">{SeriesDescription}</div>
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames)}
{getSeriesInformation(
SeriesNumber,
InstanceNumber,
numImageFrames,
inconsistencyWarnings
)}
</div>
);
}
@@ -60,14 +133,38 @@ function Thumbnail(props) {
numImageFrames,
SeriesDescription,
SeriesNumber,
stackPercentComplete,
hasWarnings,
StudyInstanceUID,
onClick,
onDoubleClick,
onMouseDown,
supportsDrag,
showProgressBar,
} = props;
const [stackPercentComplete, setStackPercentComplete] = useState(0);
useEffect(() => {
const onProgressChange = ({ detail }) => {
const { progressId, progressData } = detail;
if (`StackProgress:${displaySetInstanceUID}` === progressId) {
const percent = progressData ? progressData.percentComplete : 0;
setStackPercentComplete(percent);
}
};
document.addEventListener(
StudyLoadingListener.events.OnProgress,
onProgressChange
);
return () => {
document.removeEventListener(
StudyLoadingListener.events.OnProgress,
onProgressChange
);
};
}, [displaySetInstanceUID]);
const [collectedProps, drag, dragPreview] = useDrag({
// `droppedItem` in `dropTarget`
// The only data it will have access to
@@ -95,10 +192,12 @@ function Thumbnail(props) {
{/* SHOW IMAGE */}
{hasImage && (
<ImageThumbnail
active={active}
imageSrc={imageSrc}
imageId={imageId}
error={error}
stackPercentComplete={stackPercentComplete}
showProgressBar={showProgressBar}
/>
)}
{/* SHOW TEXT ALTERNATIVE */}
@@ -126,17 +225,19 @@ Thumbnail.propTypes = {
stackPercentComplete: PropTypes.number,
/**
altImageText will be used when no imageId or imageSrc is provided.
It will be displayed inside the <div>. This is useful when it is difficult
It will be displayed inside the <div>. This is useful when it is difficult
to make a preview for a type of DICOM series (e.g. DICOM-SR)
*/
altImageText: PropTypes.string,
SeriesDescription: PropTypes.string,
SeriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
InstanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
hasWarnings: PropTypes.instanceOf(Promise),
numImageFrames: PropTypes.number,
onDoubleClick: PropTypes.func,
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
showProgressBar: PropTypes.bool,
};
Thumbnail.defaultProps = {
@@ -2,8 +2,15 @@
:root {
--series-count-background-color: #678696;
--active-color: #20A5D6;
}
.thumbnail.active
.alt-image-text
border-color: var(--active-color);
box-shadow: none
transition: var(--sidebar-transition);
.thumbnail
cursor: pointer
display: table
@@ -16,7 +23,7 @@
justify-content: center;
background-color: var(--primary-background-color);
box-shadow: inset 0 0 0 1px var(--ui-border-color-dark);
border: 5px solid transparent;
border: 2px solid transparent;
border-radius: 12px;
height: 135px;
margin: 0 auto;
@@ -78,6 +85,17 @@
height: 11px
width: 11px
.warning
margin: auto 0;
opacity: 1
color: #e29e4a;
svg
width: 16px;
height: 14px;
pointer-events: inherit;
.value
color: var(--text-secondary-color);
display: inline-block
@@ -6,10 +6,13 @@ import React, {
useEffect,
} from 'react';
import PropTypes from 'prop-types';
import { classes } from '@ohif/core';
import SnackbarContainer from '../components/snackbar/SnackbarContainer';
import SnackbarTypes from '../components/snackbar/SnackbarTypes';
const { LogManager } = classes;
const SnackbarContext = createContext(null);
export const useSnackbarContext = () => useContext(SnackbarContext);
@@ -28,6 +31,20 @@ const SnackbarProvider = ({ children, service }) => {
const [count, setCount] = useState(1);
const [snackbarItems, setSnackbarItems] = useState([]);
useEffect(() => {
const onLogHandler = ({ type, notify, title, message }) => {
if (notify) {
show({ type, title, message });
}
};
LogManager.subscribe(LogManager.EVENTS.OnLog, onLogHandler);
return () => {
LogManager.subscribe(LogManager.EVENTS.OnLog, onLogHandler);
};
}, [show]);
const show = useCallback(
options => {
if (!options || (!options.title && !options.message)) {
+31 -2
View File
@@ -12,8 +12,6 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
// ~~ Rules
const extractStyleChunksRule = require('./rules/extractStyleChunks.js');
// ~~ Directories
@@ -41,6 +39,23 @@ module.exports = (env, argv) => {
path: DIST_DIR,
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix
devtoolModuleFilenameTemplate: function(info) {
if (isProdBuild) {
return `webpack:///${info.resourcePath}`;
} else {
return 'file:///' + encodeURI(info.absoluteResourcePath);
}
},
},
resolve: {
// We use this alias and the CopyPlugin below to support using the dynamic-import version
// of WADO Image Loader, but only when building a PWA. When we build a package, we must use the
// bundled version of WADO Image Loader so we can produce a single file for the viewer.
// (Note: script-tag version of the viewer will no longer be supported in OHIF v3)
alias: {
'cornerstone-wado-image-loader':
'cornerstone-wado-image-loader/dist/dynamic-import/cornerstoneWADOImageLoader.min.js',
},
},
module: {
rules: [...extractStyleChunksRule(isProdBuild)],
@@ -71,6 +86,11 @@ module.exports = (env, argv) => {
from: `${PUBLIC_DIR}/${APP_CONFIG}`,
to: `${DIST_DIR}/app-config.js`,
},
{
from:
'../../../node_modules/cornerstone-wado-image-loader/dist/dynamic-import',
to: DIST_DIR,
},
]),
// https://github.com/faceyspacey/extract-css-chunks-webpack-plugin#webpack-4-standalone-installation
new ExtractCssChunksPlugin({
@@ -95,6 +115,15 @@ module.exports = (env, argv) => {
// maximumFileSizeToCacheInBytes: 4 * 1024 * 1024
}),
],
optimization: {
splitChunks: {
// include all types of chunks
chunks: 'all',
},
//runtimeChunk: 'single',
minimize: isProdBuild,
sideEffects: true,
},
// https://webpack.js.org/configuration/dev-server/
devServer: {
// gzip compression of everything served
+203
View File
@@ -3,6 +3,209 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [4.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.10.1...@ohif/viewer@4.11.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))
## [4.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.10.0...@ohif/viewer@4.10.1) (2021-09-13)
**Note:** Version bump only for package @ohif/viewer
# [4.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.24...@ohif/viewer@4.10.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))
## [4.9.24](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.23...@ohif/viewer@4.9.24) (2021-09-03)
**Note:** Version bump only for package @ohif/viewer
## [4.9.23](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.22...@ohif/viewer@4.9.23) (2021-08-30)
### Bug Fixes
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
## [4.9.22](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.21...@ohif/viewer@4.9.22) (2021-08-19)
### Bug Fixes
* Remove internet dependency for workbox (addressing [#2109](https://github.com/OHIF/Viewers/issues/2109)) ([#2151](https://github.com/OHIF/Viewers/issues/2151)) ([5f92f6e](https://github.com/OHIF/Viewers/commit/5f92f6ec7faa11d3fdc33ef8f22971a0bab87a36))
## [4.9.21](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.20...@ohif/viewer@4.9.21) (2021-07-26)
**Note:** Version bump only for package @ohif/viewer
## [4.9.20](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.19...@ohif/viewer@4.9.20) (2021-06-03)
**Note:** Version bump only for package @ohif/viewer
## [4.9.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.18...@ohif/viewer@4.9.19) (2021-06-03)
**Note:** Version bump only for package @ohif/viewer
## [4.9.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.17...@ohif/viewer@4.9.18) (2021-06-02)
### Bug Fixes
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
## [4.9.17](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.16...@ohif/viewer@4.9.17) (2021-05-17)
**Note:** Version bump only for package @ohif/viewer
## [4.9.16](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.15...@ohif/viewer@4.9.16) (2021-05-14)
**Note:** Version bump only for package @ohif/viewer
## [4.9.15](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.14...@ohif/viewer@4.9.15) (2021-04-22)
**Note:** Version bump only for package @ohif/viewer
## [4.9.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.13...@ohif/viewer@4.9.14) (2021-04-22)
**Note:** Version bump only for package @ohif/viewer
## [4.9.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.12...@ohif/viewer@4.9.13) (2021-04-21)
**Note:** Version bump only for package @ohif/viewer
## [4.9.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.11...@ohif/viewer@4.9.12) (2021-04-21)
**Note:** Version bump only for package @ohif/viewer
## [4.9.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.10...@ohif/viewer@4.9.11) (2021-04-21)
**Note:** Version bump only for package @ohif/viewer
## [4.9.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.9...@ohif/viewer@4.9.10) (2021-04-16)
**Note:** Version bump only for package @ohif/viewer
## [4.9.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.8...@ohif/viewer@4.9.9) (2021-04-15)
**Note:** Version bump only for package @ohif/viewer
## [4.9.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.7...@ohif/viewer@4.9.8) (2021-03-31)
**Note:** Version bump only for package @ohif/viewer
## [4.9.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.6...@ohif/viewer@4.9.7) (2021-03-26)
**Note:** Version bump only for package @ohif/viewer
## [4.9.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.5...@ohif/viewer@4.9.6) (2021-03-26)
**Note:** Version bump only for package @ohif/viewer
## [4.9.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.4...@ohif/viewer@4.9.5) (2021-03-25)
### Bug Fixes
* IDC-2288: Update toolbar tabs after derived displayset loaded ([#2307](https://github.com/OHIF/Viewers/issues/2307)) ([6e723eb](https://github.com/OHIF/Viewers/commit/6e723ebced58852007ca553792c740976cfc4201))
## [4.9.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.3...@ohif/viewer@4.9.4) (2021-03-19)
**Note:** Version bump only for package @ohif/viewer
@@ -91,7 +91,7 @@ describe('OHIF Cornerstone Hotkeys', () => {
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .left-mid.orientation-marker'
).as('viewport2InfoMidLeft');
cy.get(
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOverlay > div.bottom-right.overlay-element > div'
':nth-child(2) > .viewport-wrapper > .viewport-element > .OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div'
).as('viewport2InfoBottomRight');
// Press multiples hotkeys on viewport #2
@@ -333,6 +333,7 @@ describe('OHIF User Preferences', () => {
.scrollIntoView()
.click();
// Header should be in "English (USA)""
cy.get('.research-use')
.scrollIntoView()
@@ -560,6 +561,13 @@ describe('OHIF User Preferences', () => {
.scrollIntoView()
.click();
// Close Success Message overlay (if displayed)
cy.get('body').then(body => {
if (body.find('.sb-closeIcon').length > 0) {
cy.get('.sb-closeIcon').click({ force: true });
}
});
// Open User Preferences modal again
cy.openPreferences();
@@ -668,6 +676,13 @@ describe('OHIF User Preferences', () => {
// Save changes
cy.get('@saveBtn').click();
// Close Success Message overlay (if displayed)
cy.get('body').then(body => {
if (body.find('.sb-closeIcon').length > 0) {
cy.get('.sb-closeIcon').click({ force: true });
}
});
// Open User Preferences modal
cy.openPreferences();
// Navigate to Window Level tab
@@ -741,7 +756,7 @@ describe('OHIF User Preferences', () => {
// Overlay information from 2nd viewport
let second_viewport_overlay =
'div:nth-child(2) > div > div.viewport-element > div.ViewportOverlay > div.bottom-right.overlay-element > div';
'div:nth-child(2) > div > div.viewport-element > div.OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div';
// Shift active viewport to Viewport #2
cy.get('body').type('{rightarrow}');
+2 -2
View File
@@ -25,10 +25,10 @@ export function initCommonElementsAliases() {
'.pull-left > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton'
).as('seriesBtn');
cy.get('section.sidepanel.from-left').as('seriesPanel');
cy.get('div.ViewportOverlay > div.bottom-left.overlay-element > div').as(
cy.get('div.OHIFCornerstoneViewportOverlay > div.bottom-left.overlay-element > div').as(
'viewportInfoBottomLeft'
);
cy.get('div.ViewportOverlay > div.bottom-right.overlay-element > div').as(
cy.get('div.OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div').as(
'viewportInfoBottomRight'
);
cy.get('.left-mid.orientation-marker').as('viewportInfoMidLeft');
+6 -4
View File
@@ -504,6 +504,9 @@ Cypress.Commands.add('resetUserHotkeyPreferences', () => {
cy.get('@restoreBtn').click();
});
// Click on Save Button
cy.get('@saveBtn').click();
// Close Success Message overlay (if displayed)
cy.get('body').then(body => {
if (body.find('.sb-closeIcon').length > 0) {
@@ -511,8 +514,6 @@ Cypress.Commands.add('resetUserHotkeyPreferences', () => {
.first()
.click({ force: true });
}
// Click on Save Button
cy.get('@saveBtn').click();
});
});
@@ -525,6 +526,9 @@ Cypress.Commands.add('resetUserGeneralPreferences', () => {
cy.get('@restoreBtn').click();
});
// Click on Save Button
cy.get('@saveBtn').click();
// Close Success Message overlay (if displayed)
cy.get('body').then(body => {
if (body.find('.sb-closeIcon').length > 0) {
@@ -532,8 +536,6 @@ Cypress.Commands.add('resetUserGeneralPreferences', () => {
.first()
.click({ force: true });
}
// Click on Save Button
cy.get('@saveBtn').click();
});
});
+17 -19
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "4.9.4",
"version": "4.11.0",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -19,14 +19,12 @@
"scripts": {
"build:package": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.commonjs.js --progress",
"build:viewer": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
"build:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
"build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/pwa/ APP_CONFIG=config/netlify.js QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js",
"build:viewer:demo": "cross-env NODE_ENV=production APP_CONFIG=config/demo.js HTML_TEMPLATE=rollbar.html QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --progress --config .webpack/webpack.pwa.js",
"build:viewer:package": "yarn run build:package",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"dev:viewer": "yarn run dev",
"dev:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"start": "yarn run dev",
"test:e2e": "cypress open",
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
@@ -49,28 +47,28 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^2.13.1",
"@ohif/extension-cornerstone": "^2.10.5",
"@ohif/extension-debugging": "^0.2.1",
"@ohif/extension-dicom-html": "^1.3.4",
"@ohif/core": "^2.15.0",
"@ohif/extension-cornerstone": "^2.12.0",
"@ohif/extension-debugging": "^0.3.0",
"@ohif/extension-dicom-html": "^1.3.10",
"@ohif/extension-dicom-microscopy": "^0.52.0",
"@ohif/extension-dicom-pdf": "^1.0.6",
"@ohif/extension-dicom-rt": "^0.6.7",
"@ohif/extension-dicom-segmentation": "^0.6.1",
"@ohif/extension-dicom-pdf": "^1.1.0",
"@ohif/extension-dicom-rt": "^0.7.0",
"@ohif/extension-dicom-segmentation": "^0.7.0",
"@ohif/extension-lesion-tracker": "^0.2.1",
"@ohif/extension-vtk": "^1.11.2",
"@ohif/extension-vtk": "^1.12.0",
"@ohif/i18n": "^0.53.0",
"@ohif/ui": "^1.9.0",
"@ohif/ui": "^1.10.1",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
"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.3",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4",
"cornerstone-core": "^2.5.0",
"cornerstone-math": "^0.1.9",
"cornerstone-tools": "^6.0.0",
"cornerstone-wado-image-loader": "^4.0.3",
"dcmjs": "0.18.8",
"dicom-parser": "^1.8.9",
"dicomweb-client": "^0.8.1",
"hammerjs": "^2.0.8",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
+11 -1
View File
@@ -4,6 +4,16 @@ window.config = {
extensions: [],
showStudyList: true,
filterQueryParam: false,
disableServersCache: false,
studyPrefetcher: {
enabled: true,
order: 'closest',
displaySetCount: 3,
preventCache: false,
prefetchDisplaySetsTimeout: 300,
displayProgress: true,
includeActiveDisplaySet: true,
},
servers: {
dicomWeb: [
{
@@ -123,6 +133,6 @@ window.config = {
// If the server is particularly slow to respond to series metadata
// requests as it extracts the metadata from raw files everytime,
// try setting this to even lower value
// Leave it undefined for no limit, sutiable for HTTP/2 enabled servers
// Leave it undefined for no limit, suitable for HTTP/2 enabled servers
// maxConcurrentMetadataRequests: 5,
};
+9
View File
@@ -1,6 +1,15 @@
window.config = {
routerBasename: '/pwa',
showStudyList: true,
studyPrefetcher: {
enabled: true,
order: 'closest',
displaySetCount: 3,
preventCache: false,
prefetchDisplaySetsTimeout: 300,
displayProgress: true,
includeActiveDisplaySet: true,
},
servers: {
dicomWeb: [
{
+12 -1
View File
@@ -3,7 +3,17 @@
// modules, so it's perfectly fine to serve this code to any browsers
// (older browsers will just ignore it)
//
import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/5.0.0-beta.1/workbox-window.prod.mjs';
// The workbox code is locally stored and can be updated
// by running `npx workbox-cli copyLibraries platform/viewer/public/third_party/workbox`
// from the root directory.
// Since the workbox version may change with copyLibraries ensure that the
// right version is used here and in `service-worker.js`
import { Workbox } from './third_party/workbox/workbox-v5.1.4/workbox-window.dev.mjs';
// Alternatively use the internet version by commenting out the above import and
// replace with this line and do the corresponding change in service-worker.js
// import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/5.0.0-beta.1/workbox-window.prod.mjs';
var supportsServiceWorker = 'serviceWorker' in navigator;
var isNotLocalDevelopment =
@@ -11,6 +21,7 @@ var isNotLocalDevelopment =
if (supportsServiceWorker && isNotLocalDevelopment) {
const swFileLocation = (window.PUBLIC_URL || '/') + 'sw.js';
const wb = new Workbox(swFileLocation);
// Add an event listener to detect when the registered
@@ -0,0 +1,818 @@
this.workbox = this.workbox || {};
this.workbox.backgroundSync = (function (exports, WorkboxError_js, logger_js, assert_js, getFriendlyURL_js, DBWrapper_js) {
'use strict';
try {
self['workbox:background-sync:5.1.4'] && _();
} catch (e) {}
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
const DB_VERSION = 3;
const DB_NAME = 'workbox-background-sync';
const OBJECT_STORE_NAME = 'requests';
const INDEXED_PROP = 'queueName';
/**
* A class to manage storing requests from a Queue in IndexedDB,
* indexed by their queue name for easier access.
*
* @private
*/
class QueueStore {
/**
* Associates this instance with a Queue instance, so entries added can be
* identified by their queue name.
*
* @param {string} queueName
* @private
*/
constructor(queueName) {
this._queueName = queueName;
this._db = new DBWrapper_js.DBWrapper(DB_NAME, DB_VERSION, {
onupgradeneeded: this._upgradeDb
});
}
/**
* Append an entry last in the queue.
*
* @param {Object} entry
* @param {Object} entry.requestData
* @param {number} [entry.timestamp]
* @param {Object} [entry.metadata]
* @private
*/
async pushEntry(entry) {
{
assert_js.assert.isType(entry, 'object', {
moduleName: 'workbox-background-sync',
className: 'QueueStore',
funcName: 'pushEntry',
paramName: 'entry'
});
assert_js.assert.isType(entry.requestData, 'object', {
moduleName: 'workbox-background-sync',
className: 'QueueStore',
funcName: 'pushEntry',
paramName: 'entry.requestData'
});
} // Don't specify an ID since one is automatically generated.
delete entry.id;
entry.queueName = this._queueName;
await this._db.add(OBJECT_STORE_NAME, entry);
}
/**
* Prepend an entry first in the queue.
*
* @param {Object} entry
* @param {Object} entry.requestData
* @param {number} [entry.timestamp]
* @param {Object} [entry.metadata]
* @private
*/
async unshiftEntry(entry) {
{
assert_js.assert.isType(entry, 'object', {
moduleName: 'workbox-background-sync',
className: 'QueueStore',
funcName: 'unshiftEntry',
paramName: 'entry'
});
assert_js.assert.isType(entry.requestData, 'object', {
moduleName: 'workbox-background-sync',
className: 'QueueStore',
funcName: 'unshiftEntry',
paramName: 'entry.requestData'
});
}
const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {
count: 1
});
if (firstEntry) {
// Pick an ID one less than the lowest ID in the object store.
entry.id = firstEntry.id - 1;
} else {
// Otherwise let the auto-incrementor assign the ID.
delete entry.id;
}
entry.queueName = this._queueName;
await this._db.add(OBJECT_STORE_NAME, entry);
}
/**
* Removes and returns the last entry in the queue matching the `queueName`.
*
* @return {Promise<Object>}
* @private
*/
async popEntry() {
return this._removeEntry({
direction: 'prev'
});
}
/**
* Removes and returns the first entry in the queue matching the `queueName`.
*
* @return {Promise<Object>}
* @private
*/
async shiftEntry() {
return this._removeEntry({
direction: 'next'
});
}
/**
* Returns all entries in the store matching the `queueName`.
*
* @param {Object} options See {@link module:workbox-background-sync.Queue~getAll}
* @return {Promise<Array<Object>>}
* @private
*/
async getAll() {
return await this._db.getAllMatching(OBJECT_STORE_NAME, {
index: INDEXED_PROP,
query: IDBKeyRange.only(this._queueName)
});
}
/**
* Deletes the entry for the given ID.
*
* WARNING: this method does not ensure the deleted enry belongs to this
* queue (i.e. matches the `queueName`). But this limitation is acceptable
* as this class is not publicly exposed. An additional check would make
* this method slower than it needs to be.
*
* @private
* @param {number} id
*/
async deleteEntry(id) {
await this._db.delete(OBJECT_STORE_NAME, id);
}
/**
* Removes and returns the first or last entry in the queue (based on the
* `direction` argument) matching the `queueName`.
*
* @return {Promise<Object>}
* @private
*/
async _removeEntry({
direction
}) {
const [entry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {
direction,
index: INDEXED_PROP,
query: IDBKeyRange.only(this._queueName),
count: 1
});
if (entry) {
await this.deleteEntry(entry.id);
return entry;
}
}
/**
* Upgrades the database given an `upgradeneeded` event.
*
* @param {Event} event
* @private
*/
_upgradeDb(event) {
const db = event.target.result;
if (event.oldVersion > 0 && event.oldVersion < DB_VERSION) {
if (db.objectStoreNames.contains(OBJECT_STORE_NAME)) {
db.deleteObjectStore(OBJECT_STORE_NAME);
}
}
const objStore = db.createObjectStore(OBJECT_STORE_NAME, {
autoIncrement: true,
keyPath: 'id'
});
objStore.createIndex(INDEXED_PROP, INDEXED_PROP, {
unique: false
});
}
}
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
const serializableProperties = ['method', 'referrer', 'referrerPolicy', 'mode', 'credentials', 'cache', 'redirect', 'integrity', 'keepalive'];
/**
* A class to make it easier to serialize and de-serialize requests so they
* can be stored in IndexedDB.
*
* @private
*/
class StorableRequest {
/**
* Accepts an object of request data that can be used to construct a
* `Request` but can also be stored in IndexedDB.
*
* @param {Object} requestData An object of request data that includes the
* `url` plus any relevant properties of
* [requestInit]{@link https://fetch.spec.whatwg.org/#requestinit}.
* @private
*/
constructor(requestData) {
{
assert_js.assert.isType(requestData, 'object', {
moduleName: 'workbox-background-sync',
className: 'StorableRequest',
funcName: 'constructor',
paramName: 'requestData'
});
assert_js.assert.isType(requestData.url, 'string', {
moduleName: 'workbox-background-sync',
className: 'StorableRequest',
funcName: 'constructor',
paramName: 'requestData.url'
});
} // If the request's mode is `navigate`, convert it to `same-origin` since
// navigation requests can't be constructed via script.
if (requestData['mode'] === 'navigate') {
requestData['mode'] = 'same-origin';
}
this._requestData = requestData;
}
/**
* Converts a Request object to a plain object that can be structured
* cloned or JSON-stringified.
*
* @param {Request} request
* @return {Promise<StorableRequest>}
*
* @private
*/
static async fromRequest(request) {
const requestData = {
url: request.url,
headers: {}
}; // Set the body if present.
if (request.method !== 'GET') {
// Use ArrayBuffer to support non-text request bodies.
// NOTE: we can't use Blobs becuse Safari doesn't support storing
// Blobs in IndexedDB in some cases:
// https://github.com/dfahlander/Dexie.js/issues/618#issuecomment-398348457
requestData.body = await request.clone().arrayBuffer();
} // Convert the headers from an iterable to an object.
for (const [key, value] of request.headers.entries()) {
requestData.headers[key] = value;
} // Add all other serializable request properties
for (const prop of serializableProperties) {
if (request[prop] !== undefined) {
requestData[prop] = request[prop];
}
}
return new StorableRequest(requestData);
}
/**
* Returns a deep clone of the instances `_requestData` object.
*
* @return {Object}
*
* @private
*/
toObject() {
const requestData = Object.assign({}, this._requestData);
requestData.headers = Object.assign({}, this._requestData.headers);
if (requestData.body) {
requestData.body = requestData.body.slice(0);
}
return requestData;
}
/**
* Converts this instance to a Request.
*
* @return {Request}
*
* @private
*/
toRequest() {
return new Request(this._requestData.url, this._requestData);
}
/**
* Creates and returns a deep clone of the instance.
*
* @return {StorableRequest}
*
* @private
*/
clone() {
return new StorableRequest(this.toObject());
}
}
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
const TAG_PREFIX = 'workbox-background-sync';
const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes
const queueNames = new Set();
/**
* Converts a QueueStore entry into the format exposed by Queue. This entails
* converting the request data into a real request and omitting the `id` and
* `queueName` properties.
*
* @param {Object} queueStoreEntry
* @return {Object}
* @private
*/
const convertEntry = queueStoreEntry => {
const queueEntry = {
request: new StorableRequest(queueStoreEntry.requestData).toRequest(),
timestamp: queueStoreEntry.timestamp
};
if (queueStoreEntry.metadata) {
queueEntry.metadata = queueStoreEntry.metadata;
}
return queueEntry;
};
/**
* A class to manage storing failed requests in IndexedDB and retrying them
* later. All parts of the storing and replaying process are observable via
* callbacks.
*
* @memberof module:workbox-background-sync
*/
class Queue {
/**
* Creates an instance of Queue with the given options
*
* @param {string} name The unique name for this queue. This name must be
* unique as it's used to register sync events and store requests
* in IndexedDB specific to this instance. An error will be thrown if
* a duplicate name is detected.
* @param {Object} [options]
* @param {Function} [options.onSync] A function that gets invoked whenever
* the 'sync' event fires. The function is invoked with an object
* containing the `queue` property (referencing this instance), and you
* can use the callback to customize the replay behavior of the queue.
* When not set the `replayRequests()` method is called.
* Note: if the replay fails after a sync event, make sure you throw an
* error, so the browser knows to retry the sync event later.
* @param {number} [options.maxRetentionTime=7 days] The amount of time (in
* minutes) a request may be retried. After this amount of time has
* passed, the request will be deleted from the queue.
*/
constructor(name, {
onSync,
maxRetentionTime
} = {}) {
this._syncInProgress = false;
this._requestsAddedDuringSync = false; // Ensure the store name is not already being used
if (queueNames.has(name)) {
throw new WorkboxError_js.WorkboxError('duplicate-queue-name', {
name
});
} else {
queueNames.add(name);
}
this._name = name;
this._onSync = onSync || this.replayRequests;
this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
this._queueStore = new QueueStore(this._name);
this._addSyncListener();
}
/**
* @return {string}
*/
get name() {
return this._name;
}
/**
* Stores the passed request in IndexedDB (with its timestamp and any
* metadata) at the end of the queue.
*
* @param {Object} entry
* @param {Request} entry.request The request to store in the queue.
* @param {Object} [entry.metadata] Any metadata you want associated with the
* stored request. When requests are replayed you'll have access to this
* metadata object in case you need to modify the request beforehand.
* @param {number} [entry.timestamp] The timestamp (Epoch time in
* milliseconds) when the request was first added to the queue. This is
* used along with `maxRetentionTime` to remove outdated requests. In
* general you don't need to set this value, as it's automatically set
* for you (defaulting to `Date.now()`), but you can update it if you
* don't want particular requests to expire.
*/
async pushRequest(entry) {
{
assert_js.assert.isType(entry, 'object', {
moduleName: 'workbox-background-sync',
className: 'Queue',
funcName: 'pushRequest',
paramName: 'entry'
});
assert_js.assert.isInstance(entry.request, Request, {
moduleName: 'workbox-background-sync',
className: 'Queue',
funcName: 'pushRequest',
paramName: 'entry.request'
});
}
await this._addRequest(entry, 'push');
}
/**
* Stores the passed request in IndexedDB (with its timestamp and any
* metadata) at the beginning of the queue.
*
* @param {Object} entry
* @param {Request} entry.request The request to store in the queue.
* @param {Object} [entry.metadata] Any metadata you want associated with the
* stored request. When requests are replayed you'll have access to this
* metadata object in case you need to modify the request beforehand.
* @param {number} [entry.timestamp] The timestamp (Epoch time in
* milliseconds) when the request was first added to the queue. This is
* used along with `maxRetentionTime` to remove outdated requests. In
* general you don't need to set this value, as it's automatically set
* for you (defaulting to `Date.now()`), but you can update it if you
* don't want particular requests to expire.
*/
async unshiftRequest(entry) {
{
assert_js.assert.isType(entry, 'object', {
moduleName: 'workbox-background-sync',
className: 'Queue',
funcName: 'unshiftRequest',
paramName: 'entry'
});
assert_js.assert.isInstance(entry.request, Request, {
moduleName: 'workbox-background-sync',
className: 'Queue',
funcName: 'unshiftRequest',
paramName: 'entry.request'
});
}
await this._addRequest(entry, 'unshift');
}
/**
* Removes and returns the last request in the queue (along with its
* timestamp and any metadata). The returned object takes the form:
* `{request, timestamp, metadata}`.
*
* @return {Promise<Object>}
*/
async popRequest() {
return this._removeRequest('pop');
}
/**
* Removes and returns the first request in the queue (along with its
* timestamp and any metadata). The returned object takes the form:
* `{request, timestamp, metadata}`.
*
* @return {Promise<Object>}
*/
async shiftRequest() {
return this._removeRequest('shift');
}
/**
* Returns all the entries that have not expired (per `maxRetentionTime`).
* Any expired entries are removed from the queue.
*
* @return {Promise<Array<Object>>}
*/
async getAll() {
const allEntries = await this._queueStore.getAll();
const now = Date.now();
const unexpiredEntries = [];
for (const entry of allEntries) {
// Ignore requests older than maxRetentionTime. Call this function
// recursively until an unexpired request is found.
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
if (now - entry.timestamp > maxRetentionTimeInMs) {
await this._queueStore.deleteEntry(entry.id);
} else {
unexpiredEntries.push(convertEntry(entry));
}
}
return unexpiredEntries;
}
/**
* Adds the entry to the QueueStore and registers for a sync event.
*
* @param {Object} entry
* @param {Request} entry.request
* @param {Object} [entry.metadata]
* @param {number} [entry.timestamp=Date.now()]
* @param {string} operation ('push' or 'unshift')
* @private
*/
async _addRequest({
request,
metadata,
timestamp = Date.now()
}, operation) {
const storableRequest = await StorableRequest.fromRequest(request.clone());
const entry = {
requestData: storableRequest.toObject(),
timestamp
}; // Only include metadata if it's present.
if (metadata) {
entry.metadata = metadata;
}
await this._queueStore[`${operation}Entry`](entry);
{
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(request.url)}' has ` + `been added to background sync queue '${this._name}'.`);
} // Don't register for a sync if we're in the middle of a sync. Instead,
// we wait until the sync is complete and call register if
// `this._requestsAddedDuringSync` is true.
if (this._syncInProgress) {
this._requestsAddedDuringSync = true;
} else {
await this.registerSync();
}
}
/**
* Removes and returns the first or last (depending on `operation`) entry
* from the QueueStore that's not older than the `maxRetentionTime`.
*
* @param {string} operation ('pop' or 'shift')
* @return {Object|undefined}
* @private
*/
async _removeRequest(operation) {
const now = Date.now();
const entry = await this._queueStore[`${operation}Entry`]();
if (entry) {
// Ignore requests older than maxRetentionTime. Call this function
// recursively until an unexpired request is found.
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
if (now - entry.timestamp > maxRetentionTimeInMs) {
return this._removeRequest(operation);
}
return convertEntry(entry);
} else {
return undefined;
}
}
/**
* Loops through each request in the queue and attempts to re-fetch it.
* If any request fails to re-fetch, it's put back in the same position in
* the queue (which registers a retry for the next sync event).
*/
async replayRequests() {
let entry;
while (entry = await this.shiftRequest()) {
try {
await fetch(entry.request.clone());
if ("dev" !== 'production') {
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}'` + `has been replayed in queue '${this._name}'`);
}
} catch (error) {
await this.unshiftRequest(entry);
{
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}'` + `failed to replay, putting it back in queue '${this._name}'`);
}
throw new WorkboxError_js.WorkboxError('queue-replay-failed', {
name: this._name
});
}
}
{
logger_js.logger.log(`All requests in queue '${this.name}' have successfully ` + `replayed; the queue is now empty!`);
}
}
/**
* Registers a sync event with a tag unique to this instance.
*/
async registerSync() {
if ('sync' in self.registration) {
try {
await self.registration.sync.register(`${TAG_PREFIX}:${this._name}`);
} catch (err) {
// This means the registration failed for some reason, possibly due to
// the user disabling it.
{
logger_js.logger.warn(`Unable to register sync event for '${this._name}'.`, err);
}
}
}
}
/**
* In sync-supporting browsers, this adds a listener for the sync event.
* In non-sync-supporting browsers, this will retry the queue on service
* worker startup.
*
* @private
*/
_addSyncListener() {
if ('sync' in self.registration) {
self.addEventListener('sync', event => {
if (event.tag === `${TAG_PREFIX}:${this._name}`) {
{
logger_js.logger.log(`Background sync for tag '${event.tag}'` + `has been received`);
}
const syncComplete = async () => {
this._syncInProgress = true;
let syncError;
try {
await this._onSync({
queue: this
});
} catch (error) {
syncError = error; // Rethrow the error. Note: the logic in the finally clause
// will run before this gets rethrown.
throw syncError;
} finally {
// New items may have been added to the queue during the sync,
// so we need to register for a new sync if that's happened...
// Unless there was an error during the sync, in which
// case the browser will automatically retry later, as long
// as `event.lastChance` is not true.
if (this._requestsAddedDuringSync && !(syncError && !event.lastChance)) {
await this.registerSync();
}
this._syncInProgress = false;
this._requestsAddedDuringSync = false;
}
};
event.waitUntil(syncComplete());
}
});
} else {
{
logger_js.logger.log(`Background sync replaying without background sync event`);
} // If the browser doesn't support background sync, retry
// every time the service worker starts up as a fallback.
this._onSync({
queue: this
});
}
}
/**
* Returns the set of queue names. This is primarily used to reset the list
* of queue names in tests.
*
* @return {Set}
*
* @private
*/
static get _queueNames() {
return queueNames;
}
}
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
/**
* A class implementing the `fetchDidFail` lifecycle callback. This makes it
* easier to add failed requests to a background sync Queue.
*
* @memberof module:workbox-background-sync
*/
class BackgroundSyncPlugin {
/**
* @param {string} name See the [Queue]{@link module:workbox-background-sync.Queue}
* documentation for parameter details.
* @param {Object} [options] See the
* [Queue]{@link module:workbox-background-sync.Queue} documentation for
* parameter details.
*/
constructor(name, options) {
/**
* @param {Object} options
* @param {Request} options.request
* @private
*/
this.fetchDidFail = async ({
request
}) => {
await this._queue.pushRequest({
request
});
};
this._queue = new Queue(name, options);
}
}
exports.BackgroundSyncPlugin = BackgroundSyncPlugin;
exports.Queue = Queue;
return exports;
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
//# sourceMappingURL=workbox-background-sync.dev.js.map
Loaded 100 of 174 files, more files were not shown because too many files have changed in this diff. Show more