Compare commits

..
Author SHA1 Message Date
ohif-bot cd28877ed8 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.1
 - @ohif/ui@0.60.1
 - @ohif/viewer@2.6.2
2019-11-08 12:14:44 +00:00
Mirna Silva e81a39f5be Merge pull request #1159 from mirnasilva/fix_studylist
Fix for modalities search result
2019-11-08 09:02:34 -03:00
Mirna Silva 9a2cf70504 Fix for modalities search result 2019-11-08 08:47:32 -03:00
ladeirarodolfo d70eae3eb0 fix: Fix display issues with incorrect thumbnails. Change ImageThumb to functional component. (#1148) 2019-11-08 11:26:31 +01:00
ohif-bot 8481e94d4d chore(release): publish [skip ci]
- @ohif/viewer@2.6.1
2019-11-06 04:56:33 +00:00
Mirna Silva 19b22c39ae test: Study list test cases: Patient Name, MRN, Modality, Description (#1121)
* Study list tests: Patient Name, MRN, Modality, Description

* Fixed field selector

* Desktop tests

* Fix for study list

* Added tablet tests and small refactor on aliases

* Fix based on reviews
2019-11-05 23:46:38 -05:00
ohif-bot 05802a3cf1 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.0
 - @ohif/i18n@0.52.0
 - @ohif/ui@0.60.0
 - @ohif/viewer@2.6.0
2019-11-06 04:23:15 +00:00
Danny Brown 75d88bc454 feat: modal provider (#1151)
* feat: 🎸 New modal provider

A new modal provider/context component to unify modal related code and
simplify modal use

Closes: #1086

* refactor(ModalContext): Keep current modal lib

* Update modal provider version according to PR #1116

* Update modal provider props to use inner props

* Add custom class prop

* CR Update: Refactor provider to extract specific modal

* Fix modal import

* CR Update: Move from spread to object assign

* CR Update: Add proptypes, use classnames dependency over interpolation and rename modal

* fix: 🎸 Update AboutModal to use new modal provider (#1116)

* feat: 🎸 Update AboutModal to use new modal provider

This feature updates the AboutModal component and Header to use the new
modal context (modal provider)

* feat(ModalProvider): Add title prop to provider

* fix(ModalContext): Add defaults on hide

* CR Updates: Refactor modal provider and fix import

* CR Update: Expose props and flat modal optons

* Use customclassname instead of static

* Add cypress tag

* test: set right viewport when layout is decreased (#1146)

* chore(release): publish [skip ci]

 - @ohif/viewer@2.3.6

* fix: 🐛 Minor issues measurement panel related to description (#1142)

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.52.28
 - @ohif/ui@0.58.5
 - @ohif/viewer@2.3.7

* fix: 🐛 Upgrade react-vtkjs-viewport to fix volume orientation (#1143)

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.52.29
 - @ohif/viewer@2.3.8

* CR Update: Extract and rename AboutModal to AboutContent

* feat: 🎸 New modal provider (#1110)

* feat: 🎸 New modal provider

A new modal provider/context component to unify modal related code and
simplify modal use

Closes: #1086

* refactor(ModalContext): Keep current modal lib

* Update modal provider version according to PR #1116

* Update modal provider props to use inner props

* Add custom class prop

* CR Update: Refactor provider to extract specific modal

* Fix modal import

* CR Update: Move from spread to object assign

* CR Update: Add proptypes, use classnames dependency over interpolation and rename modal

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.52.30
 - @ohif/ui@0.59.0
 - @ohif/viewer@2.4.0

* CR Update: Keep translation name

* CR Update: Reposition cy tag of about modal in the correct level

* feat: 🎸 Update DownloadModal to use new modal provider (#1119)

* feat: 🎸 Update AboutModal to use new modal provider

This feature updates the AboutModal component and Header to use the new
modal context (modal provider)

* feat(ModalProvider): Add title prop to provider

* feat(DownloadModal): Refactor to use new modal provider

* Update modal provider props

* Use translation for DownloadDialog

* CR Update: Use useTranslation hook

* CR Update: Extract downloadDialog and rename to downloadContent

* CR Update: Fix typo

* CR Update: Rename download content to viewport download form
2019-11-05 23:07:03 -05:00
ohif-bot 9827d1bf84 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.32
 - @ohif/core@1.7.0
 - @ohif/viewer@2.5.0
2019-11-05 22:00:36 +00:00
ladeirarodolfo e208f2e6a9 feat: 🎸 Filter by url query param for seriesInstnaceUID (#1117)
* feat: 🎸 Filter by url query param for seriesInstnaceUID

* fix: Set SR viewport as active by interaction (#1118)

* fix: Set SR viewport as active by interaction

* quick fix

* (eslint) add "before" as global variables

* add data-cy

* add data-cy

* create E2E test

* (E2E) create custom command to set layout size

* remove .only e2e

* remove throttle for onScroll

* feat: 🎸 Code review in progress

Code review. Move retrieveMEtadata load to separate folders. Some minor
code clean up

* feat: 🎸 Code review. Missing changes from previous commit

* feat: 🎸 Code review missing changes from previous commit

When sorting, Criteria for instance must use instanceNumber and not
instancesNumber

* feat: 🎸 Code review. Add more jsdoc info

* feat: 🎸 Code review. Prettify changed code

* feat: 🎸 Code review

Changed case for seriesInstanceUID. Use qido to filter (async). Fallback
to no params and same api method(async)
2019-11-05 16:48:44 -05:00
ohif-bot 9c1a3c3183 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.31
 - @ohif/core@1.6.2
 - @ohif/ui@0.59.1
 - @ohif/viewer@2.4.1
2019-11-05 20:01:36 +00:00
Emanuel Fiuza de Oliveira ecaf578f92 fix: #1075 Returning to the Study List before all series have finishe… (#1090)
* fix: #1075 Returning to the Study List before all series have finished loading throws multiple errors in console

* fix: 🐛 Addressing review feedback from issue #1075

Adding support for cancelable pattern

Closes: #1075

* fix: 🐛 update unit test for exports

Closes: #1075
2019-11-05 16:47:54 -03:00
ohif-bot d26adf92bd chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.30
 - @ohif/ui@0.59.0
 - @ohif/viewer@2.4.0
2019-11-04 16:06:44 +00:00
Igor Octaviano 5ee832b195 feat: 🎸 New modal provider (#1110)
* feat: 🎸 New modal provider

A new modal provider/context component to unify modal related code and
simplify modal use

Closes: #1086

* refactor(ModalContext): Keep current modal lib

* Update modal provider version according to PR #1116

* Update modal provider props to use inner props

* Add custom class prop

* CR Update: Refactor provider to extract specific modal

* Fix modal import

* CR Update: Move from spread to object assign

* CR Update: Add proptypes, use classnames dependency over interpolation and rename modal
2019-11-04 10:55:05 -05:00
ohif-bot 8baba22a2e chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.29
 - @ohif/viewer@2.3.8
2019-11-04 09:55:56 +00:00
James Petts 1d86b1a102 fix: 🐛 Upgrade react-vtkjs-viewport to fix volume orientation (#1143) 2019-11-04 10:45:53 +01:00
ohif-bot 25e6817248 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.28
 - @ohif/ui@0.58.5
 - @ohif/viewer@2.3.7
2019-11-04 04:07:09 +00:00
ladeirarodolfo 681384b742 fix: 🐛 Minor issues measurement panel related to description (#1142) 2019-11-03 22:53:24 -05:00
ohif-bot 54e07c281f chore(release): publish [skip ci]
- @ohif/viewer@2.3.6
2019-11-02 11:39:36 +00:00
Rodrigo Antinarelli 9dedccc7f9 test: set right viewport when layout is decreased (#1146) 2019-11-02 07:26:24 -04:00
ohif-bot 621e63b3e1 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.27
 - @ohif/core@1.6.1
 - @ohif/ui@0.58.4
 - @ohif/viewer@2.3.5
2019-10-31 18:51:34 +00:00
Danny Brown 64cf3b324d fix: application crash if patientName is an object (#1138)
* fix: get adapter store picker to show

* fix: error caused by DICOMWeb "empty object" for patientName

* formatting

* refactor: kill the pills

* Indicate modalities is missing if not provided

* Consistent modality display

* Make sure modalities styles are applied

* revert config
2019-10-31 14:41:08 -04:00
ohif-bot b7f315ca47 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.26
 - @ohif/ui@0.58.3
 - @ohif/viewer@2.3.4
2019-10-30 20:03:51 +00:00
ladeirarodolfo caaa032c4b fix: 🐛 Fix ghost shadow on thumb (#1113)
* fix: 🐛 Fix ghost shadow on thumb

Updated react-dnd package for the lates. Refactored thumbnail component

Closes: 1087

* feat: 🎸 Code review

Set flex instead of float. Add cypress tag to thumb container

* feat: 🎸 Code review. Revised style when no description
2019-10-30 15:53:58 -04:00
ohif-bot 22127a8f59 chore(release): publish [skip ci]
- @ohif/viewer@2.3.3
2019-10-30 15:25:05 +00:00
Danny Brown 50ca2bde97 fix: get adapter store picker to show (#1134) 2019-10-30 11:15:51 -04:00
ohif-bot c73cfbc110 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.25
 - @ohif/ui@0.58.2
 - @ohif/viewer@2.3.2
2019-10-29 20:47:05 +00:00
Igor Octaviano 5716b71d40 fix: 🐛 Limit image download size to avoid browser issues (#1112)
* fix: 🐛 Limit image download size to avoid browser issues

This fix adjusts the max texture size to allow browser compatibility
(blob size limit)

Closes: #1099

* CR Updates: Clamp values using math.min, remove unnecessary onBlur event and small adjustments
2019-10-29 16:36:34 -04:00
ohif-bot a3028ad1f6 chore(release): publish [skip ci]
- @ohif/viewer@2.3.1
2019-10-29 20:17:36 +00:00
Danny Brown 352407c71a fix: rollbar template needs PUBLIC_URL defined (#1127) 2019-10-29 16:04:53 -04:00
ohif-bot 1b5a5a2963 chore(release): publish [skip ci]
- @ohif/viewer@2.3.0
2019-10-29 19:02:49 +00:00
Danny Brown cf5136899e feat: service worker (#1045)
* init

* use workbox-window

* Try to accomodate SW hosted at non root url

* v5 updated namespaces

* Test w/ no prompt

* add icons to manifest

* remove scope for now

* add icon tile information to entrypoint

* See if we can skirt by without a duplicate install

* shift client claims to skip waiting message

* Remove prepended slash

* comment out cache handler for now

* Simplify service-worker to skip waiting and install when a new worker is detected

* Use generated icons

* Update rollbar config
2019-10-29 14:51:40 -04:00
ohif-bot 8c4e96bb3d chore(release): publish [skip ci]
- @ohif/extension-dicom-html@1.0.1
 - @ohif/viewer@2.2.2
2019-10-29 18:30:34 +00:00
Rodrigo Antinarelli 5b334175c3 fix: Set SR viewport as active by interaction (#1118)
* fix: Set SR viewport as active by interaction

* quick fix

* (eslint) add "before" as global variables

* add data-cy

* add data-cy

* create E2E test

* (E2E) create custom command to set layout size

* remove .only e2e

* remove throttle for onScroll
2019-10-29 14:21:56 -04:00
ohif-bot 20bfc829f7 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.24
 - @ohif/ui@0.58.1
 - @ohif/viewer@2.2.1
2019-10-29 11:13:46 +00:00
米老朱 0d498ba17d fix(StudyList): camel case colSpan (#1123) 2019-10-29 12:05:36 +01:00
ohif-bot a0180a9f0c chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.23
 - @ohif/ui@0.58.0
 - @ohif/viewer@2.2.0
2019-10-28 20:49:00 +00:00
Danny BrownandRodrigo Antinarelli 2cdef4b984 feat: responsive study list (#1068)
* feat: 🎸 Load spinner when selecting gcloud store. Add key on td

* feat: 🎸 Code review. Translate loading study status

* feat: 🎸 Code review. Minor code refactoring

* fix: add missing study and index declerations

* Newer version of react-dates (calendar input component)

* Add useMedia hook

* Add useDebounce hook

* Export hooks for consumption

* Pull search filter into own component

* StudyListWithData to StudyListRoute

* Rename pagination component

* Rename Toolbar

* CustomeDateRangePicker to plain ol' css

* Remove unused styles

* Misc

* Magic to make our "OR" filters work

* Play w/ Mobile styles

* Fix pagination pushing out max width for viewport

* Tidy up small and medium templates

* Remove unecessary nesting

* Better mobile header and logo styles

* Fix studyList font size

* Remove unused code

* hookup on click

* Fix table search selectors

* Cypress uses medium width

* Remove irrelevant test

* Fix primary sort

* Fix sort for our smaller displays

* fix study list sometimes longer than specified rowsPerPage

* Simplify date picker styles

* restore study date filters

* Fix about table styling

* fix take value for search

* Fix header sizing for viewer and list route

* Remove dead code

* Restore isLoading

* Update platform/ui/src/components/studyList/StudyList.styl

Co-Authored-By: Rodrigo Antinarelli <rodrigoantinarelli@gmail.com>

* Attempt first usage of translations w/ hooks

* Pull from react-i18n library

* Fit presets on to a single line

* add no results message

* Show error message in studyList table

* Update input selectors for e2e tests

* Make sure messages span all columns

* fix test selector for table results

* Skip hover for result rows

* fix imports

* Date filtering w/ config

* Default export

* Pushed to PageComponent

* Remove DropZone

This previously only worked for null/undefined studies. I'm unable to trigger the state by testing the data. This "might" have happened when using a fresh/empty PACS; but even an empty array of studies would not show
2019-10-28 14:47:57 -04:00
Danny Brown 70532a6cc6 test: percy snapshot workaround (#1111)
* Try workaround for percy snapshot

* Return our modified document

* test: some dark magic to make it possible to take screenshots of WebGL canvases

* Apply transformation directly to test DOM

* investigate: window:before:load

* Register flag in correct place; add experimental-webgl check

* Remaining canvas fixes

* Remove comment
2019-10-28 12:43:37 -04:00
ohif-bot f0519920fa chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.22
 - @ohif/ui@0.57.1
 - @ohif/viewer@2.1.4
2019-10-28 03:19:19 +00:00
Danny Brown 0d21cc6ad0 fix: MIP styling (#1109)
* Try to restore previous select component as new component

* fix issue where background is gray in download panel
2019-10-27 23:10:20 -04:00
ohif-bot dc502190ed chore(release): publish [skip ci]
- @ohif/viewer@2.1.3
2019-10-26 11:18:29 +00:00
Mirna Silva 7d009ef1d9 test: hotkeys in Cornerstone (#1097) 2019-10-26 07:12:03 -04:00
ohif-bot 777cca7471 chore(release): publish [skip ci]
- @ohif/viewer@2.1.2
2019-10-26 11:06:53 +00:00
Danny Brown e4bbbfbbe4 Merge pull request #1103 from OHIF/dannyrb/fix/script-tag-template-changes
fix: update script-tag output to include config from default.js
2019-10-26 06:58:42 -04:00
dannyrb 10059adc57 make sure copy webpack plugin is available 2019-10-26 06:53:44 -04:00
dannyrb c522ff3dda fix: update script-tag output to include config from default.js
instead of hardcoded config
2019-10-26 06:48:06 -04:00
ohif-bot 980516fcfc chore(release): publish [skip ci]
- @ohif/viewer@2.1.1
2019-10-26 04:06:48 +00:00
Emanuel Fiuza de Oliveira 2677170d67 fix: 🐛 JSON launch not working properly (#1089) (#1093)
* fix: 🐛 JSON launch not working properly (#1089)

JSON launch not working properly after QIDO/WADO switch #1089

Closes: #1089

* fix: 🐛 addressing review feedback (#1089)

Closes: #1089
2019-10-26 00:00:34 -04:00
ohif-bot 3452f1b91e chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.4.0
 - @ohif/extension-vtk@0.52.21
 - @ohif/core@1.6.0
 - @ohif/ui@0.57.0
 - @ohif/viewer@2.1.0
2019-10-26 02:53:05 +00:00
Romulo Bordezani 450e0981a5 feat: Snapshot Download Tool (#840)
* feat(Download ScreenShot Dialog)

- UI part of it

* feat(Download ScreenShot Dialog)

- UI part of it

* feat(Download ScreenShot Dialog)

- Missing Annotations showing controls

* feat(Download ScreenShot Dialog)

- Missing Annotations showing controls

* feat(Download ScreenShot Dialog)

- Partial Pull Request - Not ready to Prod yet.

* feat(Download ScreenShot Dialog)

  - UI - v2

* feat(Download ScreenShot Dialog)

  - UI - v3

* feat(Download ScreenShot Dialog)

  - Resizing tool

* feat(Download ScreenShot Dialog)

  - Resizing tool

* feat(Download ScreenShot Dialog)

  - Removing hardcoded CSS left

* feat(Download ScreenShot Dialog)

  - Bugfixes

* feat(Download ScreenShot Dialog)

  - Bugfixes - Escape closes the modal

* feat(Download ScreenShot Dialog)

  - Bugfixes - Responsive issues

* WIP: Refactoring DownloadDialog

* WIP: Refactoring DownloadDialog

* Align label and hide background canvas

* Add constantes and remove code not used

* Reset state on close modal

* Extract cornerstone to connect

* Fix modal zindex and add custom canvas classname

* CR Update: Add proptypes and fix useeffect warning

* CR Update: Refactor hooks, styles and imports

* CR Update: Remove important from css

* CR Update: Update core utils test of index
2019-10-25 22:46:32 -04:00
ohif-bot 8cf1e54490 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.20
 - @ohif/ui@0.56.1
 - @ohif/viewer@2.0.0
2019-10-26 02:25:52 +00:00
ladeirarodolfo 85f836cd91 fix: 🐛 Desc of meas.table not being updated on properly (#1094)
Is not possible to add Measurement Description if Relabel was not
previously selected

BREAKING CHANGE: 1013

Closes: #1013
2019-10-25 22:19:16 -04:00
Mirna Silva f5560c11b0 test: Stack Scroll, Right-click to zoom, Middle-click to Pan (#1073)
* E2E tests: Stack Scroll, Right-click to zoom, Middle-click to Pan

* About modal tests and added some cypress selectors

* Checking app version number

* Removed accidental import

* Review fixes
2019-10-25 22:14:57 -04:00
ohif-bot bef50f9298 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.19
 - @ohif/core@1.5.2
 - @ohif/viewer@1.12.2
2019-10-25 18:32:05 +00:00
Rodrigo Antinarelli 986b7ae2bf fix: set SR in ActiveViewport by clicking thumb (#1091) 2019-10-25 14:22:44 -04:00
ohif-bot 86c479ac04 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.18
 - @ohif/core@1.5.1
 - @ohif/viewer@1.12.1
2019-10-25 17:51:36 +00:00
James Petts 460e375f0a fix: 🐛 Orthographic MPR fix (#1092)
* fix: 🐛 Orthographic MPR fix
2019-10-25 19:43:34 +02:00
ohif-bot 78344c2c66 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.17
 - @ohif/core@1.5.0
 - @ohif/viewer@1.12.0
2019-10-25 13:17:26 +00:00
James Petts b451ce4407 fix: 🐛 Fix crosshairs location when using MIP (#1080) 2019-10-25 15:07:35 +02:00
ladeirarodolfo 21b586b08f feat: 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List (#1069) 2019-10-25 14:49:48 +02:00
ohif-bot 3bbeb1c521 chore(release): publish [skip ci]
- @ohif/viewer@1.11.5
2019-10-24 20:23:16 +00:00
Danny Brown 7668b69f20 test: percy snapshot workaround (#1085)
* Try workaround for percy snapshot

* Return our modified document
2019-10-24 16:09:59 -04:00
Danny Brown 3a12138134 test: Try workaround for percy snapshot (#1084) 2019-10-24 15:54:06 -04:00
Danny Brown 97786b808c ci: revert flags 2019-10-23 12:55:39 -04:00
Danny Brown cd1baa1b36 ci: force flag 2019-10-23 12:41:44 -04:00
Danny Brown 3ae3bee452 ci: test recursive removal 2019-10-23 12:24:56 -04:00
ohif-bot fd017c4512 chore(release): publish [skip ci]
- @ohif/viewer@1.11.4
2019-10-23 15:37:56 +00:00
Danny Brown c680720ce5 fix: Revert "Revert "fix: MPR initialization"" (#1065)
* Revert "Revert "fix: MPR initialization (#1062)" (#1064)"

This reverts commit a6c1e6c717.

* fix: MPR Initialization

* fix e2e openStudy

* add isRequired for numRows and numColumns
2019-10-23 11:27:22 -04:00
ohif-bot dfc4e89a91 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.16
 - @ohif/viewer@1.11.3
2019-10-23 10:44:45 +00:00
James Petts 13d337aaab fix: 🐛 Switch to orhtographic view for 2D MPR (#1074) 2019-10-23 12:36:41 +02:00
ohif-bot 4f26940005 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.15
 - @ohif/viewer@1.11.2
2019-10-23 07:56:28 +00:00
James Petts 9d08e81aa5 perf: ⚡️ Throttle UI refresh of synced WL value in vtk port (#1070) 2019-10-23 09:45:26 +02:00
ohif-bot 1a539d04c2 chore(release): publish [skip ci]
- @ohif/viewer@1.11.1
2019-10-23 01:12:25 +00:00
Rodrigo Antinarelli 5cc78c867f test: E2E Intermittent Failures (#1071) 2019-10-22 21:04:07 -04:00
ohif-bot cefa480979 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.14
 - @ohif/ui@0.56.0
 - @ohif/viewer@1.11.0
2019-10-22 20:24:10 +00:00
Danny Brown 66537bf1b9 ci: specify our custom executor
fixes broken build
2019-10-22 16:15:09 -04:00
Danny Brown 862d426c71 ci: first attempt at adding a gated docker PR Publish (#1038) (#1041)
* ci: first attempt at adding a gated docker PR Publish (#1038)

* ci: first attempt at adding a gated docker PR Publish

* See if we can use contexts to inject variables after approval

* fix build command

* try alternative context

* docs: pull request template that spells out PR checklist

* change context back to Docker Hub

* Switch optional approval to another workflow

* Make docker builds faster

* Update .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
2019-10-22 15:53:44 -04:00
ladeirarodolfo e62f403fe9 feat: 🎸 Load spinner when selecting gcloud store. Add key on td (#1034)
* feat: 🎸 Load spinner when selecting gcloud store. Add key on td

* feat: 🎸 Code review. Translate loading study status

* feat: 🎸 Code review. Minor code refactoring

* fix: add missing study and index declerations

* test: Improviment on VTK beforeEach hook (#1057)

* feat: 🎸 Some improvements on table cell and react lifecycle

* feat: 🎸 Prevent component update on a not searchData changing

* feat: 🎸 Some cypress improv to pass ci

* feat: 🎸 Code reviewe and also fixing list for gcloud

* feat: 🎸 Code review. Add spinner on first load of store

* feat: 🎸 Code review. Centering loading list spinner icon
2019-10-22 14:40:23 -04:00
Mirna Silva dea0ceab57 test: Adding Percy Snapshot on VTK, PDF and Microscopy tests (#1059)
* Adding Percy Snapshot on VTK, PDF and Microscopy tests

* Small improvement to always click on first search result

* Wait for search result

* Added timouts all over the code to avoid intermitted failures in CI

* Increasing Default Timeout and other configs

* Cleaning up the code

* try force enabling gpu

* Try custom executor with newer version of chrome

* Fix based on review comments

* Increased minimum thumbnails expected on VTK test

* Fix config validation

* Make sure we're using the chrome browser
2019-10-21 14:30:50 -04:00
Danny Brown a6c1e6c717 Revert "fix: MPR initialization (#1062)" (#1064)
This reverts commit b03739428f.
2019-10-19 17:00:51 -04:00
Rodrigo Antinarelli b03739428f fix: MPR initialization (#1062)
* fix: MPR initialization

* small refactoring
2019-10-18 14:29:00 -04:00
ohif-bot 2a52d9f05d chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.13
 - @ohif/viewer@1.10.3
2019-10-18 14:02:07 +00:00
James Petts d5e21bb5bb * chore: 🤖 Update react-vtkjs-viewport and refactor vtk extension
* chore: 🤖 Update react-vtkjs-viewport and refactor vtk extension
2019-10-18 15:53:43 +02:00
ohif-bot 04ffe3ec76 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.12
 - @ohif/viewer@1.10.2
2019-10-18 10:16:06 +00:00
James Petts 9cdf8b690b * feat: 🎸 Progressive volume loading for vtk viewport (#1055)
* feat: 🎸 Progressive volume loading for vtk viewport

Adds progressive volume loading for vtkjs viewport whilst the frames are
streamed from PACs and rebuilt in the volume.

Closes: closes #1051

* Update to react-vtkjs-viewport 0.3.0
2019-10-18 11:43:23 +02:00
Mirna Silva 8d49b31d03 test: Improviment on VTK beforeEach hook (#1057) 2019-10-16 15:59:50 -04:00
Danny Brown e4cfae7ad9 docs: add percy badge to readme 2019-10-16 11:51:45 -04:00
ohif-bot e53041f630 chore(release): publish [skip ci]
- @ohif/viewer@1.10.1
2019-10-16 15:46:46 +00:00
Mirna Silva 35c54eac56 test: Test cases for extensions: VTK, HTML and PDF (#1036)
* Test cases for extensions: VTK, HTML and PDF

* Test Cases: Delete measurement, Relabel and Description through Contextmenu

* Fix: PDF, HTML and VTK extensions

* CI: running into different folders for PWA and Script-tag tests

* CI: running into different folders for PWA and Script-tag tests

* CI: running into different folders for PWA and Script-tag tests

* Fix for VTK tests and Cornerstone Eraser tool

* Removed VTK alias from Cornerstone aliases initialization

* Temporary fix for Eraser tool

* Test workaround

* Use juno for vtk

* Shift VTK tests to PWA folder
2019-10-16 11:39:41 -04:00
ohif-bot e7be573328 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.11
 - @ohif/i18n@0.51.0
 - @ohif/ui@0.55.0
 - @ohif/viewer@1.10.0
2019-10-15 18:59:20 +00:00
Danny Brown 8a7706b286 test: percy visual testing (#1053)
* test: percy visual testing init

* Try PR checks with custom command

* Only run percy visual tests for PWA on merge for now
2019-10-15 14:52:23 -04:00
Rodrigo Antinarelli c217b8bbde feat: Add browser info and app version (#1046)
* feat: Add browser info and app version

* quick fix

* lock file

* add build_num

* add screenshot e2e test

* quick test build_num

* revert header

* include i18n texts
2019-10-15 13:09:14 -04:00
ohif-bot 74f3cbc4b2 chore(release): publish [skip ci]
- @ohif/viewer@1.9.1
2019-10-15 16:10:17 +00:00
James Petts d091cd604c fix: 🐛 Remove debugger statement left in from last PR (#1052) 2019-10-15 18:03:29 +02:00
ohif-bot 3a4280163d chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.10
 - @ohif/core@1.4.0
 - @ohif/viewer@1.9.0
2019-10-15 15:18:15 +00:00
James Petts 2d75e01ea0 feat: 🎸 Only allow reconstruction of datasets that make sense (#1010)
* feat: 🎸 Only allow reconstruction of datasets that make sense

Only allow reconstruction of datasets which are imaging data, that have
frames in the same orientation, with the same size and make sense to be
reconstructed in 3D.

Closes: #561
2019-10-15 17:08:53 +02:00
Danny Brown 5c5a49486d docs: more specific location for build output 2019-10-15 08:56:54 -04:00
ohif-bot a8a57132a7 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.9
 - @ohif/ui@0.54.0
 - @ohif/viewer@1.8.0
2019-10-14 15:46:13 +00:00
Rodrigo Antinarelli 92c8996ed4 feat: Notification Service (#1011)
* WIP - Notification Service

* add snackbar provider into the main App component

* feat: Notification Service

* revert unmodified file

* Update DicomHtmlViewport.js

* update css classnames

* revert toolbarRow

* fix revert toolbarRow

* styles adjustments and hide method

* remove unecessary default options

* minor fixes

* fix long truncated messages

* move SnackbarContext to ui project, rename, export
2019-10-14 11:39:31 -04:00
ohif-bot f07dca5fa4 chore(release): publish [skip ci]
- @ohif/viewer@1.7.0
2019-10-14 12:50:19 +00:00
Karthik Rao c99e0d8a23 feat: Implement a 'Exit 2D MPR' button in the toolbar
* feat: Implement a 'Exit 2D MPR' button in the toolbar to act as a toggle when '2D MPR' button is clicked

* Move logic to exit MPR and toggling into ConnectedPluginSwitch and PluginSwitch. Also, discard the ConnectedExitPluginSwitch and ExitPluginSwitch components.

* Update ToolbarRow.js
2019-10-14 08:41:29 -04:00
ohif-bot 84144bc3ef chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.8
 - @ohif/core@1.3.2
 - @ohif/viewer@1.6.3
2019-10-14 11:15:08 +00:00
Erik Ziegler 5cbdd0c616 ci: Skip copying URL to clipboard on headless CI servers (#1043) 2019-10-14 13:08:59 +02:00
Chris McGee fc015326c7 fix: Return display sets in StudyMetadata._createDisplaySetsForSeries (#1042) 2019-10-13 20:53:01 -04:00
ohif-bot 01860b6094 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.7
 - @ohif/core@1.3.1
 - @ohif/ui@0.53.4
 - @ohif/viewer@1.6.2
2019-10-11 18:56:39 +00:00
Danny Brown cfdf827ff0 ci: fix broken build (#1033) 2019-10-11 14:50:12 -04:00
Danny Brown ceb6837153 ci: speed up script tag e2e tests (#1032)
* show package bundle progress

* Ability to run "quick builds" for faster feedback on status checks (e2e)

* ci: speed up PR checks

* specify quick build value
2019-10-11 14:26:24 -04:00
Danny Brown b01b0108b0 refactor: layout manager to viewer (#1026)
* docs: formatting

* Prefer numRows and numColumns to percentage width/height for layout

* fix: LayoutManager --> ViewportGrid (get out of UI component library)

* docs: remove outdated docs

* Don't expose ExampleDropTarget

* Revert thumnail entry drag source removal

* Update screaming tests

* fix drag-n-drop

* fix vtk mpr2d CC: @jamesapetts

* remove setSingleLayoutData

* remove vtk qualifier
2019-10-11 12:03:59 -04:00
Erik Ziegler d6862e7418 ci: Set cache key to only use yarn.lock checksum (#1031)
* ci: Set cache key to only use yarn.lock checksum

* Use increasingly general pattern for restoring the cache

* Avoid running script tag e2e tests twice

* Performance improvements for CI

* Valid YAML

* Valid YAML?

* Valid YAML!!!!

* Update Node image, update yarn version

* circle

* serve
2019-10-11 11:40:21 -04:00
ohif-bot beb7083e55 chore(release): publish [skip ci]
- @ohif/viewer@1.6.1
2019-10-11 13:13:57 +00:00
Erik Ziegler 412fe4e23f fix: Switch token storage back to localStorage because in-memory was annoying for end users (#1030) 2019-10-11 14:53:49 +02:00
ohif-bot 659649e10b chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.6
 - @ohif/core@1.3.0
 - @ohif/viewer@1.6.0
2019-10-11 09:32:51 +00:00
Erik Ziegler f431b277dd ci: Disable docker layer caching because CircleCI doesn't let open source plans use it (#1028) 2019-10-11 11:14:54 +02:00
ladeirarodolfo 2bc361cdca feat: 🎸 Improve usability of Google Cloud adapter, including direct routes to studies (#989) 2019-10-11 09:59:38 +02:00
Erik Ziegler c6f306c0de Revert "ci: Disable docker layer caching because CircleCI doesn't let open source plans use it"
This reverts commit c0ea27f6b3.
2019-10-11 08:52:07 +02:00
Erik Ziegler c0ea27f6b3 ci: Disable docker layer caching because CircleCI doesn't let open source plans use it 2019-10-11 08:45:02 +02:00
ohif-bot df8d4029f1 chore(release): publish [skip ci]
- @ohif/viewer@1.5.4
2019-10-10 20:07:32 +00:00
Mirna Silva a36cb63926 tests: Test cases implemented: Cornerstone tools Clear and Eraser; Measurement panel Relabel and Description (#1014)
* Fix for test scripts that were failing in CI

* Test cases for tools: Length and Angle

* Test cases implemented: Scroll series, Levels, Pan, Reset, Zoom

* Added test cases for tools: Cine, More, Layout, Invert, Rotate, FlipH, FlipV

* Fixes made according to comments on PR review

* Fixes for failing scenarios: Layout and Cine

* Test cases implemented: Cornerstone tools Clear and Eraser; Measurement panel Relabel and Description

* Fix for CI failure
2019-10-10 15:46:41 -04:00
ohif-bot e51529e6c9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.5
 - @ohif/viewer@1.5.3
2019-10-10 16:57:21 +00:00
James Petts 750a150644 Merge pull request #1025 from JamesAPetts/vtkjsViewportPerformance
perf: ⚡️ Update react-vtkjs-viewport for performance improvement
2019-10-10 18:37:34 +02:00
James A. Petts a9d28d85da Merge remote-tracking branch 'upstream/master' into vtkjsViewportPerformance 2019-10-10 18:02:08 +02:00
James A. Petts 6ee5094cbe perf: ⚡️ Update react-vtkjs-viewport for performance improvement 2019-10-10 17:57:53 +02:00
ohif-bot 6bfb9bb140 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.4
 - @ohif/ui@0.53.3
 - @ohif/viewer@1.5.2
2019-10-10 02:41:15 +00:00
Danny Brown db5b760432 ci: skip minimize flag (#1023)
* Better css chunk filenames depending on context

* skip minimize flag to speed up builds that are timing out (short-term fix)

* make sure our custom TerserJS config is actually applied

* Use SKIP_MINIMIZE flag for netlify and demo builds
2019-10-09 22:18:06 -04:00
Igor Octaviano e7de8be2d8 fix: 🎸 switch ohif logo from text + font to SVG (#1021)
currently we are using font + text, not it was changed to use svg
instead

Closes: 723
2019-10-09 22:15:53 -04:00
ohif-bot cb2088daf7 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.3.1
 - @ohif/extension-vtk@0.52.3
 - @ohif/ui@0.53.2
 - @ohif/viewer@1.5.1
2019-10-09 19:55:40 +00:00
Igor Octaviano 2a743554b6 fix: 🐛 set current viewport as active when switching layouts (#1018)
* fix: 🐛 set current viewport as active when switching layouts

check if current viewport index is less than the current layout lenght
and set 0 if so or keep current selected cell index as active viewport

Closes: 999

* chore: 🤖 commenting out docker layer caching in the short-term

commenting out docker layer caching in the short-term

* refactor: 💡 refactor layout matrix index conditional
2019-10-09 15:37:26 -04:00
Igor Octaviano a28984ef1f fix: 🐛 set tools bidirectional, eraser and annotate command (#1020)
annotate, bidirectional and eraser had types as command but they should
be typed as setToolActive

Closes: 981
2019-10-09 15:30:00 -04:00
Rodrigo Antinarelli 95e5e11d09 test: E2E Tests - Thumbnail loading (#1017)
* fix: E2E Tests - Thumbnail loading

* rename data-cy attr
2019-10-09 12:57:24 -04:00
ohif-bot ff9f199850 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.3.0
 - @ohif/viewer@1.5.0
2019-10-09 14:45:40 +00:00
Danny Brown 1cc94f36a7 feat: Multiple fixes and implementation changes to react-cornerstone-viewport
* Update props for new react-cornerstone-viewport implementation

* Create an preRegistration hook for the cornerstone extension to setup cornerstone tools (takes configuration)

* Isolate measurements panel logic

* reorder extension registration

* remove unused setupTools

* Restore CINE connection

* fix stack prefetcher toggling

* Cleanup OHIFCornerstoneViewport props

* updated yarn lock

* bust yarn.lock to get updated react-cornerstone-viewport

* fix review comment; us isActive to better show it's influence on prefetch

* refactor: remove pass through method

* review performance optimization

* review; comment out unused variable

* shift tool under correct comment

* Use alternative csTools config, if provided

* Note regarding config options
2019-10-09 10:23:14 -04:00
ohif-bot 5c5223fcf1 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.2
 - @ohif/core@1.2.0
 - @ohif/viewer@1.4.5
2019-10-09 11:27:27 +00:00
James Petts 8513ad9ef3 perf: 🎸 Update vtk viewport for faster image reconstruction (#1016) 2019-10-09 13:13:13 +02:00
Emanuel Fiuza de Oliveira 66d8bc6d6f fix: OHIF-1002 Study lazy load should be true by default (#1004) 2019-10-07 16:31:47 -04:00
Jody Zeitler 0064a4b1cd feat: Allow a server requestOptions.auth to be a function that returns the Authorization header. (#928) 2019-10-06 21:07:44 -04:00
ohif-bot 74ce89b22b chore(release): publish [skip ci]
- @ohif/viewer@1.4.4
2019-10-07 01:07:06 +00:00
Mirna Silva c92975f555 test: Fix for test scripts that were failing in CI (#968)
* Fix for test scripts that were failing in CI
* Test cases for tools: Length and Angle
* Test cases implemented: Scroll series, Levels, Pan, Reset, Zoom
* Added test cases for tools: Cine, More, Layout, Invert, Rotate, FlipH, FlipV
* Fixes made according to comments on PR review
* Fixes for failing scenarios: Layout and Cine
2019-10-06 20:57:49 -04:00
ohif-bot ce6c0533a0 chore(release): publish [skip ci]
- @ohif/i18n@0.50.5
 - @ohif/viewer@1.4.3
2019-10-04 16:55:28 +00:00
Rodrigo Antinarelli 4df624b8c4 fix: CineDialog buttons label (#998)
* fix: CineDialog buttons label

* Revert "fix: CineDialog buttons label"

This reverts commit 48ea7a28e50b845c4939a8aaf2d95eba49d2c102.

* fix: CineDialog buttons label
2019-10-04 12:46:39 -04:00
ohif-bot e064fd353e chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.1
 - @ohif/ui@0.53.1
 - @ohif/viewer@1.4.2
2019-10-04 13:37:03 +00:00
Sanders DeNardi 458d310c78 fix: Move Series Information to Separate Row (#990)
* fix ThumbnailEntry alignment

* move series information to separate row
2019-10-04 09:26:05 -04:00
ohif-bot 73bc4d9094 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.0
 - @ohif/viewer@1.4.1
2019-10-03 14:05:24 +00:00
James Petts 7709ebb107 Merge pull request #993 from JamesAPetts/vtkViewportSubscriptionManager
feat: 🎸 Synced Window Leveling
2019-10-03 15:49:19 +02:00
James A. Petts 559b637290 feat: 🎸 Synced Window Leveling
Syncs window leveling for vtkjs viewports in MPR 2D mode.

Closes: #558
2019-10-03 15:29:56 +02:00
ohif-bot 36910f2b47 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.3
 - @ohif/core@1.1.0
 - @ohif/ui@0.53.0
 - @ohif/viewer@1.4.0
2019-10-03 08:02:00 +00:00
Emanuel Fiuza de Oliveira 9e10c2b2de feat: Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once (#953) 2019-10-03 09:51:29 +02:00
ohif-bot c5cbd77cb9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.2
 - @ohif/core@1.0.2
 - @ohif/viewer@1.3.3
2019-10-02 07:49:25 +00:00
Erik Ziegler b3b4c10e4c fix: Temporarily sort SEG files to the end of the display set list as a workaround for several metadata issues (#987) 2019-10-02 09:39:38 +02:00
ohif-bot 855020d730 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.1
 - @ohif/viewer@1.3.2
2019-10-01 20:36:02 +00:00
Zaid Safadi 8503cbce4e fix(rotate): Use new MPR rotate in react-vtkjs-viewport 0.1.2 (#964) 2019-10-01 22:21:31 +02:00
ohif-bot c8667fb2d6 chore(release): publish [skip ci]
- @ohif/viewer@1.3.1
2019-10-01 10:07:08 +00:00
Erik Ziegler 674ca9f96f fix: Exit MPR mode if Layout is changed (#984) 2019-10-01 11:55:58 +02:00
ohif-bot 7a4b97b4e8 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.0
 - @ohif/ui@0.52.0
 - @ohif/viewer@1.3.0
2019-10-01 08:16:51 +00:00
ladeirarodolfo c79c0c301d feat: 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle (#947) 2019-10-01 10:06:56 +02:00
ohif-bot ff5060f276 chore(release): publish [skip ci]
- @ohif/viewer@1.2.8
2019-10-01 07:42:58 +00:00
Erik Ziegler dafb26b34b chore: Fix dev server so that it can be accessed on LAN from other devices (#983) 2019-10-01 09:32:50 +02:00
ohif-bot c4f9d5e750 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.23
 - @ohif/ui@0.51.3
 - @ohif/viewer@1.2.7
2019-10-01 07:11:22 +00:00
Erik Ziegler cf40a8388c fix: Address issues with touch devices and drag/drop causing crashes (#982) 2019-10-01 09:00:55 +02:00
Erik Ziegler d596ab5c3d ci: Try to fix codecov threshold (#976) 2019-09-30 12:24:14 +02:00
Erik Ziegler 72198eee7d ci: Increase build timeout limit as a stopgap measure (#977) 2019-09-30 12:19:44 +02:00
Danny Brown 057f68a9b4 fix: yarn run start as alias for yarn run dev (#973) 2019-09-28 20:25:47 -04:00
Steve Pieper 034ca64d6b docs: Update node version in readme (#971)
Fixes #970
2019-09-28 19:10:20 -04:00
ohif-bot 4a0901aee9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.22
 - @ohif/core@1.0.1
 - @ohif/viewer@1.2.6
2019-09-27 15:42:12 +00:00
Bhavesh Pandey d0ec9cf0e0 fix: Check for Value in 00081155 sequence (Few patient protocol images doesn't have this value) and removed a duplicate declaration (#921) 2019-09-27 11:30:26 -04:00
ohif-bot 71bea0d4d8 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.2.5
 - @ohif/viewer@1.2.5
2019-09-27 13:18:31 +00:00
Danny Brown e607ed2aa0 fix: version bump issue (#963)
* Revert "fix: version bump issue (#962)"

This reverts commit c80ea17cc2.

* fix: stick
2019-09-27 09:04:29 -04:00
ohif-bot 4939f43509 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.0
 - @ohif/viewer@2.0.0
2019-09-27 12:54:25 +00:00
Danny Brown c80ea17cc2 fix: version bump issue (#962)
* fix: version bump issue

* fix: catch the cornerstone-bump as well
2019-09-27 08:43:40 -04:00
ohif-bot aa7dc1e2c1 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.0
 - @ohif/extension-dicom-html@1.0.0
 - @ohif/extension-dicom-pdf@1.0.0
 - @ohif/extension-vtk@0.50.21
 - @ohif/core@1.0.0
 - @ohif/viewer@2.0.0
2019-09-27 11:58:23 +00:00
ladeirarodolfo e7e1a8a6cd fix: 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases (#862)
* fix: 🐛 Local file: failing when retrieving segmentation data

Fix segmentation data retrieval issues for local file. Changed from
fecthing to use cornerstone loadAndCache method

BREAKING CHANGE: DICOM Seg

Closes: part of #838

* Switch SEG retrieval to WADO-RS

* Forgot a debugger

* refactor: 💡 Code refactor. Minor changes into methods

* fix: 🐛 Load local files: PDF

Items:
1. FileLoaderService: used for serveral operations on local files(load it,  get list of studies, group them, accepting dicom and pdf)
2. DicomLoaderService: used for loading dicom based on dataset and studies. Depending on type of dicom loader might change. WIP
3. Refactor PDF and handleSegmentationStorage to use DicomLoaderService

* fix: 🐛 Code review

* fix: 🐛 Code review. Changed:Folder organization and dicom file

Move fileLoaderService and others to a specific folder. When loading
dicom file change to only retrieve the file(not use cornerstone to cache
or anything else).

* fix: 🐛 Code review. Move dicomLoaderService to core

Moved dicomLoaderService to ohif/core and localFileLoaders to a specific
folder.

* fix: 🐛 Code review

Simplified method to get study for dicom file. Added error handling on
file loading. DicomLoaderService to be exposed on ohif/core/utils
instead.

* fix: 🐛 Reduce local load to one method only

Reduced local file load to one method only

* fix: 🐛 HTML to use dicomLoaderService. Prefer wadors than (uri)

* fix: 🐛 Code implementation for multiframe files

* fix: 🐛 Code review. Default local loader to dicom

Closes: 838

* fix: 🐛 Code review. Use relative path to require DICOMWeb

Closes: 838

* fix: 🐛 Code review. Fix unit test. Added DicomLoaderService mod

Closes: 838

* fix: 🐛 Code review. Add 'Seg' on left thumb

When getting/creating dataset get modality for file/image read

Closes: 838
2019-09-27 13:47:08 +02:00
ohif-bot e66c27affb chore(release): publish [skip ci]
- @ohif/viewer@1.2.3
2019-09-27 11:10:33 +00:00
Danny Brown ae26904885 Revert "* fix: 🐛 Enabling workbox to cache webfonts and fonts-stylesheets, add local fonts for UMD build (#906)" (#961)
This reverts commit 69b96478cb.
2019-09-27 06:54:47 -04:00
ohif-bot 8bb730318a chore(release): publish [skip ci]
- @ohif/viewer@1.2.2
2019-09-27 09:20:09 +00:00
Gustavo André Lelis 69b96478cb * fix: 🐛 Enabling workbox to cache webfonts and fonts-stylesheets, add local fonts for UMD build (#906)
* fix: 🐛 commonjs/umd fonts import

Importing font into index-umd.js and creating a simple font css file
with font-face

Closes: #724

* fix: 🐛 Enabling workbox to cache webfonts and fonts-stylesheets

Closes: #724

* fix: 🐛 Adding all type of fonts from google - Roboto

Closes: #724
2019-09-27 11:11:05 +02:00
ohif-bot 816666df7b chore(release): publish [skip ci]
- @ohif/viewer@1.2.1
2019-09-26 18:24:46 +00:00
Danny Brown e375a4ab11 fix: google cloud support w/ docker (via env var) (#958)
* fix: make sure google config is in output

* fix: update config sub to use new paths and file names
2019-09-26 14:08:44 -04:00
ohif-bot 7f4d71e412 chore(release): publish [skip ci]
- @ohif/viewer@1.2.0
2019-09-26 17:56:20 +00:00
ladeirarodolfo a90605c82d feat: 🎸 React custom component on toolbar button (#935)
* feat: 🎸 React custom component on toolbar button

Toolbar button can consume/render react custom components. Its only
required to expose the component into toolbar definitions
(toolbarModule)

Closes: 924

* feat: 🎸 Core review. Minor changes on documentation

Closes: 924

* feat: 🎸 Code review

* feat: 🎸 Code review. Changing function call to variable decl
2019-09-26 13:45:19 -04:00
ohif-bot 5b523c2afa chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.20
 - @ohif/ui@0.51.2
 - @ohif/viewer@1.1.14
2019-09-26 14:21:10 +00:00
Gustavo André Lelis 5551f817e3 fix: 🐛 Set series into active viewport by clicking on thumbnail (#945)
* fix: 🐛 Set series into active viewport by clicking on thumbnail

Users should be able to set the series for the active viewport by
clicking it's thumbnail into Study browser

Closes: #895

* fix: 🐛 Set series into active viewport by clicking on thumbnail

Small refactor into onClick data callback and thumbnailClick function

Closes: #895

* fix: 🐛 Refactor thumbnail click code changes

* fix: 🐛 We must clear the sopInstanceUid if we don't update it together with the viewportSpecificData, preventing console warning.

* fix: 🐛  Cleaning up a few more code changes
2019-09-26 10:08:30 -04:00
ohif-bot a9ca3fa026 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.2.2
 - @ohif/extension-dicom-html@0.50.8
 - @ohif/extension-dicom-microscopy@0.50.6
 - @ohif/extension-vtk@0.50.19
 - @ohif/viewer@1.1.13
2019-09-26 12:24:19 +00:00
Erik Ziegler 89380353d2 fix: Add some code splitting for PWA build (#937)
* fix: Add some code splitting for PWA build

* Fix missing files
2019-09-26 11:35:13 +02:00
ohif-bot edb86fad55 chore(release): publish [skip ci]
- @ohif/viewer@1.1.12
2019-09-26 03:56:47 +00:00
Danny Brown 395a1be3fb Merge pull request #952 from dannyrb/fix/docker-ci
ci: docker publish (first attempt)
2019-09-25 23:45:59 -04:00
dannyrb 65d852b9e4 enable ci 2019-09-25 23:39:42 -04:00
dannyrb e797a3a141 Add modified dockerfile from previous version 2019-09-25 23:16:00 -04:00
Danny Brown 246936f90b Merge pull request #949 from TZubiri/2dmpr-ui-fix
ui fix: Made mpr more accessible from toolbar.
2019-09-24 21:29:43 -04:00
Tomas Zubiri c57298475a UI fix: Collapsed 2dMPR button to main toolbar 2019-09-24 18:43:32 -03:00
Tomas Zubiri 6a5ebb3e25 initial attempt to simplify code and ui 2019-09-24 18:20:06 -03:00
ohif-bot 291b23aee1 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.18
 - @ohif/core@0.50.10
 - @ohif/viewer@1.1.11
2019-09-23 13:17:23 +00:00
Ivan Aksamentov 72427fe2ff fix: Avoid using variable name "module" (#942)
On some configurations variables named `module` may clash with the predefined JS global.
resolves #940
2019-09-23 15:05:26 +02:00
Erik Ziegler 882d04c9bf chore: add husky for automatic linting of staged files (#938) 2019-09-20 13:44:52 +02:00
Erik Ziegler d1a24bc9d9 ci: Update terser-webpack-plugin (#936) 2019-09-19 22:49:15 +02:00
ohif-bot 16a87fa75f chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.17
 - @ohif/ui@0.51.1
 - @ohif/viewer@1.1.10
2019-09-19 13:13:23 +00:00
Erik Ziegler 6fdac4d605 fix: Use HTML5Backend for drag-drop if the device does not support touch (#927)
* fix: Use HTML5Backend for drag-drop if the device does not support touch, since it's less buggy

* Forgot React when using jsx
2019-09-19 14:57:04 +02:00
ohif-bot 6192502fe0 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.16
 - @ohif/viewer@1.1.9
2019-09-19 12:50:18 +00:00
Erik Ziegler 2341b32638 fix: Set transfer function range to data range when loading volumes in VTK (#930) 2019-09-19 14:39:36 +02:00
ohif-bot 45aeaec916 chore(release): publish [skip ci]
- @ohif/viewer@1.1.8
2019-09-19 12:34:20 +00:00
Erik Ziegler 66b84ae9c2 ci: Use development mode for e2e tests for now until we have a better alternative (#934) 2019-09-19 14:20:06 +02:00
ohif-bot bc28fe26ac chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.15
 - @ohif/viewer@1.1.7
2019-09-19 12:13:07 +00:00
Erik Ziegler 7f642a05da fix: Stop adding empty labelmap to VTK Viewports for now. Wait until paint tools are implemented (#926) 2019-09-19 13:59:50 +02:00
Erik Ziegler 66a0aadcce fix: Bump VTK viewport dependency (#929) 2019-09-19 12:54:10 +02:00
ohif-bot 808af7ee96 chore(release): publish [skip ci]
- @ohif/viewer@1.1.6
2019-09-19 10:01:01 +00:00
Erik Ziegler 10b5e071f4 ci: Comment out an E2E test that is failing on CI due to timing issues 2019-09-19 11:40:36 +02:00
Erik Ziegler fe1969b071 ci: Add progress reporting to build step so CircleCI won't think the step has timed out 2019-09-19 11:40:36 +02:00
Danny Brown 16b2a8295f chore: stalebot misbehaving 2019-09-18 16:15:33 -04:00
ohif-bot d33bb4210d chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.2.1
 - @ohif/extension-vtk@0.50.14
 - @ohif/core@0.50.9
 - @ohif/viewer@1.1.5
2019-09-17 03:00:30 +00:00
Danny Brown ab681b9929 Merge pull request #920 from dannyrb/fix/bump-cornerstone-tools-version
fix: bump cornerstone-tools to latest version
2019-09-16 22:36:04 -04:00
dannyrb f52daca5e5 fix: force rebuild of yarn.lock 2019-09-16 22:19:18 -04:00
dannyrb 4afc88c0e5 fix: bump cornerstone-tools version in peerDeps 2019-09-16 22:14:18 -04:00
dannyrb f519f8609c fix: bump cornerstone-tools to latest version 2019-09-16 21:53:06 -04:00
ohif-bot e27d5b58a4 chore(release): publish [skip ci]
- @ohif/viewer@1.1.4
2019-09-17 01:39:08 +00:00
Mirna Silva a18b292a9d test: [TEST E2E] Drag and drop thumbnail into viewport (#904)
* TEST E2E: Drag and drop thumbnail into viewport
* TEST E2E: Drag and drop thumbnail into viewport
* fix: add pollyfill to script-tag template
* Add wait's to command
* Try to store artifacts
* ci: updated artifact location
* Changing Patient to MisterMR
* Refactors based on PR comments
* Adding new sub-folders to separate the testing scripts according to context/features
* Adding test cases for Side Panels Measurements and Series
* Removing duplicated file OHIFStudyViewer.spec.js
2019-09-16 21:26:14 -04:00
Danny Brown 7bd1507a89 chore: bad stale bot 2019-09-16 13:53:41 -04:00
Danny Brown a634177578 chore: bad stale bot 2019-09-16 12:53:05 -04:00
ohif-bot fe4d71ad0b chore(release): publish [skip ci]
- @ohif/viewer@1.1.3
2019-09-16 14:41:16 +00:00
ladeirarodolfo f723546e6b fix: 🐛 Fix issue on not loading gcloud (#919)
Fixed issue on not loading gcloud. Error on name variable declaration
2019-09-16 10:26:59 -04:00
Danny Brown 348b8949d4 docs: update code sandbox links to use sandboxes in our team (#915) 2019-09-13 14:54:15 -04:00
ohif-bot 26e037dee7 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.2.0
 - @ohif/viewer@1.1.2
2019-09-12 19:34:57 +00:00
Romulo Bordezani 26be96783d feat(Annotate): Add annotate tool back to toolbar
* feat(Arrow Annotate Tool)

* feat(Arrow Annotate Tool)

* refactor: label "Arrow Annotate" to "Annotate"

to match viewer-legacy
2019-09-12 15:22:09 -04:00
ohif-bot 029ba1fb4a chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.1.1
 - @ohif/extension-dicom-html@0.50.7
 - @ohif/extension-vtk@0.50.13
 - @ohif/viewer@1.1.1
2019-09-12 10:40:45 +00:00
James Petts 56d658f02d Merge pull request #913 from JamesAPetts/dcmjs0.6.1
chore: 🤖 Upgrade dcmjs to 0.6.1
2019-09-12 12:24:36 +02:00
James A. Petts 64921b905f Cosmetic change to trigger netlify rebuild. 2019-09-12 11:26:36 +02:00
James A. Petts d00d7daf41 chore: 🤖 Upgrade dcmjs to 0.6.1 2019-09-12 11:22:46 +02:00
ohif-bot 144a173306 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.1.0
 - @ohif/extension-vtk@0.50.12
 - @ohif/ui@0.51.0
 - @ohif/viewer@1.1.0
2019-09-12 03:07:49 +00:00
Romulo Bordezani e40cbae113 feat(BidirectionalTool): Add BidrectionalTool to "more" menu (#911)
* feat(Bidirectional Tool)

* feat(Bidirectional Tool)
2019-09-11 22:53:06 -04:00
Romulo Bordezani 698d274c64 feat(EraserTool): add eraserTool to @ohif/extension-cornerstone (#912)
This includes `ui` changes in the same commit, so it will see a feature bump as well
2019-09-11 16:28:14 -04:00
ladeirarodolfo c221dd86d2 feat: 🎸 Load local file or folder using native dialog (#870)
Created to specialized dropzone elements to take care of loading local
file/folder from dialog

Closes: 847
2019-09-10 23:30:53 -04:00
Danny Brown a8c3d1b9a2 docs: runtime extensions (#908)
* docs: runtime extensions

* docs: small readability changes

* docs: clarity
2019-09-10 22:50:13 -04:00
ohif-bot 6cce8e6720 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.0.1
 - @ohif/extension-dicom-html@0.50.6
 - @ohif/extension-dicom-microscopy@0.50.5
 - @ohif/extension-dicom-pdf@0.50.4
 - @ohif/extension-vtk@0.50.11
 - @ohif/i18n@0.50.4
 - @ohif/viewer@1.0.5
2019-09-10 20:38:26 +00:00
Danny Brown e8dbe3e1b6 Merge pull request #907 from dannyrb/publish/react-ohif-org
fix: runtime extension usage
2019-09-10 16:03:50 -04:00
dannyrb ac5dbda3b2 fix: simplify runtime-extension usage 2019-09-10 16:02:08 -04:00
dannyrb bf77b3959a chore: bump react peer deps 2019-09-10 15:12:59 -04:00
ohif-bot c6a6bce0b8 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.10
 - @ohif/core@0.50.8
 - @ohif/viewer@1.0.4
2019-09-10 02:48:31 +00:00
dannyrb 3f9decc6d5 Merge branch 'master' of github.com:OHIF/Viewers into upstream-master 2019-09-09 22:33:39 -04:00
dannyrb 1b1ff8068d docs: updated embedded-viewer recipe 2019-09-09 22:33:33 -04:00
ohif-bot eb1a900b13 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.9
 - @ohif/core@0.50.7
 - @ohif/viewer@1.0.3
2019-09-10 02:13:05 +00:00
dannyrb e8938111d5 ci: add checkout and restore persist 2019-09-09 21:57:01 -04:00
Danny Brown 73122e717c Merge pull request #905 from dannyrb/fix/regenerator-runtime-issue-for-umd-build
Misc. small script tag and config clean-up items
2019-09-09 21:47:21 -04:00
dannyrb 300fe06b60 chore: remove unused template 2019-09-09 21:46:38 -04:00
dannyrb 37e5387cf0 ci: update all of release to use cypress job names 2019-09-09 21:43:10 -04:00
dannyrb 7c2860789d without numbers 2019-09-09 21:41:50 -04:00
dannyrb df35b1bc06 ci: try using new names 2019-09-09 21:41:14 -04:00
dannyrb 0460a487ec Testing w/ no release steps 2019-09-09 21:39:31 -04:00
dannyrb 0195a3d860 ci: See if we can wait for dynamic job names 2019-09-09 21:37:39 -04:00
dannyrb 2740e34ad3 ignore video output 2019-09-09 21:34:34 -04:00
dannyrb 61d2caef96 ci: update RELEASE to use new cypress tests 2019-09-09 21:34:23 -04:00
dannyrb a88f37595d ci: upgrade cypress orb 2019-09-09 21:33:38 -04:00
Danny Brown 42d80a3e60 re-add working directory key 2019-09-09 18:05:03 -04:00
dannyrb 51143ffe35 Merge branch 'fix/regenerator-runtime-issue-for-umd-build' of github.com:dannyrb/Viewers into fix/regenerator-runtime-issue-for-umd-build 2019-09-09 16:40:21 -04:00
dannyrb 76f7b00b65 ci: script tag e2e tests 2019-09-09 16:40:07 -04:00
Danny Brown 64ca61745b Merge branch 'master' into fix/regenerator-runtime-issue-for-umd-build 2019-09-09 15:52:13 -04:00
dannyrb ababe63a55 fix: on-brand library global name 2019-09-09 15:50:45 -04:00
dannyrb 6c2217f2e5 chore: remove unused relativeWebWorkerScriptsPath config key 2019-09-09 15:50:10 -04:00
dannyrb cf9bec3ffa Merge branch 'fix/regenerator-runtime-issue-for-umd-build' of github.com:dannyrb/Viewers into fix/regenerator-runtime-issue-for-umd-build 2019-09-09 15:49:28 -04:00
dannyrb 32bc47d315 fix: remove requestOptions when key is not needed 2019-09-09 15:49:12 -04:00
ohif-bot 1fb98884d2 chore(release): publish [skip ci]
- @ohif/viewer@1.0.2
2019-09-09 16:35:06 +00:00
Danny Brown 7f8f9dc073 Merge pull request #902 from dannyrb/fix/regenerator-runtime-issue-for-umd-build
fix: import regenerator-runtime for umd build
2019-09-09 12:26:33 -04:00
Danny Brown c6f89495b2 Merge branch 'master' into fix/regenerator-runtime-issue-for-umd-build 2019-09-09 12:26:20 -04:00
dannyrb bad987a06b fix: import regenerator-runtime for umd build 2019-09-09 12:25:37 -04:00
ohif-bot c73abb518f chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.0.0
 - @ohif/extension-dicom-html@0.50.5
 - @ohif/extension-vtk@0.50.8
 - @ohif/core@0.50.6
 - @ohif/viewer@1.0.1
2019-09-09 15:17:11 +00:00
James Petts d12d01215e Merge pull request #900 from JamesAPetts/cst4
Upgrade to cornerstoneTools 4.0.1 and dcmjs 0.6.0
2019-09-09 17:03:10 +02:00
James A. Petts 86adb5113a feat: 🎸 Upgraded to cornerstoneTools 4.0
Upgraded OHIF to run on cornerstone-tools 4.0.1 and dcmjs 0.6.0.

BREAKING CHANGE: n
2019-09-09 16:43:48 +02:00
James A. Petts 5220cd371c Upgrade to cornerstoneTools 4.0.1 and dcmjs 0.6.0 2019-09-09 16:32:52 +02:00
Danny Brown 16b063a504 ci: fix docs deploy-preview redirect (#878)
* docs: filling in missing readme sections

* ci: fix docs deploy-preview redirect
2019-09-06 15:09:20 -04:00
ohif-bot d0d75987f7 chore(release): publish [skip ci]
- @ohif/viewer@1.0.0
2019-09-06 18:40:55 +00:00
ladeirarodolfo 7c4ee734fa refactor: 💡 React components to consume appConfig using Context (#852)
* refactor: 💡 React components to consume appConfig using Context

React components to consume app configuration using React Context and
React Hooks. Non React components to continue using global variable
window.config. Related documentation also changed.

Closes: #725

* refactor: 💡 Removing unecessary code

On current React component there is no need to import useContext method
from React, so, removing it.

* refactor: 💡 Code review

Code review. Minor changes based on review inputs and moving userManager
to an init method

BREAKING CHANGE: #725

Closes: #725

* docs: don't include implementation detail in docs

* docs: don't include implementation detail in docs

* docs: no need to specify implementation details in employment recipe

* docs: no need to specify implementation details in deployment recipe
2019-09-06 14:31:26 -04:00
ohif-bot a187783e0d chore(release): publish [skip ci]
- @ohif/viewer@0.50.21
2019-09-06 18:23:46 +00:00
Danny Brown ada05b01a7 Merge pull request #874 from dannyrb/docs/monorepo-updates-3
docs: contributing, tests, react-vtkjs-viewport link
2019-09-06 14:13:35 -04:00
Danny Brown 1a44d78277 Merge branch 'master' into docs/monorepo-updates-3 2019-09-06 14:13:22 -04:00
dannyrb 691a073b86 docs(build-pwa): updated commands, file locations, point to configs instead of duplicating 2019-09-06 10:27:02 -04:00
dannyrb 5a54564bff A way to list linked packages 2019-09-06 10:05:02 -04:00
dannyrb 94b625d4e6 fix: viewer project should build output before publish 2019-09-06 09:51:45 -04:00
dannyrb 8f730a5124 Merge branch 'docs/monorepo-updates-3' of github.com:dannyrb/Viewers into docs/monorepo-updates-3 2019-09-06 09:45:26 -04:00
dannyrb 0ff4d90878 docs(contributing): react-vtkjs-viewport, tests 2019-09-06 09:45:13 -04:00
Danny Brown a047994e3d Merge branch 'master' into docs/monorepo-updates-3 2019-09-05 22:38:24 -04:00
dannyrb 672ad2d0e7 docs(contributing): example gif 2019-09-05 22:37:17 -04:00
350 changed files with 18146 additions and 7322 deletions

No files matched your search

+231 -113
View File
@@ -9,22 +9,27 @@ version: 2.1
# create pull request previews and to update `https://docs.ohif.org` # create pull request previews and to update `https://docs.ohif.org`
### ###
## https://github.com/cypress-io/circleci-orb
##
orbs: orbs:
codecov: codecov/codecov@1.0.5 codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@1.8.0 cypress: cypress-io/cypress@1.11.0
defaults: &defaults defaults: &defaults
docker: docker:
- image: circleci/node:10.16.0 - image: circleci/node:12.9.1
environment: environment:
TERM: xterm # Enable colors in term TERM: xterm # Enable colors in term
working_directory: ~/repo working_directory: ~/repo
jobs: jobs:
PR_UNIT_TESTS: UNIT_TESTS:
<<: *defaults <<: *defaults
steps: steps:
# Update yarn
- run: yarn -v
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
# Enable yarn workspaces # Enable yarn workspaces
- run: yarn config set workspaces-experimental true - run: yarn config set workspaces-experimental true
@@ -32,21 +37,87 @@ jobs:
- checkout: - checkout:
post: post:
- git fetch --all - git fetch --all
- restore_cache: - restore_cache:
name: Restore Yarn and Cypress Package Cache name: Restore Yarn and Cypress Package Cache
keys: keys:
# when lock file changes, use increasingly general patterns to restore cache # when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }} - yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-v1-{{ .Branch }}- - yarn-packages-
- yarn-packages-v1-
- run: - run:
name: Install Dependencies name: Install Dependencies
command: yarn install --frozen-lockfile command: yarn install --frozen-lockfile
- save_cache: - save_cache:
name: Save Yarn Package Cache name: Save Yarn Package Cache
paths: paths:
- ~/.cache ## Cache yarn and Cypress - ~/.cache ## Cache yarn and Cypress
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }} key: yarn-packages-{{ checksum "yarn.lock" }}
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: yarn run test:unit:ci
# PLATFORM/VIEWER
- run:
name: 'VIEWER: Combine report output'
command: |
viewerCov="/home/circleci/repo/platform/viewer/coverage"
touch "${viewerCov}/reports"
cat "${viewerCov}/clover.xml" >> "${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
cat "${viewerCov}/lcov.info" >>"${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/viewer/coverage/reports'
flags: 'viewer'
# PLATFORM/CORE
- run:
name: 'CORE: Combine report output'
command: |
coreCov="/home/circleci/repo/platform/core/coverage"
touch "${coreCov}/reports"
cat "${coreCov}/clover.xml" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
cat "${coreCov}/lcov.info" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
UNIT_TESTS_PERSIST_WORKSPACE:
<<: *defaults
steps:
# Update yarn
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- 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-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# RUN TESTS # RUN TESTS
- run: - run:
@@ -82,76 +153,11 @@ jobs:
flags: 'core' flags: 'core'
# Persist :+1: # Persist :+1:
- persist_to_workspace: - persist_to_workspace: &persist_to_workspace
root: ~/repo root: ~/repo
paths: . paths: .
MERGE_UNIT_TESTS: NPM_PUBLISH:
<<: *defaults
steps:
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- 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-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-packages-v1-{{ .Branch }}-
- yarn-packages-v1-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: yarn run test:unit:ci
# PLATFORM/VIEWER
- run:
name: 'VIEWER: Combine report output'
command: |
viewerCov="/home/circleci/repo/platform/viewer/coverage"
touch "${viewerCov}/reports"
cat "${viewerCov}/clover.xml" >> "${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
cat "${viewerCov}/lcov.info" >>"${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/viewer/coverage/reports'
flags: 'viewer'
# PLATFORM/CORE
- run:
name: 'CORE: Combine report output'
command: |
coreCov="/home/circleci/repo/platform/core/coverage"
touch "${coreCov}/reports"
cat "${coreCov}/clover.xml" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
cat "${coreCov}/lcov.info" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
# Persist :+1:
- persist_to_workspace:
root: ~/repo
paths: .
npm_publish:
<<: *defaults <<: *defaults
steps: steps:
- attach_workspace: - attach_workspace:
@@ -175,7 +181,7 @@ jobs:
root: ~/repo root: ~/repo
paths: . paths: .
docs_publish: DOCS_PUBLISH:
<<: *defaults <<: *defaults
steps: steps:
- attach_workspace: - attach_workspace:
@@ -192,25 +198,25 @@ jobs:
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh - run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
- run: ~/repo/.circleci/build-and-publish-docs.sh - run: ~/repo/.circleci/build-and-publish-docs.sh
docker_publish: DOCKER_MASTER_PUBLISH:
<<: *defaults <<: *defaults
steps: steps:
- attach_workspace: - attach_workspace:
at: ~/repo at: ~/repo
- setup_remote_docker: - setup_remote_docker:
docker_layer_caching: true docker_layer_caching: false
- run: - run:
name: Build and push Docker image name: Build and push Docker image
command: | command: |
# This file will exist if a new version was published by # This file will exist if a new version was published by
# our `semantic-release` command in the previous job # our `semantic-release` command in the previous job
if [[ ! -e tmp/updated-version.txt ]]; then if [[ ! -e platform/viewer/success_version.txt ]]; then
exit 0 exit 0
else else
# Remove npm config # Remove npm config
rm -f ./.npmrc rm -f ./.npmrc
# Set our version number using vars # Set our version number using vars
export IMAGE_VERSION=$(cat tmp/updated-version.txt) export IMAGE_VERSION=$(cat platform/viewer/success_version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM} export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
echo $IMAGE_VERSION echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL echo $IMAGE_VERSION_FULL
@@ -221,7 +227,44 @@ jobs:
docker push ohif/$IMAGE_NAME:latest docker push ohif/$IMAGE_NAME:latest
fi fi
viewer_demo_build: DOCKER_PR_PUBLISH:
<<: *defaults
steps:
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- 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-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build and push Docker image
command: |
# Remove npm config
rm -f ./.npmrc
# Set our version number using vars
echo $CIRCLE_BUILD_NUM
# Build our image, auth, and push
docker build --tag ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM
VIEWER_DEMO_BUILD:
<<: *defaults <<: *defaults
steps: steps:
- attach_workspace: - attach_workspace:
@@ -230,7 +273,7 @@ jobs:
- run: - run:
name: 'Build Demo Site' name: 'Build Demo Site'
command: yarn run build:demo command: yarn run build:demo
no_output_timeout: 30m no_output_timeout: 45m
- run: - run:
name: 'Upload SourceMaps, Send Deploy Notification' name: 'Upload SourceMaps, Send Deploy Notification'
command: | command: |
@@ -246,7 +289,7 @@ jobs:
root: ~/repo root: ~/repo
paths: . paths: .
viewer_demo_publish: VIEWER_DEMO_PUBLISH:
working_directory: ~/repo working_directory: ~/repo
docker: docker:
- image: google/cloud-sdk - image: google/cloud-sdk
@@ -254,7 +297,7 @@ jobs:
- attach_workspace: - attach_workspace:
at: ~/repo at: ~/repo
- setup_remote_docker: - setup_remote_docker:
docker_layer_caching: true docker_layer_caching: false
- run: - run:
name: Deploy latest version to viewer.ohif.org name: Deploy latest version to viewer.ohif.org
command: | command: |
@@ -272,71 +315,146 @@ workflows:
# PULL REQUESTS # PULL REQUESTS
PR_CHECKS: PR_CHECKS:
jobs: jobs:
- PR_UNIT_TESTS: - UNIT_TESTS:
filters: filters:
branches: branches:
ignore: ignore:
- master - master
- feature/* - feature/*
- hotfix/* - hotfix/*
# Cypress job added by `cypress-io` orb # E2E: PWA
- cypress/run: - cypress/run:
name: 'E2E: PWA'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps: pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest add wait-on' # Use yarn latest
yarn: true yarn: true
store_artifacts: true record: false
start: yarn run dev store_artifacts: false
wait-on: 'http://localhost:3000'
no-workspace: true # Don't persist workspace
working_directory: platform/viewer working_directory: platform/viewer
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}' build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps: post-steps:
- store_test_results: - store_artifacts:
path: cypress/results path: platform/viewer/cypress/screenshots
requires: requires:
- PR_UNIT_TESTS - UNIT_TESTS
# E2E: script-tag
- cypress/run:
name: 'E2E: Script Tag'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest
yarn: true
record: false
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
requires:
- UNIT_TESTS
PR_OPTIONAL_DOCKER_PUBLISH:
jobs:
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
- AWAIT_APPROVAL:
type: approval
# Update hub.docker.org
- DOCKER_PR_PUBLISH:
context: Docker Hub
requires:
- AWAIT_APPROVAL
# MERGE TO MASTER # MERGE TO MASTER
RELEASE: RELEASE:
jobs: jobs:
- MERGE_UNIT_TESTS: - UNIT_TESTS_PERSIST_WORKSPACE:
filters: filters:
branches: branches:
only: master only: master
# Cypress job added by `cypress-io` orb # E2E: PWA + Persist
- cypress/run: - cypress/run:
name: 'E2E: PWA'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps: pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest add wait-on' # Use yarn latest
yarn: true yarn: true
record: true record: false
store_artifacts: true store_artifacts: true
start: yarn run dev
wait-on: 'http://localhost:3000'
working_directory: platform/viewer working_directory: platform/viewer
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}' build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
command: 'yarn run test:e2e:ci'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps: post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_test_results: - store_test_results:
path: cypress/results path: cypress/results
requires: requires:
- MERGE_UNIT_TESTS - UNIT_TESTS_PERSIST_WORKSPACE
# E2E: script-tag
- cypress/run:
name: 'E2E: Script Tag'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest
yarn: true
record: false
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_test_results:
path: cypress/results
requires:
- UNIT_TESTS_PERSIST_WORKSPACE
# Update NPM # Update NPM
- npm_publish: - NPM_PUBLISH:
requires: requires:
- cypress/run - 'E2E: Script Tag'
- 'E2E: PWA'
# Update docs.ohif.org # Update docs.ohif.org
- docs_publish: - DOCS_PUBLISH:
requires: requires:
- cypress/run - 'E2E: Script Tag'
# Update hub.docker.org - 'E2E: PWA'
# - docker_publish: - VIEWER_DEMO_BUILD:
# requires: requires:
# - npm_publish - 'E2E: Script Tag'
- 'E2E: PWA'
# Update viewer.ohif.org # Update viewer.ohif.org
- viewer_demo_build: - VIEWER_DEMO_PUBLISH:
requires: requires:
- cypress/run - VIEWER_DEMO_BUILD
- viewer_demo_publish: # Update hub.docker.org
- DOCKER_MASTER_PUBLISH:
requires: requires:
- viewer_demo_build - NPM_PUBLISH
+3 -1
View File
@@ -8,11 +8,13 @@ coverage:
status: status:
project: project:
default: default:
threshold: 0.10% threshold: 0.5%
core: core:
flags: core flags: core
threshold: 0.5%
viewer: viewer:
flags: viewer flags: viewer
threshold: 0.5%
patch: off patch: off
flags: flags:
core: core:
+12
View File
@@ -0,0 +1,12 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
if [ ! -z "$CLIENT_ID" ]
then
echo "Google Cloud Healthcare $CLIENT_ID has been provided: "
echo $CLIENT_ID
echo "Updating config..."
# - Use SED to replace the CLIENT_ID that is currently in google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
# - Copy google.js to overwrite app-config.js
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
fi
echo "Starting Nginx to serve the OHIF Viewer..."
exec "$@"
+30
View File
@@ -0,0 +1,30 @@
# Reduces size of context and hides
# files from Docker (can't COPY or ADD these)
# Output
dist/
build/
# Dependencies
node_modules/
# Root
README.md
Dockerfile
dockerfile
# Misc. Config
.git
.DS_Store
.gitignore
.vscode
.circleci
# Unnecessary things to pull into container
.circleci/
.github/
.netlify/
.scripts/
.vscode/
coverage/
docs/
+1
View File
@@ -0,0 +1 @@
PERCY_TOKEN=<your token here>
+1
View File
@@ -16,6 +16,7 @@
}, },
"globals": { "globals": {
"cy": true, "cy": true,
"before": true,
"context": true, "context": true,
"Cypress": true, "Cypress": true,
"assert": true "assert": true
@@ -0,0 +1,29 @@
### Request Checklist
- [] Brief description of changes
- [] Links to any relevant issues
- [] Required status checks are passing
- [] `@mention` a maintainer to request a review
#### Special
> If this PR is not on a branch for this repo, it is "untrusted" and does not
> have access to env vars. Reviewers can kick off a trusted docker deploy by:
>
> 1. Verifying there are no malicious changes to CI build
> 2. Using the [git-push-fork-to-upstream-branch][script] script
> 3. Executing the manual approval step in the CI workflow
- [] Request docker publish of PR
_Want to improve this process? Consider PR'ing the ability for reviewers to add
a label to trigger this process._
<!--
Links
-->
<!-- prettier-ignore-start -->
[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/
[script]: https://github.com/jklukas/git-push-fork-to-upstream-branch
<!-- prettier-ignore-end -->
+3 -3
View File
@@ -7,13 +7,13 @@ daysUntilStale: 21
daysUntilClose: 7 daysUntilClose: 7
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- 'Story 🙌' - 'Story :raised_hands:'
- 'Bug: Verified 🐛' - 'Bug: Verified :bug:'
- 'Task: CI/Tooling 🤖' - 'Task: CI/Tooling 🤖'
- 'Task: Docs 📖' - 'Task: Docs 📖'
- 'Task: Docs :book:' - 'Task: Docs :book:'
- 'Task: Refactor 🛠' - 'Task: Refactor 🛠'
- 'Task: Tests 🔬' - 'Task: Tests :microscope:'
- 'PR: Awaiting Review 👀' - 'PR: Awaiting Review 👀'
- 'Triage 🏳️' - 'Triage 🏳️'
- 'Announcement 🎉' - 'Announcement 🎉'
+3 -2
View File
@@ -21,6 +21,7 @@ npm-debug.log
package-lock.json package-lock.json
yarn-error.log yarn-error.log
.DS_Store .DS_Store
.env
# Common Example Data Directories # Common Example Data Directories
sampledata/ sampledata/
@@ -28,7 +29,7 @@ example/deps/
docker/dcm4che/dcm4che-arc docker/dcm4che/dcm4che-arc
# Cypress test results # Cypress test results
cypress/videos/ videos/
# Locize settings # Locize settings
.locize .locize
+6
View File
@@ -0,0 +1,6 @@
# Specific to our deploy-preview
# Our docs are published using CircleCI + GitBook
# Confgure redirects using netlify.toml
# PWA Demo
/pwa/* /pwa/index.html 200
+9 -7
View File
@@ -1,10 +1,8 @@
{ {
"editor.rulers": [80, 120], "editor.rulers": [80, 120],
// === // ===
// Spacing // Spacing
// === // ===
"editor.insertSpaces": true, "editor.insertSpaces": true,
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.trimAutoWhitespace": true, "editor.trimAutoWhitespace": true,
@@ -12,19 +10,23 @@
"files.eol": "\n", "files.eol": "\n",
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.trimFinalNewlines": true, "files.trimFinalNewlines": true,
// === // ===
// Event Triggers // Event Triggers
// === // ===
"editor.formatOnSave": true, "editor.formatOnSave": true,
"eslint.autoFixOnSave": true, "eslint.autoFixOnSave": true,
"eslint.run": "onSave", "eslint.run": "onSave",
"eslint.validate": [ "eslint.validate": [
{ "language": "javascript", "autoFix": true }, {
{ "language": "javascriptreact", "autoFix": true } "language": "javascript",
"autoFix": true
},
{
"language": "javascriptreact",
"autoFix": true
}
], ],
"prettier.disableLanguages": [], "prettier.disableLanguages": ["html"],
"prettier.endOfLine": "lf", "prettier.endOfLine": "lf",
"workbench.colorCustomizations": {} "workbench.colorCustomizations": {}
} }
+56 -4
View File
@@ -1,23 +1,54 @@
// ~~ ENV
const dotenv = require('dotenv');
//
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const PACKAGE = require('../platform/viewer/package.json');
// ~~ RULES
const loadShadersRule = require('./rules/loadShaders.js'); const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js'); const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js'); const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
// ~~ PLUGINS
const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
const QUICK_BUILD = process.env.QUICK_BUILD;
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
//
dotenv.config();
module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => { module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
if (!process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
throw new Error('process.env.NODE_ENV not set'); throw new Error('process.env.NODE_ENV not set');
} }
const mode = const mode = NODE_ENV === 'production' ? 'production' : 'development';
process.env.NODE_ENV === 'production' ? 'production' : 'development'; const isProdBuild = NODE_ENV === 'production';
const isQuickBuild = QUICK_BUILD === 'true';
return { const config = {
mode, mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
entry: { entry: {
app: `${SRC_DIR}/index.js`, app: `${SRC_DIR}/index.js`,
}, },
optimization: {
minimize: isProdBuild,
sideEffects: true,
},
context: SRC_DIR, context: SRC_DIR,
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
module: { module: {
rules: [ rules: [
transpileJavaScriptRule(mode), transpileJavaScriptRule(mode),
@@ -45,6 +76,8 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
'process.env.DEBUG': JSON.stringify(process.env.DEBUG), 'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''), 'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'), 'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
}), }),
], ],
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881 // Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
@@ -53,4 +86,23 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
fs: 'empty', fs: 'empty',
}, },
}; };
if (isProdBuild) {
config.optimization.minimizer = [
new TerserJSPlugin({
// Supports:
// source-map and inline-source-map
sourceMap: isProdBuild && !isQuickBuild,
parallel: true,
terserOptions: {},
}),
];
}
if (isQuickBuild) {
config.optimization.minimize = false;
config.devtool = false;
}
return config;
}; };
+65
View File
@@ -0,0 +1,65 @@
# This dockerfile is used to publish the `ohif/viewer` image on dockerhub.
#
# It's a good example of how to build our static application and package it
# with a web server capable of hosting it as static content.
#
# docker build
# --------------
# If you would like to use this dockerfile to build and tag an image, make sure
# you set the context to the project's root directory:
# https://docs.docker.com/engine/reference/commandline/build/
#
#
# SUMMARY
# --------------
# This dockerfile has two stages:
#
# 1. Building the React application for production
# 2. Setting up our Nginx (Alpine Linux) image w/ step one's output
#
# Stage 1: Build the application
# docker build -t ohif/viewer:latest .
FROM node:10.16.3-slim as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
# Copy Files
COPY .docker /usr/src/app/.docker
COPY .webpack /usr/src/app/.webpack
COPY extensions /usr/src/app/extensions
COPY platform /usr/src/app/platform
COPY .browserslistrc /usr/src/app/.browserslistrc
COPY aliases.config.js /usr/src/app/aliases.config.js
COPY babel.config.js /usr/src/app/babel.config.js
COPY lerna.json /usr/src/app/lerna.json
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 the install before copying the rest of the files
RUN yarn config set workspaces-experimental true
RUN yarn install
ENV PATH /usr/src/app/node_modules/.bin:$PATH
ENV QUICK_BUILD true
# ENV GENERATE_SOURCEMAP=false
# ENV REACT_APP_CONFIG=config/default.js
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
RUN apk add --no-cache bash
RUN rm -rf /etc/nginx/conf.d
COPY .docker/Viewer-v2.x /etc/nginx/conf.d
COPY .docker/Viewer-v2.x/entrypoint.sh /usr/src/
RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/platform/viewer/dist /usr/share/nginx/html
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+5 -3
View File
@@ -26,6 +26,7 @@
[![Netlify Status][netlify-image]][netlify-url] [![Netlify Status][netlify-image]][netlify-url]
[![CircleCI][circleci-image]][circleci-url] [![CircleCI][circleci-image]][circleci-url]
[![codecov][codecov-image]][codecov-url] [![codecov][codecov-image]][codecov-url]
[![This project is using Percy.io for visual regression testing.][percy-image]](percy-url)
[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
@@ -123,8 +124,7 @@ window.config = {
`window.OHIFStandaloneViewer.installViewer(window.config);` `window.OHIFStandaloneViewer.installViewer(window.config);`
This exact setup is demonstrated in this This exact setup is demonstrated in this
[CodeSandbox](https://codesandbox.io/s/ohif-viewer-script-tag-usage-c4u4t) and [CodeSandbox](https://codesandbox.io/s/viewer-script-tag-tprch) and in our
in our
[Embedding The Viewer](https://docs.ohif.org/deployment/recipes/embedded-viewer.html) [Embedding The Viewer](https://docs.ohif.org/deployment/recipes/embedded-viewer.html)
deployment recipe. deployment recipe.
@@ -133,7 +133,7 @@ deployment recipe.
### Requirements ### Requirements
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install) - [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
- [Node 8+](https://nodejs.org/en/) - [Node 10+](https://nodejs.org/en/)
- Yarn Workspaces should be enabled on your machine: - Yarn Workspaces should be enabled on your machine:
- `yarn config set workspaces-experimental true` - `yarn config set workspaces-experimental true`
@@ -289,6 +289,8 @@ MIT © [OHIF](https://github.com/OHIF)
[docker-image-url]: https://hub.docker.com/r/ohif/viewer [docker-image-url]: https://hub.docker.com/r/ohif/viewer
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
[license-url]: LICENSE [license-url]: LICENSE
[percy-image]: https://percy.io/static/images/percy-badge.svg
[percy-url]: https://percy.io/Open-Health-Imaging-Foundation/OHIF-Viewer
<!-- Links --> <!-- Links -->
[monorepo]: https://en.wikipedia.org/wiki/Monorepo [monorepo]: https://en.wikipedia.org/wiki/Monorepo
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo [how-to-fork]: https://help.github.com/en/articles/fork-a-repo
+3
View File
@@ -0,0 +1,3 @@
# Netlify redirects
# SPA rules for our docs
/* /index.html 200
+36 -3
View File
@@ -140,8 +140,8 @@ children: PropTypes.node,
customProps: PropTypes.object customProps: PropTypes.object
``` ```
Viewport components are managed by the `LayoutManager`. Which Viewport component Viewport components are managed by the `ViewportGrid` Component. Which Viewport
is used depends on: component is used depends on:
- The Layout Configuration - The Layout Configuration
- Registered SopClassHandlers - Registered SopClassHandlers
@@ -159,7 +159,8 @@ For a complete example implementation,
An extension can register a Toolbar Module by providing a `getToolbarModule()` An extension can register a Toolbar Module by providing a `getToolbarModule()`
method that returns a React Component. The component does not receive any props. method that returns a React Component. The component does not receive any props.
If you want to modify or react to state, you will need to connect to the redux If you want to modify or react to state, you will need to connect to the redux
store. store. The given toolbar must determine its set of elements and the context of
them. The set of elements will be listed on toolbar `definitions`.
![Toolbar Extension](../assets/img/extensions-toolbar.gif) ![Toolbar Extension](../assets/img/extensions-toolbar.gif)
@@ -170,6 +171,38 @@ Toolbar components are rendered in the `ToolbarRow` component.
For a complete example implementation, For a complete example implementation,
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js). [check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
##### Toolbar Custom Component
Toolbar elements can define its own custom react component to be consumed when
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
you just need to expose your `CustomToolbarComponent` as the value of key
`CustomComponent`. In case the property `CustomComponent` is not present, a
default toolbar component will be used to render it. See bellow
```js
definitions: [
...
{
id: 'Custom',
label: 'Custom',
icon: 'custom-icon',
CustomComponent: CustomToolbarComponent,
}
...
]
```
`CustomComponent` components will receive the following props:
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
instance)
- toolbarClickCallback: callback method when clicking on toolbar
- button: its own definition object
- key: react key prop
- activeButtons: list of active elements
- isActive: if current
#### SopClassHandler #### SopClassHandler
... ...
+36 -5
View File
@@ -10,7 +10,15 @@ minimum, you may want to read the following documentation:
- [Essentials: Getting Started](./essentials/getting-started.md) - [Essentials: Getting Started](./essentials/getting-started.md)
- [Advanced: Architecture](./advanced/architecture.md) - [Advanced: Architecture](./advanced/architecture.md)
### When changes impact multiple repositories Pull requests that are:
- Small
- [Well tested](./testing.md)
- Decoupled
Are much more likely to get reviewed and merged in a timely manner.
## When changes impact multiple repositories
While this can be tricky, we've tried to reduce how often this situation crops While this can be tricky, we've tried to reduce how often this situation crops
up this with our [recent switch to a monorepo][monorepo]. Our maintained up this with our [recent switch to a monorepo][monorepo]. Our maintained
@@ -22,7 +30,7 @@ package outside of the monorepo is most common with extension development. Let's
demonstrate how to accomplish this with two commonly forked extension demonstrate how to accomplish this with two commonly forked extension
dependencies: dependencies:
#### `cornerstone-tools` ### `cornerstone-tools`
On your local file system: On your local file system:
@@ -47,7 +55,12 @@ On your local file system:
As you make changed to `cornerstone-tools`, and it's output is rebuilt, you As you make changed to `cornerstone-tools`, and it's output is rebuilt, you
should see the following behavior: should see the following behavior:
<!-- Insert GIF here --> <div style="text-align: center;">
<a href="/assets/img/cornerstone-tools-link.gif">
<img src="/assets/img/cornerstone-tools-link.gif" alt="Example of linked cornerstone-tools package" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>example of linked cornerstone-tools package</i></div>
</div>
If you wish to stop using your local package, run the following commands in the If you wish to stop using your local package, run the following commands in the
`ohif/viewers` repository root: `ohif/viewers` repository root:
@@ -55,9 +68,27 @@ If you wish to stop using your local package, run the following commands in the
- `yarn unlink cornerstone-tools` - `yarn unlink cornerstone-tools`
- `yarn install --force` - `yarn install --force`
#### `react-vtkjs-viewport` ### `react-vtkjs-viewport`
... On your local file system:
```bash
# code/my-projects/
.
├── ohif/react-vtkjs-viewport
└── ohif/viewers
```
- Open a terminal/shell
- Navigate to `ohif/react-vtkjs-viewport`
- `yarn install`
- [`yarn link`](https://yarnpkg.com/en/docs/cli/link)
- `yarn run start`
- Open a new terminal/shell
- Navigate to `ohif/viewers`.
- `yarn install`
- [`yarn link react-vtkjs-viewport`](https://yarnpkg.com/en/docs/cli/link)
- `yarn run dev`
#### Other linkage notes #### Other linkage notes
+1 -1
View File
@@ -205,5 +205,5 @@ Please feel free to suggest or contribute your own recipes.
[host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/ [host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/
[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS [cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
[code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660 [code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
[code-sandbox]: https://codesandbox.io/s/ohif-viewer-script-tag-usage-b3st9 [code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
@@ -19,9 +19,6 @@ _With Git:_
```bash ```bash
# Clone the remote repository to your local machine # Clone the remote repository to your local machine
git clone https://github.com/OHIF/Viewers.git git clone https://github.com/OHIF/Viewers.git
# Make sure the local code reflects the `react` version of the OHIF Viewer
git checkout react
``` ```
More on: _[`git clone`](https://git-scm.com/docs/git-clone), More on: _[`git clone`](https://git-scm.com/docs/git-clone),
@@ -29,7 +26,7 @@ More on: _[`git clone`](https://git-scm.com/docs/git-clone),
_From .zip:_ _From .zip:_
[OHIF/Viewers: react.zip](https://github.com/OHIF/Viewers/archive/react.zip) [OHIF/Viewers: react.zip](https://github.com/OHIF/Viewers/archive/master.zip)
### Restore Dependencies & Build ### Restore Dependencies & Build
@@ -37,20 +34,24 @@ Open your terminal, and navigate to the directory containing the source files.
Next run these commands: Next run these commands:
```js ```js
// If you haven't already, enable yarn workspaces
yarn config set workspaces-experimental true
// Restore dependencies // Restore dependencies
yarn install yarn install
// Build source code for production // Build source code for production
yarn run build:web yarn run build
``` ```
If everything worked as expected, you should have a new `build/` directory in If everything worked as expected, you should have a new `dist/` directory in the
the project's folder. It should roughly resemble the following: project's folder. It should roughly resemble the following:
```bash ```bash
build <root>platform/viewer/dist/
├── config/ ├── app-config.js
├── static/ ├── app.bundle.js
├── app.css
├── index.html ├── index.html
├── manifest.json ├── manifest.json
├── service-worker.js ├── service-worker.js
@@ -64,60 +65,15 @@ how to configure the project for your own imaging archive below.
### Configuration ### Configuration
> This step assumes you have an imaging archive. If you need assistance setting The configuration for our viewer is in the `<root>platform/viewer/public/config`
> one up, check out the [`Data Source` Guide](./../../essentials/data-source.md) directory. Our build process knows which configuration file to use based on the
> or a deployment recipe that contains an open source Image Archive `APP_CONFIG` environment variable. By default, its value is
[`config/default.js`][default-config]. The majority of the viewer's features,
and registered extension's features, are configured using this file.
#### How it Works The easiest way to apply your own configuration is to modify the `default.js`
file. For more advanced cofiguration options, check out our
The configuration for our project is in the `/public/config` directory. Our [configuration essentials guide](/essentials/configuration.md).
build process knows which configuration file to use based on the `APP_CONFIG`
environment variable. By default, its value is
[`default.js`](https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js).
When we build, the `%APP_CONFIG%` value in
our[`/public/index.html`](https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/index.html)
file is substituted for the correct configuration file's name. This sets
the`window.config` equal to our configuration file's value.
#### How do I configure my project?
The simplest way is to update the existing default config:
_/public/config/default.js_
```js
window.config = {
routerBasename: '/',
servers: {
dicomWeb: [
{
name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
requestOptions: {
requestFromBrowser: true,
},
},
],
},
};
```
You can also create a new config file and specify its path relative to the build
output's root by setting the `APP_CONFIG` environment variable. You can set the
value of this environment variable a few different ways:
- [Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)
- [Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)
- Using the `cross-env` package in an npm script:
- `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"`
After updating the configuration, `yarn run build:web` to generate updated build
output.
## Next Steps ## Next Steps
@@ -141,7 +97,7 @@ _Advanced_
### Testing Build Output Locally ### Testing Build Output Locally
A quick way to test your build output locally is to spin up a small webserver. A quick way to test your build output locally is to spin up a small webserver.
You can do this by running the following commands in the `build/` output You can do this by running the following commands in the `dist/` output
directory: directory:
```js ```js
@@ -162,8 +118,7 @@ web application. For a starting point, check out this repository's own use of:
- [CircleCI][circleci]: [config.yaml][circleci-config] - [CircleCI][circleci]: [config.yaml][circleci-config]
- [Netlify][netlify]: [netlify.toml][netlify.toml] | - [Netlify][netlify]: [netlify.toml][netlify.toml] |
[generateStaticSite.sh][generatestaticsite.sh] [build-deploy-preview.sh][build-deploy-preview.sh]
- [Semantic-Release][semantic-release]: [.releaserc][releaserc]
## Troubleshooting ## Troubleshooting
@@ -173,10 +128,8 @@ web application. For a starting point, check out this repository's own use of:
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[circleci]: https://circleci.com/gh/OHIF/Viewers [circleci]: https://circleci.com/gh/OHIF/Viewers
[circleci-config]: https://github.com/OHIF/Viewers/blob/react/.circleci/config.yml [circleci-config]: https://github.com/OHIF/Viewers/blob/master/.circleci/config.yml
[netlify]: https://app.netlify.com/sites/ohif/deploys [netlify]: https://app.netlify.com/sites/ohif/deploys
[netlify.toml]: https://github.com/OHIF/Viewers/blob/react/netlify.toml [netlify.toml]: https://github.com/OHIF/Viewers/blob/master/netlify.toml
[generateStaticSite.sh]: https://github.com/OHIF/Viewers/blob/react/generateStaticSite.sh [build-deploy-preview.sh]: https://github.com/OHIF/Viewers/blob/master/.netlify/build-deploy-preview.sh
[semantic-release]: https://semantic-release.gitbook.io/semantic-release/
[releaserc]: https://github.com/OHIF/Viewers/blob/react/.releaserc
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
@@ -16,16 +16,6 @@ include tags. Here's how it works:
<code>Google Font: Roboto</code> <code>Google Font: Roboto</code>
</a> </a>
</li> </li>
<li>
<a href="https://unpkg.com/react@16/umd/react.production.min.js">
<code>react@16.8.6</code>
</a>
</li>
<li>
<a href="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js">
<code>react-dom@16.8.6</code>
</a>
</li>
<li> <li>
<a href="https://unpkg.com/@ohif/viewer"> <a href="https://unpkg.com/@ohif/viewer">
<code>@ohif/viewer@latest</code> <code>@ohif/viewer@latest</code>
@@ -53,28 +43,64 @@ window.config = {
qidoSupportsIncludeField: true, qidoSupportsIncludeField: true,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
requestOptions: {
requestFromBrowser: true,
},
}, },
], ],
}, },
}; };
``` ```
<ol start="5"><li> <ol start="3"><li>
Render the viewer in the web page's target <code>div</code> Render the viewer in the web page's target <code>div</code>
</li></ol> </li></ol>
```js ```js
// Made available by the `@ohif/viewer` script included in step 1 // Made available by the `@ohif/viewer` script included in step 1
var Viewer = window.OHIFStandaloneViewer.App; var containerId = 'id-of-div-to-render-component-to';
var app = React.createElement(Viewer, window.config, null); var componentRenderedOrUpdatedCallback = function() {
console.log('OHIF Viewer rendered/updated');
ReactDOM.render(app, document.getElementById('ohif-viewer-target')); };
window.OHIFViewer.installViewer(
window.config,
containerId,
componentRenderedOrUpdatedCallback
);
``` ```
#### Tips & Tricks You can see a live example of this recipe in [this CodeSandbox][code-sandbox].
## Add Extensions
The UMD build of the OHIF Viewer is a "light weight" build that only contains
the core extensions required for basic 2D image viewing. It's possible to add
other extensions at runtime.
This only requires us to include a single script tag, and add it using the
`extensions` key to our config. In this practical example, we register our
popular whole slide microscopy extension:
```html
<script
src="https://unpkg.com/@ohif/extension-dicom-microscopy@0.50.5/dist/index.umd.js"
crossorigin
></script>
<!-- --->
<script>
window.config = {
// ...
extensions: [OHIFExtDicomMicroscopy],
};
</script>
```
You can see an example of a slide microscopy study in the viewer [with the
extension enabled here][whole-slide-ext-demo] ([source code][ext-code-sandbox])
and [without it here][whole-slide-base-demo] ([source code][code-sandbox]).
You can read more about extensions and how to create your own in our
[extensions guide](/advanced/extensions.md).
#### FAQ
> I'm having trouble getting this to work. Where can I go for help? > I'm having trouble getting this to work. Where can I go for help?
@@ -94,8 +120,22 @@ Good. Now `embed` that new page using an
This should produce the expected result while also protecting your page from any This should produce the expected result while also protecting your page from any
globally defined styles/scripts. globally defined styles/scripts.
> We're trying to embed the OHIF Viewer into an existing React App, but seeing
> react-dom and react conflicts. What can we do?
`installViewer` is a convenience method that pulls in some dependencies that may
not be compatible with existing `react` apps. `@ohif/viewer` also exports `App`
which is a react component that takes the `configuration` outlined above as
props. You can use it as a reusable component, and to avoid `react` version
conflict issues.
<!-- <!--
LINKS LINKS
--> -->
[code-sandbox]: https://codesandbox.io/s/ohif-viewer-script-tag-usage-b3st9 <!-- prettier-ignore-start -->
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
[whole-slide-base-demo]: https://tprch.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2020.1519719354.757
[ext-code-sandbox]: https://codesandbox.io/s/viewer-script-tag-microscopy-extension-44unk
[whole-slide-ext-demo]: https://44unk.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2448.1519719572.518
<!-- prettier-ignore-end -->
@@ -121,11 +121,7 @@ likely want to update:
#### OHIF Viewer #### OHIF Viewer
The OHIF Viewer's configuration is imported from a static `.js` file and made The OHIF Viewer's configuration is imported from a static `.js` file. The configuration we use is set to a specific file when we build the viewer, and determined by the env variable: `APP_CONFIG`. You can see where we set its value in the `dockerfile` for this solution:
available globally at `window.config`. The configuration we use is set to a
specific file when we build the viewer, and determined by the env variable:
`APP_CONFIG`. You can see where we set its value in the `dockerfile` for this
solution:
`ENV APP_CONFIG=config/docker_openresty-orthanc.js` `ENV APP_CONFIG=config/docker_openresty-orthanc.js`
@@ -122,11 +122,7 @@ likely want to update:
#### OHIF Viewer #### OHIF Viewer
The OHIF Viewer's configuration is imported from a static `.js` file and made The OHIF Viewer's configuration is imported from a static `.js` file. The configuration we use is set to a specific file when we build the viewer, and determined by the env variable: `APP_CONFIG`. You can see where we set its value in the `dockerfile` for this solution:
available globally at `window.config`. The configuration we use is set to a
specific file when we build the viewer, and determined by the env variable:
`APP_CONFIG`. You can see where we set its value in the `dockerfile` for this
solution:
`ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js` `ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js`
-3
View File
@@ -59,9 +59,6 @@ window.config = {
qidoSupportsIncludeField: true, qidoSupportsIncludeField: true,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
requestOptions: {
requestFromBrowser: true,
},
}, },
], ],
}, },
@@ -1,3 +1,4 @@
const webpack = require('webpack');
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
@@ -29,9 +30,15 @@ module.exports = (env, argv) => {
}, },
output: { output: {
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohifExtensionCornerstone', library: 'OHIFExtCornerstone',
libraryTarget: 'umd', libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main, filename: pkg.main,
}, },
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
}); });
}; };
+157
View File
@@ -3,6 +3,163 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.1...@ohif/extension-cornerstone@1.4.0) (2019-10-26)
### Features
* Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.0...@ohif/extension-cornerstone@1.3.1) (2019-10-09)
### Bug Fixes
* 🐛 set tools bidirectional, eraser and annotate command ([#1020](https://github.com/OHIF/Viewers/issues/1020)) ([a28984e](https://github.com/OHIF/Viewers/commit/a28984e))
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.5...@ohif/extension-cornerstone@1.3.0) (2019-10-09)
### Features
* Multiple fixes and implementation changes to react-cornerstone-viewport ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3))
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.4...@ohif/extension-cornerstone@1.2.5) (2019-09-27)
### Bug Fixes
* version bump issue ([#963](https://github.com/OHIF/Viewers/issues/963)) ([e607ed2](https://github.com/OHIF/Viewers/commit/e607ed2)), closes [#962](https://github.com/OHIF/Viewers/issues/962)
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
### Bug Fixes
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
* version bump issue ([#962](https://github.com/OHIF/Viewers/issues/962)) ([c80ea17](https://github.com/OHIF/Viewers/commit/c80ea17))
### BREAKING CHANGES
* DICOM Seg
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
### Bug Fixes
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
### BREAKING CHANGES
* DICOM Seg
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.1...@ohif/extension-cornerstone@1.2.2) (2019-09-26)
### Bug Fixes
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.0...@ohif/extension-cornerstone@1.2.1) (2019-09-17)
### Bug Fixes
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.1...@ohif/extension-cornerstone@1.2.0) (2019-09-12)
### Features
* **Annotate:** Add annotate tool back to toolbar ([26be967](https://github.com/OHIF/Viewers/commit/26be967))
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.0...@ohif/extension-cornerstone@1.1.1) (2019-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.0.1...@ohif/extension-cornerstone@1.1.0) (2019-09-12)
### Features
* **BidirectionalTool:** Add BidrectionalTool to "more" menu ([#911](https://github.com/OHIF/Viewers/issues/911)) ([e40cbae](https://github.com/OHIF/Viewers/commit/e40cbae))
* **EraserTool:** add eraserTool to @ohif/extension-cornerstone ([#912](https://github.com/OHIF/Viewers/issues/912)) ([698d274](https://github.com/OHIF/Viewers/commit/698d274))
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.0.0...@ohif/extension-cornerstone@1.0.1) (2019-09-10)
### Bug Fixes
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.5...@ohif/extension-cornerstone@1.0.0) (2019-09-09)
### Features
* 🎸 Upgraded to cornerstoneTools 4.0 ([86adb51](https://github.com/OHIF/Viewers/commit/86adb51))
### BREAKING CHANGES
* n
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.4...@ohif/extension-cornerstone@0.50.5) (2019-09-06) ## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.4...@ohif/extension-cornerstone@0.50.5) (2019-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone **Note:** Version bump only for package @ohif/extension-cornerstone
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone", "name": "@ohif/extension-cornerstone",
"version": "0.50.5", "version": "1.4.0",
"description": "OHIF extension for Cornerstone", "description": "OHIF extension for Cornerstone",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -32,14 +32,14 @@
"@ohif/ui": "^0.50.0", "@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "^3.1.0", "cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.3.8", "dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^15.0.0 || ^16.0.0", "react": "^16.8.6",
"react-dom": "^15.0.0 || ^16.0.0", "react-dom": "^16.8.6",
"react-redux": "^6.0.0", "react-redux": "^6.0.0",
"react-resize-detector": "^3.4.0", "react-resize-detector": "^3.4.0",
"redux": "^4.0.1" "redux": "^4.0.1"
@@ -48,6 +48,6 @@
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"react-cornerstone-viewport": "0.1.30" "react-cornerstone-viewport": "2.x.x"
} }
} }
@@ -23,36 +23,40 @@ const MEASUREMENT_ACTION_MAP = {
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
let dataFromStore; let dataFromStore;
// TODO: This may not be updated anymore :thinking:
if (state.extensions && state.extensions.cornerstone) { if (state.extensions && state.extensions.cornerstone) {
dataFromStore = state.extensions.cornerstone; dataFromStore = state.extensions.cornerstone;
} }
// TODO: This should be extension configuration
// ...dataFromStore -->
// availableTools,
// onNewImage,
// onRightClick,
// onTouchPress,
// onTouchStart,
// onMouseClick,
// If this is the active viewport, enable prefetching. // If this is the active viewport, enable prefetching.
const { viewportIndex } = ownProps; //.viewportData; const { viewportIndex } = ownProps; //.viewportData;
const isActive = viewportIndex === state.viewports.activeViewportIndex; const isActive = viewportIndex === state.viewports.activeViewportIndex;
const viewportSpecificData = const viewportSpecificData =
state.viewports.viewportSpecificData[viewportIndex] || {}; state.viewports.viewportSpecificData[viewportIndex] || {};
// CINE
let isPlaying = false;
let frameRate = 24;
if (viewportSpecificData && viewportSpecificData.cine) {
const cine = viewportSpecificData.cine;
isPlaying = cine.isPlaying === true;
frameRate = cine.cineFrameRate || frameRate;
}
return { return {
layout: state.viewports.layout, // layout: state.viewports.layout,
isActive, isActive,
// TODO: Need a cleaner and more versatile way. // TODO: Need a cleaner and more versatile way.
// Currently justing using escape hatch + commands // Currently justing using escape hatch + commands
// activeTool: activeButton && activeButton.command, // activeTool: activeButton && activeButton.command,
...dataFromStore, ...dataFromStore,
enableStackPrefetch: isActive, isStackPrefetchEnabled: isActive,
isPlaying,
frameRate,
//stack: viewportSpecificData.stack, //stack: viewportSpecificData.stack,
cineToolData: viewportSpecificData.cine, // viewport: viewportSpecificData.viewport,
viewport: viewportSpecificData.viewport,
}; };
}; };
@@ -1,10 +1,10 @@
import React, { Component } from "react"; import React, { Component } from 'react';
import ConnectedCornerstoneViewport from "./ConnectedCornerstoneViewport"; import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
import OHIF from "@ohif/core"; import OHIF from '@ohif/core';
import PropTypes from "prop-types"; import PropTypes from 'prop-types';
import cornerstone from "cornerstone-core"; import cornerstone from 'cornerstone-core';
import handleSegmentationStorage from "./handleSegmentationStorage.js"; import handleSegmentationStorage from './handleSegmentationStorage.js';
const { StackManager } = OHIF.utils; const { StackManager } = OHIF.utils;
@@ -18,7 +18,7 @@ cornerstone.metaData.addProvider(
StackManager.setMetadataProvider(metadataProvider); StackManager.setMetadataProvider(metadataProvider);
const SOP_CLASSES = { const SOP_CLASSES = {
SEGMENTATION_STORAGE: "1.2.840.10008.5.1.4.1.1.66.4" SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
}; };
const specialCaseHandlers = {}; const specialCaseHandlers = {};
@@ -28,11 +28,11 @@ specialCaseHandlers[
class OHIFCornerstoneViewport extends Component { class OHIFCornerstoneViewport extends Component {
state = { state = {
viewportData: null viewportData: null,
}; };
static defaultProps = { static defaultProps = {
customProps: {} customProps: {},
}; };
static propTypes = { static propTypes = {
@@ -40,17 +40,17 @@ class OHIFCornerstoneViewport extends Component {
displaySet: PropTypes.object, displaySet: PropTypes.object,
viewportIndex: PropTypes.number, viewportIndex: PropTypes.number,
children: PropTypes.node, children: PropTypes.node,
customProps: PropTypes.object customProps: PropTypes.object,
}; };
static id = "OHIFCornerstoneViewport"; static id = 'OHIFCornerstoneViewport';
static init() { static init() {
console.log("OHIFCornerstoneViewport init()"); console.log('OHIFCornerstoneViewport init()');
} }
static destroy() { static destroy() {
console.log("OHIFCornerstoneViewport destroy()"); console.log('OHIFCornerstoneViewport destroy()');
StackManager.clearStacks(); StackManager.clearStacks();
} }
@@ -72,15 +72,15 @@ class OHIFCornerstoneViewport extends Component {
frameIndex = 0 frameIndex = 0
) { ) {
if (!studies || !studies.length) { if (!studies || !studies.length) {
throw new Error("Studies not provided."); throw new Error('Studies not provided.');
} }
if (!studyInstanceUid) { if (!studyInstanceUid) {
throw new Error("StudyInstanceUID not provided."); throw new Error('StudyInstanceUID not provided.');
} }
if (!displaySetInstanceUid) { if (!displaySetInstanceUid) {
throw new Error("StudyInstanceUID not provided."); throw new Error('StudyInstanceUID not provided.');
} }
// Create shortcut to displaySet // Create shortcut to displaySet
@@ -89,7 +89,7 @@ class OHIFCornerstoneViewport extends Component {
); );
if (!study) { if (!study) {
throw new Error("Study not found."); throw new Error('Study not found.');
} }
const displaySet = study.displaySets.find(set => { const displaySet = study.displaySets.find(set => {
@@ -97,7 +97,7 @@ class OHIFCornerstoneViewport extends Component {
}); });
if (!displaySet) { if (!displaySet) {
throw new Error("Display Set not found."); throw new Error('Display Set not found.');
} }
// Get stack from Stack Manager // Get stack from Stack Manager
@@ -110,7 +110,7 @@ class OHIFCornerstoneViewport extends Component {
if (sopInstanceUid) { if (sopInstanceUid) {
const index = stack.imageIds.findIndex(imageId => { const index = stack.imageIds.findIndex(imageId => {
const sopCommonModule = cornerstone.metaData.get( const sopCommonModule = cornerstone.metaData.get(
"sopCommonModule", 'sopCommonModule',
imageId imageId
); );
if (!sopCommonModule) { if (!sopCommonModule) {
@@ -124,7 +124,7 @@ class OHIFCornerstoneViewport extends Component {
stack.currentImageIdIndex = index; stack.currentImageIdIndex = index;
} else { } else {
console.warn( console.warn(
"SOPInstanceUID provided was not found in specified DisplaySet" 'SOPInstanceUID provided was not found in specified DisplaySet'
); );
} }
} }
@@ -132,22 +132,6 @@ class OHIFCornerstoneViewport extends Component {
return stack; return stack;
} }
static getViewportData = (
studies,
studyInstanceUid,
displaySetInstanceUid,
sopInstanceUid,
frameIndex
) => {
return OHIFCornerstoneViewport.getCornerstoneStack(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopInstanceUid,
frameIndex
);
};
getViewportData = async ( getViewportData = async (
studies, studies,
studyInstanceUid, studyInstanceUid,
@@ -172,7 +156,7 @@ class OHIFCornerstoneViewport extends Component {
); );
break; break;
default: default:
const stack = OHIFCornerstoneViewport.getViewportData( const stack = OHIFCornerstoneViewport.getCornerstoneStack(
studies, studies,
studyInstanceUid, studyInstanceUid,
displaySetInstanceUid, displaySetInstanceUid,
@@ -183,7 +167,7 @@ class OHIFCornerstoneViewport extends Component {
viewportData = { viewportData = {
studyInstanceUid, studyInstanceUid,
displaySetInstanceUid, displaySetInstanceUid,
stack stack,
}; };
break; break;
@@ -199,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
displaySetInstanceUid, displaySetInstanceUid,
sopClassUids, sopClassUids,
sopInstanceUid, sopInstanceUid,
frameIndex frameIndex,
} = displaySet; } = displaySet;
if (!studyInstanceUid || !displaySetInstanceUid) { if (!studyInstanceUid || !displaySetInstanceUid) {
@@ -208,7 +192,7 @@ class OHIFCornerstoneViewport extends Component {
if (sopClassUids && sopClassUids.length > 1) { if (sopClassUids && sopClassUids.length > 1) {
console.warn( console.warn(
"More than one SOPClassUid in the same series is not yet supported." 'More than one SOPClassUid in the same series is not yet supported.'
); );
} }
@@ -223,7 +207,7 @@ class OHIFCornerstoneViewport extends Component {
frameIndex frameIndex
).then(viewportData => { ).then(viewportData => {
this.setState({ this.setState({
viewportData viewportData,
}); });
}); });
} }
@@ -249,25 +233,42 @@ class OHIFCornerstoneViewport extends Component {
render() { render() {
let childrenWithProps = null; let childrenWithProps = null;
if (!this.state.viewportData) {
return null;
}
const { viewportIndex } = this.props;
const {
imageIds,
currentImageIdIndex,
// If this comes from the instance, would be a better default
// `FrameTime` in the instance
// frameRate = 0,
} = this.state.viewportData.stack;
// TODO: Does it make more sense to use Context? // TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) { if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => { childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, { return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex, viewportIndex: this.props.viewportIndex,
key: index key: index,
}); });
}); });
} }
return ( return (
<> <>
{this.state.viewportData && ( <ConnectedCornerstoneViewport
<ConnectedCornerstoneViewport viewportIndex={viewportIndex}
viewportData={this.state.viewportData} imageIds={imageIds}
viewportIndex={this.props.viewportIndex} imageIdIndex={currentImageIdIndex}
{...this.props.customProps} // ~~ Connected (From REDUX)
/> // frameRate={frameRate}
)} // isPlaying={false}
// isStackPrefetchEnabled={true}
// onElementEnabled={() => {}}
// setViewportActive{() => {}}
{...this.props.customProps}
/>
{childrenWithProps} {childrenWithProps}
</> </>
); );
@@ -0,0 +1,37 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
@@ -1,10 +1,10 @@
import * as dcmjs from "dcmjs"; import * as dcmjs from 'dcmjs';
import OHIF from "@ohif/core"; import OHIF from '@ohif/core';
import cornerstone from "cornerstone-core"; import cornerstone from 'cornerstone-core';
import cornerstoneTools from "cornerstone-tools"; import cornerstoneTools from 'cornerstone-tools';
const { StackManager } = OHIF.utils; const { StackManager, DicomLoaderService } = OHIF.utils;
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) { function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
const study = studies.find( const study = studies.find(
@@ -52,14 +52,6 @@ function addSegMetadataToCornerstoneToolState(
} }
} }
function retrieveDicomData(wadoUri) {
// TODO: Authorization header depends on the server. If we ever have multiple servers
// we will need to figure out how / when to pass this information in.
return fetch(wadoUri, {
headers: OHIF.DICOMWeb.getAuthorizationHeader()
}).then(response => response.arrayBuffer());
}
async function handleSegmentationStorage( async function handleSegmentationStorage(
studies, studies,
studyInstanceUid, studyInstanceUid,
@@ -73,8 +65,11 @@ async function handleSegmentationStorage(
studyInstanceUid, studyInstanceUid,
displaySetInstanceUid displaySetInstanceUid
); );
const segWadoUri = displaySet.images[0].getData().wadouri;
const arrayBuffer = await retrieveDicomData(segWadoUri); const arrayBuffer = await DicomLoaderService.findDicomDataPromise(
displaySet,
studies
);
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer); const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict dicomData.dict
@@ -91,7 +86,7 @@ async function handleSegmentationStorage(
if (displaySets.length > 1) { if (displaySets.length > 1) {
console.warn( console.warn(
"More than one display set with the same seriesInstanceUid. This is not supported yet..." 'More than one display set with the same seriesInstanceUid. This is not supported yet...'
); );
} }
@@ -100,17 +95,19 @@ async function handleSegmentationStorage(
const results = parseSeg(arrayBuffer, imageIds); const results = parseSeg(arrayBuffer, imageIds);
if (!results) { if (!results) {
throw new Error("Fractional segmentations are not supported"); throw new Error('Fractional segmentations are not supported');
} }
const { segMetadata, toolState } = results; const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
const { setters } = cornerstoneTools.getModule('segmentation');
segMetadata.seriesInstanceUid = seriesInstanceUid; setters.labelmap3DByFirstImageId(
imageIds[0],
addSegMetadataToCornerstoneToolState( labelmapBuffer,
0, // TODO -> Can define a color LUT based on colors in the SEG later.
segMetadata, segMetadata,
toolState, imageIds.length,
displaySetInstanceUid segmentsOnFrame
); );
const cachedStack = StackManager.findOrCreateStack( const cachedStack = StackManager.findOrCreateStack(
@@ -118,12 +115,13 @@ async function handleSegmentationStorage(
referenceDisplaySet referenceDisplaySet
); );
const stack = Object.assign({}, cachedStack); const stack = Object.assign({}, cachedStack);
stack.currentImageIdIndex = 0; stack.currentImageIdIndex = 0;
return { return {
studyInstanceUid, studyInstanceUid,
displaySetInstanceUid, displaySetInstanceUid,
stack stack,
}; };
} }
+17 -1
View File
@@ -1,7 +1,14 @@
import OHIFCornerstoneViewport from './OHIFCornerstoneViewport.js'; import init from './init.js';
import asyncComponent from './asyncComponent.js';
import commandsModule from './commandsModule.js'; import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js'; import toolbarModule from './toolbarModule.js';
const OHIFCornerstoneViewport = asyncComponent(() =>
import(
/* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'
)
);
/** /**
* *
*/ */
@@ -11,6 +18,15 @@ export default {
*/ */
id: 'cornerstone', id: 'cornerstone',
/**
*
*
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration(configuration = {}) {
init(configuration);
},
getViewportModule() { getViewportModule() {
return OHIFCornerstoneViewport; return OHIFCornerstoneViewport;
}, },
+87
View File
@@ -0,0 +1,87 @@
import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
/**
*
* @param {object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init(configuration = {}) {
const { csToolsConfig } = configuration;
const { StackManager } = OHIF.utils;
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
// ~~ Set our MetadataProvider
cornerstone.metaData.addProvider(
metadataProvider.provider.bind(metadataProvider)
);
StackManager.setMetadataProvider(metadataProvider);
// ~~
const defaultCsToolsConfig = csToolsConfig || {
globalToolSyncEnabled: true,
showSVGCursors: true,
autoResizeViewports: false,
};
initCornerstoneTools(defaultCsToolsConfig);
// ~~ Toooools 🙌
const {
PanTool,
ZoomTool,
WwwcTool,
MagnifyTool,
StackScrollTool,
StackScrollMouseWheelTool,
// Touch
PanMultiTouchTool,
ZoomTouchPinchTool,
// Annotations
EraserTool,
ArrowAnnotateTool,
BidirectionalTool,
LengthTool,
AngleTool,
FreehandRoiTool,
EllipticalRoiTool,
DragProbeTool,
RectangleRoiTool,
// Segmentation
BrushTool,
} = csTools;
const tools = [
PanTool,
ZoomTool,
WwwcTool,
MagnifyTool,
StackScrollTool,
StackScrollMouseWheelTool,
// Touch
PanMultiTouchTool,
ZoomTouchPinchTool,
// Annotations
EraserTool,
ArrowAnnotateTool,
BidirectionalTool,
LengthTool,
AngleTool,
FreehandRoiTool,
EllipticalRoiTool,
DragProbeTool,
RectangleRoiTool,
// Segmentation
BrushTool,
];
tools.forEach(tool => csTools.addTool(tool));
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required
csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
csTools.setToolActive('ZoomTouchPinch', {});
}
@@ -69,6 +69,15 @@ const definitions = [
commandName: 'setToolActive', commandName: 'setToolActive',
commandOptions: { toolName: 'Length' }, commandOptions: { toolName: 'Length' },
}, },
{
id: 'ArrowAnnotate',
label: 'Annotate',
icon: 'measure-non-target',
//
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'ArrowAnnotate' },
},
{ {
id: 'Angle', id: 'Angle',
label: 'Angle', label: 'Angle',
@@ -186,6 +195,34 @@ const definitions = [
type: TOOLBAR_BUTTON_TYPES.COMMAND, type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'clearAnnotations', commandName: 'clearAnnotations',
}, },
{
id: 'Bidirectional',
label: 'Bidirectional',
icon: 'measure-target',
//
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'Bidirectional' },
},
{
id: 'Eraser',
label: 'Eraser',
icon: 'eraser',
//
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'Eraser' },
},
{
id: 'Download',
label: 'Download',
icon: 'create-screen-capture',
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'DOWNLOAD_SCREEN_SHOT',
},
},
], ],
}, },
]; ];
@@ -1,3 +1,4 @@
const webpack = require('webpack');
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
@@ -29,9 +30,15 @@ module.exports = (env, argv) => {
}, },
output: { output: {
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohifExtensionDicomHtml', library: 'OHIFExtDicomHtml',
libraryTarget: 'umd', libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main, filename: pkg.main,
}, },
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
}); });
}; };
+65
View File
@@ -3,6 +3,71 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.0...@ohif/extension-dicom-html@1.0.1) (2019-10-29)
### Bug Fixes
* Set SR viewport as active by interaction ([#1118](https://github.com/OHIF/Viewers/issues/1118)) ([5b33417](https://github.com/OHIF/Viewers/commit/5b334175c370afb930b4b6dbd307ddece8f850e3))
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.8...@ohif/extension-dicom-html@1.0.0) (2019-09-27)
### Bug Fixes
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
### BREAKING CHANGES
* DICOM Seg
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.7...@ohif/extension-dicom-html@0.50.8) (2019-09-26)
### Bug Fixes
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.6...@ohif/extension-dicom-html@0.50.7) (2019-09-12)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.5...@ohif/extension-dicom-html@0.50.6) (2019-09-10)
### Bug Fixes
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.4...@ohif/extension-dicom-html@0.50.5) (2019-09-09)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.3...@ohif/extension-dicom-html@0.50.4) (2019-09-06) ## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.3...@ohif/extension-dicom-html@0.50.4) (2019-09-06)
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-dicom-html", "name": "@ohif/extension-dicom-html",
"version": "0.50.4", "version": "1.0.1",
"description": "OHIF extension for rendering structured reports to HTML", "description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -29,10 +29,10 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"dcmjs": "^0.3.3", "dcmjs": "^0.6.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^15.0.0 || ^16.0.0", "react": "^16.8.6",
"react-dom": "^15.0.0 || ^16.0.0" "react-dom": "^16.8.6"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5" "@babel/runtime": "^7.5.5"
@@ -0,0 +1,33 @@
import OHIF from '@ohif/core';
import { connect } from 'react-redux';
import DicomHtmlViewport from './DicomHtmlViewport';
const { setViewportActive } = OHIF.redux.actions;
const mapStateToProps = (state, ownProps) => {
const { viewportIndex, byteArray } = ownProps;
const activeViewportIndex = state.viewports;
return {
viewportIndex,
activeViewportIndex,
byteArray,
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
const { viewportIndex } = ownProps;
return {
setViewportActive: () => {
dispatch(setViewportActive(viewportIndex));
},
};
};
const ConnectedDicomHtmlViewport = connect(
mapStateToProps,
mapDispatchToProps
)(DicomHtmlViewport);
export default ConnectedDicomHtmlViewport;
+36 -16
View File
@@ -1,6 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as dcmjs from 'dcmjs'; import * as dcmjs from 'dcmjs';
import TypedArrayProp from './TypedArrayProp'; import TypedArrayProp from './TypedArrayProp';
import './DicomHtmlViewport.css'; import './DicomHtmlViewport.css';
function getRelationshipString(data) { function getRelationshipString(data) {
@@ -30,7 +32,7 @@ function getValueString(data) {
const { const {
CodeMeaning, CodeMeaning,
CodeValue, CodeValue,
CodingSchemeDesignator CodingSchemeDesignator,
} = data.ConceptNameCodeSequence; } = data.ConceptNameCodeSequence;
return `${CodeMeaning} (${CodeValue}, ${CodingSchemeDesignator})`; return `${CodeMeaning} (${CodeValue}, ${CodingSchemeDesignator})`;
@@ -75,7 +77,7 @@ function constructContentSequence(data, header) {
} }
const result = { const result = {
items items,
}; };
if (header) { if (header) {
@@ -106,9 +108,7 @@ const { DicomMetaDictionary, DicomMessage } = dcmjs.data;
function getMainData(data) { function getMainData(data) {
const root = []; const root = [];
const patientValue = `${data.PatientName} (${data.PatientSex}, #${ const patientValue = `${data.PatientName} (${data.PatientSex}, #${data.PatientID})`;
data.PatientID
})`;
root.push(getMainDataItem('Patient', patientValue)); root.push(getMainDataItem('Patient', patientValue));
const studyValue = data.StudyDescription; const studyValue = data.StudyDescription;
@@ -117,15 +117,13 @@ function getMainData(data) {
const seriesValue = `${data.SeriesDescription} (#${data.SeriesNumber})`; const seriesValue = `${data.SeriesDescription} (#${data.SeriesNumber})`;
root.push(getMainDataItem('Series', seriesValue)); root.push(getMainDataItem('Series', seriesValue));
const manufacturerValue = `${data.Manufacturer} (${ const manufacturerValue = `${data.Manufacturer} (${data.ManufacturerModelName}, #${data.DeviceSerialNumber})`;
data.ManufacturerModelName
}, #${data.DeviceSerialNumber})`;
root.push(getMainDataItem('Manufacturer', manufacturerValue)); root.push(getMainDataItem('Manufacturer', manufacturerValue));
const mainDataObjects = { const mainDataObjects = {
CompletionFlag: 'Completion flag', CompletionFlag: 'Completion flag',
VerificationFlag: 'Verification flag' VerificationFlag: 'Verification flag',
}; };
Object.keys(mainDataObjects).forEach(key => { Object.keys(mainDataObjects).forEach(key => {
@@ -154,7 +152,7 @@ const getContentSequence = (data, level = 1) => {
const { const {
CodeMeaning, CodeMeaning,
CodeValue, CodeValue,
CodingSchemeDesignator CodingSchemeDesignator,
} = data.ConceptNameCodeSequence; } = data.ConceptNameCodeSequence;
header = `${CodeMeaning} (${CodeValue} - ${CodingSchemeDesignator})`; header = `${CodeMeaning} (${CodeValue} - ${CodingSchemeDesignator})`;
@@ -198,11 +196,14 @@ function getMainDataItem(key, value) {
class DicomHtmlViewport extends Component { class DicomHtmlViewport extends Component {
state = { state = {
content: null, content: null,
error: null error: null,
}; };
static propTypes = { static propTypes = {
byteArray: TypedArrayProp.uint8 byteArray: TypedArrayProp.uint8,
setViewportActive: PropTypes.func.isRequired,
viewportIndex: PropTypes.number.isRequired,
activeViewportIndex: PropTypes.number.isRequired,
}; };
componentDidMount() { componentDidMount() {
@@ -225,15 +226,34 @@ class DicomHtmlViewport extends Component {
); );
this.setState({ this.setState({
content content,
}); });
} }
setViewportActiveHandler = () => {
const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
if (viewportIndex !== activeViewportIndex) {
setViewportActive();
}
};
render() { render() {
const { content, error } = this.state;
return ( return (
<div className={'DicomHtmlViewport'}> <div
{this.state.content} data-cy="dicom-html-viewport"
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>} className="DicomHtmlViewport"
onClick={this.setViewportActiveHandler}
onScroll={this.setViewportActiveHandler}
>
{content}
{error && <h2>{JSON.stringify(error)}</h2>}
</div> </div>
); );
} }
@@ -1,46 +1,36 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import DicomHtmlViewport from './DicomHtmlViewport'; import OHIF from '@ohif/core';
import ConnectedDicomHtmlViewport from './ConnectedDicomHtmlViewport';
const { DicomLoaderService } = OHIF.utils;
class OHIFDicomHtmlViewport extends Component { class OHIFDicomHtmlViewport extends Component {
static propTypes = { static propTypes = {
studies: PropTypes.object, studies: PropTypes.object,
displaySet: PropTypes.object, displaySet: PropTypes.object,
viewportIndex: PropTypes.number viewportIndex: PropTypes.number,
viewportData: PropTypes.object,
}; };
state = { state = {
byteArray: null, byteArray: null,
error: null error: null,
}; };
componentDidMount() { componentDidMount() {
const { displaySet } = this.props.viewportData; const { displaySet, studies } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
} = displaySet;
this.retrieveDicomData( DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
studyInstanceUid, data => {
seriesInstanceUid, const byteArray = new Uint8Array(data);
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
).then(
byteArray => {
this.setState({ this.setState({
byteArray byteArray: byteArray,
}); });
}, },
error => { error => {
this.setState({ this.setState({
error error,
}); });
throw new Error(error); throw new Error(error);
@@ -48,32 +38,14 @@ class OHIFDicomHtmlViewport extends Component {
); );
} }
retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
) {
// TODO: Passing in a lot of data we aren't using
// TODO: Authorization header depends on the server. If we ever have multiple servers
// we will need to figure out how / when to pass this information in.
return fetch(wadoUri, {
headers: authorizationHeaders
})
.then(response => response.arrayBuffer())
.then(arraybuffer => {
return new Uint8Array(arraybuffer);
});
}
render() { render() {
return ( return (
<> <>
{this.state.byteArray && ( {this.state.byteArray && (
<DicomHtmlViewport byteArray={this.state.byteArray} /> <ConnectedDicomHtmlViewport
byteArray={this.state.byteArray}
viewportIndex={this.props.viewportIndex}
/>
)} )}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>} {this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
</> </>
@@ -0,0 +1,37 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
+8 -2
View File
@@ -1,5 +1,11 @@
import asyncComponent from './asyncComponent.js';
import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js'; import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js';
import OHIFDicomHtmlViewport from './OHIFDicomHtmlViewport.js';
const OHIFDicomHtmlViewport = asyncComponent(() =>
import(
/* webpackChunkName: "OHIFDicomHtmlViewport" */ './OHIFDicomHtmlViewport.js'
)
);
export default { export default {
/** /**
@@ -12,5 +18,5 @@ export default {
}, },
getSopClassHandlerModule() { getSopClassHandlerModule() {
return OHIFDicomHtmlSopClassHandler; return OHIFDicomHtmlSopClassHandler;
} },
}; };
@@ -1,3 +1,4 @@
const webpack = require('webpack');
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
@@ -29,9 +30,15 @@ module.exports = (env, argv) => {
}, },
output: { output: {
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohifExtensionDicomMicroscopy', library: 'OHIFExtDicomMicroscopy',
libraryTarget: 'umd', libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main, filename: pkg.main,
}, },
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
]
}); });
}; };
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.5...@ohif/extension-dicom-microscopy@0.50.6) (2019-09-26)
### Bug Fixes
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.4...@ohif/extension-dicom-microscopy@0.50.5) (2019-09-10)
### Bug Fixes
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.3...@ohif/extension-dicom-microscopy@0.50.4) (2019-09-04) ## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.3...@ohif/extension-dicom-microscopy@0.50.4) (2019-09-04)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy **Note:** Version bump only for package @ohif/extension-dicom-microscopy
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-dicom-microscopy", "name": "@ohif/extension-dicom-microscopy",
"version": "0.50.4", "version": "0.50.6",
"description": "OHIF extension for Dicom Microscopy", "description": "OHIF extension for Dicom Microscopy",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -29,8 +29,8 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"react": "^15.0.0 || ^16.0.0", "react": "^16.8.6",
"react-dom": "^15.0.0 || ^16.0.0" "react-dom": "^16.8.6"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
@@ -1,10 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactResizeDetector from 'react-resize-detector'; import ReactResizeDetector from 'react-resize-detector';
import { api } from 'dicom-microscopy-viewer';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
class DicomMicroscopyViewport extends Component { class DicomMicroscopyViewport extends Component {
state = { state = {
error: null, error: null,
@@ -56,9 +53,12 @@ class DicomMicroscopyViewport extends Component {
} }
return Promise.all(promises); return Promise.all(promises);
}) })
.then(metadata => { .then(async metadata => {
metadata = metadata.filter(m => m); metadata = metadata.filter(m => m);
const { api } = await import(/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer');
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
this.viewer = new microscopyViewer({ this.viewer = new microscopyViewer({
client: dicomWebClient, client: dicomWebClient,
metadata, metadata,
@@ -0,0 +1,37 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
+7 -1
View File
@@ -1,5 +1,11 @@
import asyncComponent from './asyncComponent.js';
import DicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler.js'; import DicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler.js';
import DicomMicroscopyViewport from './DicomMicroscopyViewport.js';
const DicomMicroscopyViewport = asyncComponent(() =>
import(
/* webpackChunkName: "DicomMicroscopyViewport" */ './DicomMicroscopyViewport.js'
)
);
export default { export default {
/** /**
@@ -29,8 +29,9 @@ module.exports = (env, argv) => {
}, },
output: { output: {
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohifExtensionDicomPdf', library: 'OHIFExtDicomPdf',
libraryTarget: 'umd', libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main, filename: pkg.main,
}, },
}); });
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.4...@ohif/extension-dicom-pdf@1.0.0) (2019-09-27)
### Bug Fixes
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
### BREAKING CHANGES
* DICOM Seg
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.3...@ohif/extension-dicom-pdf@0.50.4) (2019-09-10)
### Bug Fixes
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.2...@ohif/extension-dicom-pdf@0.50.3) (2019-09-04) ## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.2...@ohif/extension-dicom-pdf@0.50.3) (2019-09-04)
**Note:** Version bump only for package @ohif/extension-dicom-pdf **Note:** Version bump only for package @ohif/extension-dicom-pdf
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-dicom-pdf", "name": "@ohif/extension-dicom-pdf",
"version": "0.50.3", "version": "1.0.0",
"description": "OHIF extension for Dicom PDF", "description": "OHIF extension for Dicom PDF",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -31,8 +31,8 @@
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^15.0.0 || ^16.0.0", "react": "^16.8.6",
"react-dom": "^15.0.0 || ^16.0.0" "react-dom": "^16.8.6"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
+20 -15
View File
@@ -4,41 +4,46 @@ import TypedArrayProp from './TypedArrayProp';
// TODO: Should probably use dcmjs for this // TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = { const SOP_CLASS_UIDS = {
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1' ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1',
}; };
class DicomPDFViewport extends Component { class DicomPDFViewport extends Component {
state = { state = {
fileURL: null, fileURL: null,
error: null error: null,
}; };
static propTypes = { static propTypes = {
byteArray: TypedArrayProp.uint8 byteArray: TypedArrayProp.uint8,
}; };
renderPDF = dataSet => { renderPDF = (dataSet, byteArray) => {
const sopClassUid = dataSet.string('x00080016'); let pdfByteArray = byteArray;
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) { if (dataSet) {
throw new Error('This is not a DICOM-encapsulated PDF'); const sopClassUid = dataSet.string('x00080016');
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
throw new Error('This is not a DICOM-encapsulated PDF');
}
const fileTag = dataSet.elements.x00420011;
const offset = fileTag.dataOffset;
const remainder = offset + fileTag.length;
pdfByteArray = dataSet.byteArray.slice(offset, remainder);
} }
const fileTag = dataSet.elements.x00420011;
const offset = fileTag.dataOffset;
const remainder = offset + fileTag.length;
const pdfByteArray = dataSet.byteArray.slice(offset, remainder);
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' }); const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(PDF); const fileURL = URL.createObjectURL(PDF);
this.setState({ this.setState({
fileURL fileURL,
}); });
}; };
parseByteArray = byteArray => { parseByteArray = byteArray => {
const options = { const options = {
untilTag: '' untilTag: '',
}; };
let dataSet; let dataSet;
@@ -47,7 +52,7 @@ class DicomPDFViewport extends Component {
dataSet = dicomParser.parseDicom(byteArray, options); dataSet = dicomParser.parseDicom(byteArray, options);
} catch (error) { } catch (error) {
this.setState({ this.setState({
error error,
}); });
} }
@@ -57,7 +62,7 @@ class DicomPDFViewport extends Component {
componentDidMount() { componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray); const dataSet = this.parseByteArray(this.props.byteArray);
this.renderPDF(dataSet); this.renderPDF(dataSet, this.props.byteArray);
} }
render() { render() {
@@ -1,60 +1,54 @@
import React, { Component } from "react"; import React, { Component } from 'react';
import PropTypes from "prop-types"; import PropTypes from 'prop-types';
import OHIF from "@ohif/core"; import OHIF from '@ohif/core';
import OHIFComponentPlugin from "./OHIFComponentPlugin.js"; import OHIFComponentPlugin from './OHIFComponentPlugin.js';
import DicomPDFViewport from "./DicomPDFViewport"; import DicomPDFViewport from './DicomPDFViewport';
const { DICOMWeb } = OHIF; const { DicomLoaderService } = OHIF.utils;
class OHIFDicomPDFViewport extends Component { class OHIFDicomPDFViewport extends Component {
static propTypes = { static propTypes = {
studies: PropTypes.object, studies: PropTypes.object,
displaySet: PropTypes.object, displaySet: PropTypes.object,
viewportIndex: PropTypes.number viewportIndex: PropTypes.number,
}; };
state = { state = {
byteArray: null, byteArray: null,
error: null error: null,
}; };
static id = "DicomPDFViewportPDF"; static id = 'DicomPDFViewportPDF';
static init() { static init() {
console.log("DicomPDFViewport init()"); console.log('DicomPDFViewport init()');
} }
static destroy() { static destroy() {
console.log("DicomPDFViewport destroy()"); console.log('DicomPDFViewport destroy()');
} }
componentDidMount() { componentDidMount() {
const { displaySet } = this.props.viewportData; const { displaySet, studies } = this.props.viewportData;
const { const {
studyInstanceUid, studyInstanceUid,
seriesInstanceUid, seriesInstanceUid,
sopInstanceUid, sopInstanceUid,
wadoRoot, wadoRoot,
wadoUri, wadoUri,
authorizationHeaders authorizationHeaders,
} = displaySet; } = displaySet;
this.retrieveDicomData( DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
studyInstanceUid, data => {
seriesInstanceUid, const byteArray = new Uint8Array(data);
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
).then(
byteArray => {
this.setState({ this.setState({
byteArray byteArray: byteArray,
}); });
}, },
error => { error => {
this.setState({ this.setState({
error error,
}); });
throw new Error(error); throw new Error(error);
@@ -62,27 +56,6 @@ class OHIFDicomPDFViewport extends Component {
); );
} }
retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
) {
// TODO: Passing in a lot of data we aren't using
// TODO: Authorization header depends on the server. If we ever have multiple servers
// we will need to figure out how / when to pass this information in.
return fetch(wadoUri, {
headers: authorizationHeaders
})
.then(response => response.arrayBuffer())
.then(arraybuffer => {
return new Uint8Array(arraybuffer);
});
}
render() { render() {
const { id, init, destroy } = OHIFDicomPDFViewport; const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy }; const pluginProps = { id, init, destroy };
+8 -1
View File
@@ -1,3 +1,4 @@
const webpack = require('webpack');
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
@@ -29,9 +30,15 @@ module.exports = (env, argv) => {
}, },
output: { output: {
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohifExtensionVtk', library: 'OHIFExtVtk',
libraryTarget: 'umd', libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main, filename: pkg.main,
}, },
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
}); });
}; };
+490
View File
@@ -3,6 +3,496 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.53.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.0...@ohif/extension-vtk@0.53.1) (2019-11-08)
**Note:** Version bump only for package @ohif/extension-vtk
# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.32...@ohif/extension-vtk@0.53.0) (2019-11-06)
### Features
* modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1146](https://github.com/OHIF/Viewers/issues/1146) [#1142](https://github.com/OHIF/Viewers/issues/1142) [#1143](https://github.com/OHIF/Viewers/issues/1143) [#1110](https://github.com/OHIF/Viewers/issues/1110) [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1119](https://github.com/OHIF/Viewers/issues/1119)
## [0.52.32](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.31...@ohif/extension-vtk@0.52.32) (2019-11-05)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.31](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.30...@ohif/extension-vtk@0.52.31) (2019-11-05)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.30](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.29...@ohif/extension-vtk@0.52.30) (2019-11-04)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.29](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.28...@ohif/extension-vtk@0.52.29) (2019-11-04)
### Bug Fixes
* 🐛 Upgrade react-vtkjs-viewport to fix volume orientation ([#1143](https://github.com/OHIF/Viewers/issues/1143)) ([1d86b1a](https://github.com/OHIF/Viewers/commit/1d86b1a102fc422053cff103de8c12ca6f437354))
## [0.52.28](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.27...@ohif/extension-vtk@0.52.28) (2019-11-04)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.27](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.26...@ohif/extension-vtk@0.52.27) (2019-10-31)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.26](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.25...@ohif/extension-vtk@0.52.26) (2019-10-30)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.25](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.24...@ohif/extension-vtk@0.52.25) (2019-10-29)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.24](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.23...@ohif/extension-vtk@0.52.24) (2019-10-29)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.22...@ohif/extension-vtk@0.52.23) (2019-10-28)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.21...@ohif/extension-vtk@0.52.22) (2019-10-28)
### Bug Fixes
* MIP styling ([#1109](https://github.com/OHIF/Viewers/issues/1109)) ([0d21cc6](https://github.com/OHIF/Viewers/commit/0d21cc6ad0c47706b9e62e05fe2a0f1d86339760))
## [0.52.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.20...@ohif/extension-vtk@0.52.21) (2019-10-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.19...@ohif/extension-vtk@0.52.20) (2019-10-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.18...@ohif/extension-vtk@0.52.19) (2019-10-25)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.17...@ohif/extension-vtk@0.52.18) (2019-10-25)
### Bug Fixes
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
## [0.52.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.16...@ohif/extension-vtk@0.52.17) (2019-10-25)
### Bug Fixes
* 🐛 Fix crosshairs location when using MIP ([#1080](https://github.com/OHIF/Viewers/issues/1080)) ([b451ce4](https://github.com/OHIF/Viewers/commit/b451ce440796f13b0891739d2130e8ee71993d3d))
## [0.52.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.15...@ohif/extension-vtk@0.52.16) (2019-10-23)
### Bug Fixes
* 🐛 Switch to orhtographic view for 2D MPR ([#1074](https://github.com/OHIF/Viewers/issues/1074)) ([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08))
## [0.52.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.14...@ohif/extension-vtk@0.52.15) (2019-10-23)
### Performance Improvements
* ⚡️ Throttle UI refresh of synced WL value in vtk port ([#1070](https://github.com/OHIF/Viewers/issues/1070)) ([9d08e81](https://github.com/OHIF/Viewers/commit/9d08e81aa5def63a5e34f464aae099f7c29f0ac5))
## [0.52.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.13...@ohif/extension-vtk@0.52.14) (2019-10-22)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.12...@ohif/extension-vtk@0.52.13) (2019-10-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.11...@ohif/extension-vtk@0.52.12) (2019-10-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.10...@ohif/extension-vtk@0.52.11) (2019-10-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.9...@ohif/extension-vtk@0.52.10) (2019-10-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.8...@ohif/extension-vtk@0.52.9) (2019-10-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.7...@ohif/extension-vtk@0.52.8) (2019-10-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.6...@ohif/extension-vtk@0.52.7) (2019-10-11)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.5...@ohif/extension-vtk@0.52.6) (2019-10-11)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.4...@ohif/extension-vtk@0.52.5) (2019-10-10)
### Performance Improvements
* ⚡️ Update react-vtkjs-viewport for performance improvement ([6ee5094](https://github.com/OHIF/Viewers/commit/6ee5094))
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.3...@ohif/extension-vtk@0.52.4) (2019-10-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.2...@ohif/extension-vtk@0.52.3) (2019-10-09)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.1...@ohif/extension-vtk@0.52.2) (2019-10-09)
### Performance Improvements
* 🎸 Update vtk viewport for faster image reconstruction ([#1016](https://github.com/OHIF/Viewers/issues/1016)) ([8513ad9](https://github.com/OHIF/Viewers/commit/8513ad9))
## [0.52.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.0...@ohif/extension-vtk@0.52.1) (2019-10-04)
**Note:** Version bump only for package @ohif/extension-vtk
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.3...@ohif/extension-vtk@0.52.0) (2019-10-03)
### Features
* 🎸 Synced Window Leveling ([559b637](https://github.com/OHIF/Viewers/commit/559b637)), closes [#558](https://github.com/OHIF/Viewers/issues/558)
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.2...@ohif/extension-vtk@0.51.3) (2019-10-03)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.1...@ohif/extension-vtk@0.51.2) (2019-10-02)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.51.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.0...@ohif/extension-vtk@0.51.1) (2019-10-01)
### Bug Fixes
* **rotate:** Use new MPR rotate in react-vtkjs-viewport 0.1.2 ([#964](https://github.com/OHIF/Viewers/issues/964)) ([8503cbc](https://github.com/OHIF/Viewers/commit/8503cbc))
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.23...@ohif/extension-vtk@0.51.0) (2019-10-01)
### Features
* 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
## [0.50.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.22...@ohif/extension-vtk@0.50.23) (2019-10-01)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.21...@ohif/extension-vtk@0.50.22) (2019-09-27)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.20...@ohif/extension-vtk@0.50.21) (2019-09-27)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.19...@ohif/extension-vtk@0.50.20) (2019-09-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.18...@ohif/extension-vtk@0.50.19) (2019-09-26)
### Bug Fixes
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
## [0.50.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.17...@ohif/extension-vtk@0.50.18) (2019-09-23)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.16...@ohif/extension-vtk@0.50.17) (2019-09-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.15...@ohif/extension-vtk@0.50.16) (2019-09-19)
### Bug Fixes
* Set transfer function range to data range when loading volumes in VTK ([#930](https://github.com/OHIF/Viewers/issues/930)) ([2341b32](https://github.com/OHIF/Viewers/commit/2341b32))
## [0.50.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.14...@ohif/extension-vtk@0.50.15) (2019-09-19)
### Bug Fixes
* Bump VTK viewport dependency ([#929](https://github.com/OHIF/Viewers/issues/929)) ([66a0aad](https://github.com/OHIF/Viewers/commit/66a0aad))
* Stop adding empty labelmap to VTK Viewports for now. Wait until paint tools are implemented ([#926](https://github.com/OHIF/Viewers/issues/926)) ([7f642a0](https://github.com/OHIF/Viewers/commit/7f642a0))
## [0.50.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.13...@ohif/extension-vtk@0.50.14) (2019-09-17)
### Bug Fixes
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
## [0.50.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.12...@ohif/extension-vtk@0.50.13) (2019-09-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.11...@ohif/extension-vtk@0.50.12) (2019-09-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.10...@ohif/extension-vtk@0.50.11) (2019-09-10)
### Bug Fixes
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.9...@ohif/extension-vtk@0.50.10) (2019-09-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.8...@ohif/extension-vtk@0.50.9) (2019-09-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.7...@ohif/extension-vtk@0.50.8) (2019-09-09)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.6...@ohif/extension-vtk@0.50.7) (2019-09-06) ## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.6...@ohif/extension-vtk@0.50.7) (2019-09-06)
**Note:** Version bump only for package @ohif/extension-vtk **Note:** Version bump only for package @ohif/extension-vtk
+8 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-vtk", "name": "@ohif/extension-vtk",
"version": "0.50.7", "version": "0.53.1",
"description": "OHIF extension for VTK.js", "description": "OHIF extension for VTK.js",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -34,7 +34,7 @@
"@ohif/ui": "^0.50.0", "@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.4.7", "dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"i18next": "^17.0.3", "i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
@@ -48,14 +48,15 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"react-vtkjs-viewport": "0.0.11" "lodash.throttle": "^4.1.1",
"react-vtkjs-viewport": "^0.3.9"
}, },
"devDependencies": { "devDependencies": {
"@ohif/core": "^0.50.5", "@ohif/core": "^1.7.0",
"@ohif/ui": "^0.50.4", "@ohif/ui": "^0.60.1",
"cornerstone-tools": "^3.13.0", "cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.5.1", "dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1", "gh-pages": "^2.0.1",
"i18next": "^17.0.3", "i18next": "^17.0.3",
+7 -7
View File
@@ -1,6 +1,6 @@
import OHIF from "@ohif/core"; import OHIF from '@ohif/core';
import { View2D } from "react-vtkjs-viewport"; import { View2D } from 'react-vtkjs-viewport';
import { connect } from "react-redux"; import { connect } from 'react-redux';
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions; const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
@@ -24,7 +24,7 @@ const mapStateToProps = (state, ownProps) => {
// Hopefully this doesn't break anything under the hood for this one // Hopefully this doesn't break anything under the hood for this one
// activeTool: activeButton && activeButton.command, // activeTool: activeButton && activeButton.command,
...dataFromStore, ...dataFromStore,
enableStackPrefetch: isActive enableStackPrefetch: isActive,
}; };
}; };
@@ -38,7 +38,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
setViewportSpecificData: data => { setViewportSpecificData: data => {
dispatch(setViewportSpecificData(viewportIndex, data)); dispatch(setViewportSpecificData(viewportIndex, data));
} },
}; };
}; };
@@ -63,10 +63,10 @@ const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
// Store the API details for later // Store the API details for later
//setViewportSpecificData({ vtkApi: api }); //setViewportSpecificData({ vtkApi: api });
if (afterCreation && typeof afterCreation === "function") { if (afterCreation && typeof afterCreation === 'function') {
afterCreation(api); afterCreation(api);
} }
} },
}; };
return props; return props;
}; };
+4 -4
View File
@@ -3,15 +3,15 @@
} }
.loadingIndicator { .loadingIndicator {
background-color: rgba(0, 0, 0, 0.75); background-color: rgba(0, 0, 0, 0);
font-size: 18px; font-size: 8px;
height: 100%; height: 20%;
overflow: hidden; overflow: hidden;
position: absolute; position: absolute;
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
/* Necessary for click-through to cornerstone element below */ /* Necessary for click-through to vtkjs element below */
pointer-events: none; pointer-events: none;
} }
+4 -3
View File
@@ -1,7 +1,7 @@
import './LoadingIndicator.css'; import './LoadingIndicator.css';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { withTranslation } from 'react-i18next';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
class LoadingIndicator extends PureComponent { class LoadingIndicator extends PureComponent {
@@ -35,7 +35,8 @@ class LoadingIndicator extends PureComponent {
<div className="imageViewerLoadingIndicator loadingIndicator"> <div className="imageViewerLoadingIndicator loadingIndicator">
<div className="indicatorContents"> <div className="indicatorContents">
<p> <p>
Loading... <i className="fa fa-spin fa-circle-o-notch fa-fw" />{' '} {this.props.t('Reformatting')}...
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
{percComplete} {percComplete}
</p> </p>
</div> </div>
@@ -46,4 +47,4 @@ class LoadingIndicator extends PureComponent {
} }
} }
export default LoadingIndicator; export default withTranslation('Common')(LoadingIndicator);
+145 -69
View File
@@ -1,16 +1,16 @@
import React, { Component } from "react"; import React, { Component } from 'react';
import { getImageData, loadImageData } from "react-vtkjs-viewport"; import { getImageData, loadImageData } from 'react-vtkjs-viewport';
import ConnectedVTKViewport from "./ConnectedVTKViewport"; import ConnectedVTKViewport from './ConnectedVTKViewport';
import LoadingIndicator from "./LoadingIndicator.js"; import LoadingIndicator from './LoadingIndicator.js';
import OHIF from "@ohif/core"; import OHIF from '@ohif/core';
import PropTypes from "prop-types"; import PropTypes from 'prop-types';
import cornerstone from "cornerstone-core"; import cornerstone from 'cornerstone-core';
import handleSegmentationStorage from "./handleSegmentationStorage.js"; import handleSegmentationStorage from './handleSegmentationStorage.js';
import vtkDataArray from "vtk.js/Sources/Common/Core/DataArray"; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageData from "vtk.js/Sources/Common/DataModel/ImageData"; import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkVolume from "vtk.js/Sources/Rendering/Core/Volume"; import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
import vtkVolumeMapper from "vtk.js/Sources/Rendering/Core/VolumeMapper"; import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
const { StackManager } = OHIF.utils; const { StackManager } = OHIF.utils;
@@ -24,7 +24,7 @@ cornerstone.metaData.addProvider(
StackManager.setMetadataProvider(metadataProvider); StackManager.setMetadataProvider(metadataProvider);
const SOP_CLASSES = { const SOP_CLASSES = {
SEGMENTATION_STORAGE: "1.2.840.10008.5.1.4.1.1.66.4" SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
}; };
const specialCaseHandlers = {}; const specialCaseHandlers = {};
@@ -42,7 +42,7 @@ const volumeCache = {};
*/ */
function createLabelMapImageData(backgroundImageData) { function createLabelMapImageData(backgroundImageData) {
const labelMapData = vtkImageData.newInstance( const labelMapData = vtkImageData.newInstance(
backgroundImageData.get("spacing", "origin", "direction") backgroundImageData.get('spacing', 'origin', 'direction')
); );
labelMapData.setDimensions(backgroundImageData.getDimensions()); labelMapData.setDimensions(backgroundImageData.getDimensions());
labelMapData.computeTransforms(); labelMapData.computeTransforms();
@@ -50,7 +50,7 @@ function createLabelMapImageData(backgroundImageData) {
const values = new Uint8Array(backgroundImageData.getNumberOfPoints()); const values = new Uint8Array(backgroundImageData.getNumberOfPoints());
const dataArray = vtkDataArray.newInstance({ const dataArray = vtkDataArray.newInstance({
numberOfComponents: 1, // labelmap with single component numberOfComponents: 1, // labelmap with single component
values values,
}); });
labelMapData.getPointData().setScalars(dataArray); labelMapData.getPointData().setScalars(dataArray);
@@ -61,24 +61,24 @@ class OHIFVTKViewport extends Component {
state = { state = {
volumes: null, volumes: null,
paintFilterLabelMapImageData: null, paintFilterLabelMapImageData: null,
paintFilterBackgroundImageData: null paintFilterBackgroundImageData: null,
}; };
static propTypes = { static propTypes = {
studies: PropTypes.object, studies: PropTypes.object,
displaySet: PropTypes.object, displaySet: PropTypes.object,
viewportIndex: PropTypes.number, viewportIndex: PropTypes.number,
children: PropTypes.node children: PropTypes.node,
}; };
static id = "OHIFVTKViewport"; static id = 'OHIFVTKViewport';
static init() { static init() {
console.log("OHIFVTKViewport init()"); console.log('OHIFVTKViewport init()');
} }
static destroy() { static destroy() {
console.log("OHIFVTKViewport destroy()"); console.log('OHIFVTKViewport destroy()');
StackManager.clearStacks(); StackManager.clearStacks();
} }
@@ -109,7 +109,7 @@ class OHIFVTKViewport extends Component {
} else if (sopInstanceUid) { } else if (sopInstanceUid) {
const index = stack.imageIds.findIndex(imageId => { const index = stack.imageIds.findIndex(imageId => {
const sopCommonModule = cornerstone.metaData.get( const sopCommonModule = cornerstone.metaData.get(
"sopCommonModule", 'sopCommonModule',
imageId imageId
); );
if (!sopCommonModule) { if (!sopCommonModule) {
@@ -151,8 +151,8 @@ class OHIFVTKViewport extends Component {
switch (sopClassUid) { switch (sopClassUid) {
case SOP_CLASSES.SEGMENTATION_STORAGE: case SOP_CLASSES.SEGMENTATION_STORAGE:
throw new Error("Not yet implemented"); throw new Error('Not yet implemented');
/*
const data = handleSegmentationStorage( const data = handleSegmentationStorage(
stack.imageIds, stack.imageIds,
displaySetInstanceUid displaySetInstanceUid
@@ -164,67 +164,82 @@ class OHIFVTKViewport extends Component {
return loadImageData(imageDataObject).then(() => { return loadImageData(imageDataObject).then(() => {
return { return {
data: imageDataObject.vtkImageData, data: imageDataObject.vtkImageData,
labelmap: labelmapDataObject labelmap: labelmapDataObject,
}; };
}); });
*/
default: default:
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid); imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
return loadImageData(imageDataObject).then(() => { return imageDataObject;
return {
data: imageDataObject.vtkImageData
};
});
} }
}; };
getOrCreateVolume(data, displaySetInstanceUid) { getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
if (volumeCache[displaySetInstanceUid]) { if (volumeCache[displaySetInstanceUid]) {
return volumeCache[displaySetInstanceUid]; return volumeCache[displaySetInstanceUid];
} }
const { vtkImageData, imageMetaData0 } = imageDataObject;
const { windowWidth, windowCenter, modality } = imageMetaData0;
let lower;
let upper;
if (modality === 'PT') {
// For PET just set the range to 0-5 SUV
lower = 0;
upper = 5;
} else {
lower = windowCenter - windowWidth / 2.0;
upper = windowCenter + windowWidth / 2.0;
}
const volumeActor = vtkVolume.newInstance(); const volumeActor = vtkVolume.newInstance();
const volumeMapper = vtkVolumeMapper.newInstance(); const volumeMapper = vtkVolumeMapper.newInstance();
volumeActor.setMapper(volumeMapper); volumeActor.setMapper(volumeMapper);
volumeMapper.setInputData(data); volumeMapper.setInputData(vtkImageData);
// TODO: Should look into implementing autoAdjustSampleDistance in vtk volumeActor
const sampleDistance = .getProperty()
1.2 * .getRGBTransferFunction(0)
Math.sqrt( .setRange(lower, upper);
data
.getSpacing() const spacing = vtkImageData.getSpacing();
.map((v) => v * v) // Set the sample distance to half the mean length of one side. This is where the divide by 6 comes from.
.reduce((a, b) => a + b, 0) // https://github.com/Kitware/VTK/blob/6b559c65bb90614fb02eb6d1b9e3f0fca3fe4b0b/Rendering/VolumeOpenGL2/vtkSmartVolumeMapper.cxx#L344
); const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6;
volumeMapper.setSampleDistance(sampleDistance); volumeMapper.setSampleDistance(sampleDistance);
// Be generous to surpress warnings, as the logging really hurts performance.
// TODO: maybe we should auto adjust samples to 1000.
volumeMapper.setMaximumSamplesPerRay(4000);
volumeCache[displaySetInstanceUid] = volumeActor; volumeCache[displaySetInstanceUid] = volumeActor;
return volumeActor; return volumeActor;
} }
async setStateFromProps() { setStateFromProps() {
const { studies, displaySet } = this.props.viewportData; const { studies, displaySet } = this.props.viewportData;
const { const {
studyInstanceUid, studyInstanceUid,
displaySetInstanceUid, displaySetInstanceUid,
sopClassUids, sopClassUids,
sopInstanceUid, sopInstanceUid,
frameIndex frameIndex,
} = displaySet; } = displaySet;
if (sopClassUids.length > 1) { if (sopClassUids.length > 1) {
console.warn( console.warn(
"More than one SOPClassUid in the same series is not yet supported." 'More than one SOPClassUid in the same series is not yet supported.'
); );
} }
const sopClassUid = sopClassUids[0]; const sopClassUid = sopClassUids[0];
const imageDataObject = this.getViewportData(
let { data, labelmap } = await this.getViewportData(
studies, studies,
studyInstanceUid, studyInstanceUid,
displaySetInstanceUid, displaySetInstanceUid,
@@ -233,17 +248,45 @@ class OHIFVTKViewport extends Component {
frameIndex frameIndex
); );
if (!labelmap) { this.imageDataObject = imageDataObject;
labelmap = createLabelMapImageData(data);
}
const volumeActor = this.getOrCreateVolume(data, displaySetInstanceUid); // TODO: Temporarily disabling this since it is not yet
// being used and hurts performance significantly.
/*if (!labelmap) {
labelmap = createLabelMapImageData(data);
}*/
const volumeActor = this.getOrCreateVolume(
imageDataObject,
displaySetInstanceUid
);
this.setState({ this.setState({
volumes: [volumeActor], paintFilterBackgroundImageData: imageDataObject.vtkImageData,
paintFilterBackgroundImageData: data, paintFilterLabelMapImageData: null, // TODO
paintFilterLabelMapImageData: labelmap percentComplete: 0,
}); });
this.setState(
{
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
paintFilterLabelMapImageData: null, // TODO
percentComplete: 0,
},
() => {
this.loadProgressively(imageDataObject);
// TODO: There must be a better way to do this.
// We do this so that if all the data is available the react-vtkjs-viewport
// Will render _something_ before the volumes are set and the volume
// Construction that happens in react-vtkjs-viewport locks up the CPU.
setTimeout(() => {
this.setState({
volumes: [volumeActor],
});
}, 200);
}
);
} }
componentDidMount() { componentDidMount() {
@@ -264,6 +307,37 @@ class OHIFVTKViewport extends Component {
} }
} }
loadProgressively(imageDataObject) {
loadImageData(imageDataObject);
const { isLoading, insertPixelDataPromises } = imageDataObject;
const numberOfFrames = insertPixelDataPromises.length;
if (!isLoading) {
this.setState({ isLoaded: true });
return;
}
insertPixelDataPromises.forEach(promise => {
promise.then(numberProcessed => {
const percentComplete = Math.floor(
(numberProcessed * 100) / numberOfFrames
);
if (percentComplete !== this.state.percentComplete) {
this.setState({
percentComplete,
});
}
});
});
Promise.all(insertPixelDataPromises).then(() => {
this.setState({ isLoaded: true });
});
}
render() { render() {
let childrenWithProps = null; let childrenWithProps = null;
@@ -272,30 +346,32 @@ class OHIFVTKViewport extends Component {
childrenWithProps = this.props.children.map((child, index) => { childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, { return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex, viewportIndex: this.props.viewportIndex,
key: index key: index,
}); });
}); });
} }
const style = { width: "100%", height: "100%", position: "relative" }; const style = { width: '100%', height: '100%', position: 'relative' };
return ( return (
<> <>
{this.state.volumes ? ( <div style={style}>
<ConnectedVTKViewport {!this.state.isLoaded && (
volumes={this.state.volumes} <LoadingIndicator percentComplete={this.state.percentComplete} />
paintFilterLabelMapImageData={ )}
this.state.paintFilterLabelMapImageData {this.state.volumes && (
} <ConnectedVTKViewport
paintFilterBackgroundImageData={ volumes={this.state.volumes}
this.state.paintFilterBackgroundImageData paintFilterLabelMapImageData={
} this.state.paintFilterLabelMapImageData
viewportIndex={this.props.viewportIndex} }
/> paintFilterBackgroundImageData={
) : ( this.state.paintFilterBackgroundImageData
<div style={style}> }
<LoadingIndicator /> viewportIndex={this.props.viewportIndex}
</div> />
)}
</div>
)} )}
{childrenWithProps} {childrenWithProps}
</> </>
+37
View File
@@ -0,0 +1,37 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
+148 -177
View File
@@ -2,68 +2,28 @@ import {
vtkInteractorStyleMPRCrosshairs, vtkInteractorStyleMPRCrosshairs,
vtkInteractorStyleMPRWindowLevel, vtkInteractorStyleMPRWindowLevel,
vtkInteractorStyleMPRSlice, vtkInteractorStyleMPRSlice,
vtkInteractorStyleMPRRotate,
vtkSVGCrosshairsWidget, vtkSVGCrosshairsWidget,
vtkSVGWidgetManager,
} from 'react-vtkjs-viewport'; } from 'react-vtkjs-viewport';
import setMPRLayout from './utils/setMPRLayout.js'; import setMPRLayout from './utils/setMPRLayout.js';
import setViewportToVTK from './utils/setViewportToVTK.js'; import setViewportToVTK from './utils/setViewportToVTK.js';
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js'; import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
import throttle from 'lodash.throttle';
const { BlendMode } = Constants; const { BlendMode } = Constants;
// TODO: Put this somewhere else // TODO: Put this somewhere else
let apis = {}; let apis = {};
let currentSlabThickness = 0.1;
function getCrosshairCallbackForIndex(index) {
return ({ worldPos }) => {
// Set camera focal point to world coordinate for linked views
apis.forEach((api, viewportIndex) => {
if (viewportIndex !== index) {
// We are basically doing the same as getSlice but with the world coordinate
// that we want to jump to instead of the camera focal point.
// I would rather do the camera adjustment directly but I keep
// doing it wrong and so this is good enough for now.
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const sliceNormal = istyle.getSliceNormal();
const transform = vtkMatrixBuilder
.buildFromDegree()
.identity()
.rotateFromDirections(sliceNormal, [1, 0, 0]);
const mutatedWorldPos = worldPos.slice();
transform.apply(mutatedWorldPos);
const slice = mutatedWorldPos[0];
istyle.setSlice(slice);
renderWindow.render();
}
const renderer = api.genericRenderWindow.getRenderer();
const wPos = vtkCoordinate.newInstance();
wPos.setCoordinateSystemToWorld();
wPos.setValue(worldPos);
const displayPosition = wPos.getComputedDisplayValue(renderer);
const { svgWidgetManager } = api;
api.svgWidgets.crosshairsWidget.setPoint(
displayPosition[0],
displayPosition[1]
);
svgWidgetManager.render();
});
};
}
async function _getActiveViewportVTKApi(viewports) { async function _getActiveViewportVTKApi(viewports) {
const { layout, viewportSpecificData, activeViewportIndex } = viewports; const {
numRows,
numColumns,
layout,
viewportSpecificData,
activeViewportIndex,
} = viewports;
const currentData = layout.viewports[activeViewportIndex]; const currentData = layout.viewports[activeViewportIndex];
if (currentData && currentData.plugin === 'vtk') { if (currentData && currentData.plugin === 'vtk') {
@@ -81,6 +41,8 @@ async function _getActiveViewportVTKApi(viewports) {
api = await setViewportToVTK( api = await setViewportToVTK(
displaySet, displaySet,
activeViewportIndex, activeViewportIndex,
numRows,
numColumns,
layout, layout,
viewportSpecificData viewportSpecificData
); );
@@ -94,49 +56,13 @@ async function _getActiveViewportVTKApi(viewports) {
function _setView(api, sliceNormal, viewUp) { function _setView(api, sliceNormal, viewUp) {
const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const istyle = renderWindow.getInteractor().getInteractorStyle(); const istyle = renderWindow.getInteractor().getInteractorStyle();
istyle.setSliceNormal(...sliceNormal); istyle.setSliceNormal(...sliceNormal);
camera.setViewUp(...viewUp); istyle.setViewUp(...viewUp);
renderWindow.render(); renderWindow.render();
} }
function switchMPRInteractors(api, istyle) {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const currentIStyle = renderWindow.getInteractor().getInteractorStyle();
let currentNormal;
if (currentIStyle.getSliceNormal && istyle.getSliceNormal) {
currentNormal = currentIStyle.getSliceNormal();
}
let currentSlabThickness;
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
currentSlabThickness = currentIStyle.getSlabThickness();
}
renderWindow.getInteractor().setInteractorStyle(istyle);
// TODO: Not sure why this is required the second time this function is called
istyle.setInteractor(renderWindow.getInteractor());
if (istyle.getVolumeMapper() !== api.volumes[0]) {
if (currentNormal) {
istyle.setSliceNormal(currentNormal);
}
if (currentSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
istyle.setVolumeMapper(api.volumes[0]);
}
}
const actions = { const actions = {
axial: async ({ viewports }) => { axial: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports); const api = await _getActiveViewportVTKApi(viewports);
@@ -161,69 +87,91 @@ const actions = {
}, },
enableRotateTool: () => { enableRotateTool: () => {
apis.forEach(api => { apis.forEach(api => {
const istyle = vtkInteractorStyleMPRSlice.newInstance(); const istyle = vtkInteractorStyleMPRRotate.newInstance();
switchMPRInteractors(api, istyle); api.setInteractorStyle({ istyle });
}); });
}, },
enableCrosshairsTool: () => { enableCrosshairsTool: () => {
apis.forEach((api, index) => { apis.forEach((api, apiIndex) => {
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
switchMPRInteractors(api, istyle); api.setInteractorStyle({
istyle,
istyle.setCallback(getCrosshairCallbackForIndex(index)); configuration: { apis, apiIndex },
});
}); });
}, },
enableLevelTool: () => { enableLevelTool: () => {
function updateVOI(apis, windowWidth, windowCenter) {
apis.forEach(api => {
api.updateVOI(windowWidth, windowCenter);
});
}
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
const callbacks = {
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
renderWindow.render();
});
throttledUpdateVOIs(apis, windowWidth, windowCenter);
},
};
apis.forEach(api => { apis.forEach(api => {
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance(); const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
switchMPRInteractors(api, istyle); api.setInteractorStyle({ istyle, callbacks });
}); });
}, },
setSlabThickness: slabThickness => { setSlabThickness: ({ slabThickness }) => {
currentSlabThickness = slabThickness;
apis.forEach(api => { apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow(); api.setSlabThickness(slabThickness);
const istyle = renderWindow.getInteractor().getInteractorStyle();
if (istyle.setSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
// TODO: Do this inside the interactors in a setSlabThickness function instead
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const dist = camera.getDistance();
const near = dist - currentSlabThickness / 2;
const far = dist + currentSlabThickness / 2;
camera.setClippingRange(near, far);
}
renderWindow.render();
}); });
}, },
changeSlabThickness: ({ change }) => { changeSlabThickness: ({ change }) => {
currentSlabThickness += change; apis.forEach(api => {
currentSlabThickness = Math.max(currentSlabThickness, 0.1); const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
api.setSlabThickness(slabThickness);
});
},
setBlendModeToComposite: () => {
apis.forEach(api => { apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle(); const istyle = renderWindow.getInteractor().getInteractorStyle();
if (istyle.setSlabThickness) { const slabThickness = api.getSlabThickness();
istyle.setSlabThickness(currentSlabThickness);
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToComposite) {
mapper.setBlendModeToComposite();
} }
if (istyle.setSlabThickness) {
istyle.setSlabThickness(slabThickness);
}
renderWindow.render();
});
},
setBlendModeToMaximumIntensity: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToMaximumIntensity) {
mapper.setBlendModeToMaximumIntensity();
}
renderWindow.render(); renderWindow.render();
}); });
}, },
setBlendMode: ({ blendMode }) => { setBlendMode: ({ blendMode }) => {
apis.forEach(api => { apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
api.volumes[0].getMapper().setBlendMode(blendMode); api.volumes[0].getMapper().setBlendMode(blendMode);
@@ -231,75 +179,60 @@ const actions = {
}); });
}, },
mpr2d: async ({ viewports }) => { mpr2d: async ({ viewports }) => {
// TODO push a lot of this backdoor logic lower down to the library level.
const displaySet = const displaySet =
viewports.viewportSpecificData[viewports.activeViewportIndex]; viewports.viewportSpecificData[viewports.activeViewportIndex];
let apiByViewport; // Get current VOI if cornerstone viewport.
const cornerstoneVOI = getVOIFromCornerstoneViewport(displaySet);
const viewportProps = [
{
//Axial
orientation: {
sliceNormal: [0, 0, 1],
viewUp: [0, -1, 0],
},
},
{
// Sagital
orientation: {
sliceNormal: [1, 0, 0],
viewUp: [0, 0, 1],
},
},
{
// Coronal
orientation: {
sliceNormal: [0, 1, 0],
viewUp: [0, 0, 1],
},
},
];
try { try {
apiByViewport = await setMPRLayout(displaySet); apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
} catch (error) { } catch (error) {
throw new Error(error); throw new Error(error);
} }
apis = apiByViewport; if (cornerstoneVOI) {
setVOI(cornerstoneVOI);
}
/*const rgbTransferFunction = apiByViewport[0].volumes[0] // Add widgets and set default interactorStyle of each viewport.
.getProperty() apis.forEach((api, apiIndex) => {
.getRGBTransferFunction(0); api.addSVGWidget(
rgbTransferFunction.onModified(() => { vtkSVGCrosshairsWidget.newInstance(),
apiByViewport.forEach(a => { 'crosshairsWidget'
const renderWindow = a.genericRenderWindow.getRenderWindow(); );
renderWindow.render();
});
});*/
apiByViewport.forEach((api, index) => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
renderWindow.getInteractor().setInteractorStyle(istyle);
istyle.setVolumeMapper(api.volumes[0]); api.setInteractorStyle({
istyle.setCallback(getCrosshairCallbackForIndex(index)); istyle,
configuration: { apis, apiIndex },
const svgWidgetManager = vtkSVGWidgetManager.newInstance(); });
svgWidgetManager.setRenderer(renderer);
svgWidgetManager.setScale(1);
const crosshairsWidget = vtkSVGCrosshairsWidget.newInstance();
svgWidgetManager.addWidget(crosshairsWidget);
svgWidgetManager.render();
api.svgWidgetManager = svgWidgetManager;
api.svgWidgets = {
crosshairsWidget,
};
switch (index) {
default:
case 0:
//Axial
istyle.setSliceNormal(0, 0, 1);
camera.setViewUp(0, -1, 0);
break;
case 1:
// sagittal
istyle.setSliceNormal(1, 0, 0);
camera.setViewUp(0, 0, 1);
break;
case 2:
// Coronal
istyle.setSliceNormal(0, 1, 0);
camera.setViewUp(0, 0, 1);
break;
}
renderWindow.render();
}); });
}, },
}; };
@@ -338,12 +271,12 @@ const definitions = {
options: {}, options: {},
}, },
setBlendModeToComposite: { setBlendModeToComposite: {
commandFn: actions.setBlendMode, commandFn: actions.setBlendModeToComposite,
storeContexts: [], storeContexts: [],
options: { blendMode: BlendMode.COMPOSITE_BLEND }, options: { blendMode: BlendMode.COMPOSITE_BLEND },
}, },
setBlendModeToMaximumIntensity: { setBlendModeToMaximumIntensity: {
commandFn: actions.setBlendMode, commandFn: actions.setBlendModeToMaximumIntensity,
storeContexts: [], storeContexts: [],
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND }, options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
}, },
@@ -389,3 +322,41 @@ export default {
definitions, definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK', defaultContext: 'ACTIVE_VIEWPORT::VTK',
}; };
function getVOIFromCornerstoneViewport(displaySet) {
const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom);
if (cornerstoneElement) {
const imageId = cornerstoneElement.image.imageId;
const { modality } = cornerstone.metaData.get(
'generalSeriesModule',
imageId
);
if (modality !== 'PT') {
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
return {
windowWidth,
windowCenter,
};
}
}
}
function setVOI(voi) {
const { windowWidth, windowCenter } = voi;
const lower = windowCenter - windowWidth / 2.0;
const upper = windowCenter + windowWidth / 2.0;
const rgbTransferFunction = apis[0].volumes[0]
.getProperty()
.getRGBTransferFunction(0);
rgbTransferFunction.setRange(lower, upper);
apis.forEach(api => {
api.updateVOI(windowWidth, windowCenter);
});
}
+5 -1
View File
@@ -1,9 +1,13 @@
import OHIFVTKViewport from './OHIFVTKViewport.js'; import asyncComponent from './asyncComponent.js';
import commandsModule from './commandsModule.js'; import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js'; import toolbarModule from './toolbarModule.js';
// This feels weird // This feels weird
// import loadLocales from './loadLocales'; // import loadLocales from './loadLocales';
const OHIFVTKViewport = asyncComponent(() =>
import(/* webpackChunkName: "OHIFVTKViewport" */ './OHIFVTKViewport.js')
);
const vtkExtension = { const vtkExtension = {
/** /**
* Only required property. Should be a unique value across all extensions. * Only required property. Should be a unique value across all extensions.
@@ -0,0 +1,218 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Range, Checkbox, OldSelect } from '@ohif/ui';
import './slab-thickness-toolbar-button.styl';
const SLIDER = {
MIN: 0.1,
MAX: 1000,
STEP: 0.1,
};
const ToolbarLabel = props => {
const { label } = props;
return <div className="toolbar-button-label">{label}</div>;
};
ToolbarLabel.propTypes = {
label: PropTypes.string.isRequired,
};
const ToolbarSlider = props => {
const { value, min, max, onChange } = props;
return (
<div className="toolbar-slider-container">
<label htmlFor="toolbar-slider">{value}mm</label>
<Range
value={value}
min={min}
max={max}
step={SLIDER.STEP}
onChange={onChange}
id="toolbar-slider"
/>
</div>
);
};
ToolbarSlider.propTypes = {
value: PropTypes.number.isRequired,
min: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
};
const _getSelectOptions = button => {
return button.operationButtons.map(button => {
return {
key: button.label,
value: button.id,
};
});
};
const _getClassNames = (isActive, className) => {
return classnames('toolbar-button', 'slab-thickness', className, {
active: isActive,
});
};
const _applySlabThickness = (
value,
modeChecked,
toolbarClickCallback,
button
) => {
if (!modeChecked || !toolbarClickCallback) {
return;
}
const { actionButton } = button;
const generateOperation = (operation, value) => {
// Combine slider value into slider operation
const generatedOperation = { ...operation };
generatedOperation.commandOptions = {
...operation.commandOptions,
slabThickness: value,
};
return generatedOperation;
};
const operation = generateOperation(actionButton, value);
toolbarClickCallback(operation, event);
};
const _applyModeOperation = (
operation,
modeChecked,
toolbarClickCallback,
button
) => {
// in case modeChecked has not being triggered by user yet
if (typeof modeChecked !== 'boolean') {
return;
}
const { deactivateButton } = button;
const _operation = modeChecked ? operation : deactivateButton;
if (toolbarClickCallback && _operation) {
toolbarClickCallback(_operation);
}
};
const _getInitialState = currentSelectedOption => {
return {
value: SLIDER.MIN,
sliderMin: SLIDER.MIN,
sliderMax: SLIDER.MAX,
modeChecked: undefined,
operation: currentSelectedOption,
};
};
const INITIAL_OPTION_INDEX = 0;
const _getInitialtSelectedOption = (button = {}) => {
return (
button.operationButtons && button.operationButtons[INITIAL_OPTION_INDEX]
);
};
function SlabThicknessToolbarComponent({
parentContext,
toolbarClickCallback,
button,
activeButtons,
isActive,
className,
}) {
const currentSelectedOption = _getInitialtSelectedOption(button);
const [state, setState] = useState(_getInitialState(currentSelectedOption));
const { label, operationButtons } = button;
const _className = _getClassNames(isActive, className);
const selectOptions = _getSelectOptions(button);
function onChangeSelect(selectedValue) {
// find select value
const operation = operationButtons.find(
button => button.id === selectedValue
);
if (operation === state.operation) {
return;
}
setState({ ...state, operation });
}
function onChangeCheckbox(checked) {
setState({ ...state, modeChecked: checked });
}
function onChangeSlider(event) {
const value = Number(event.target.value);
if (value !== state.value) {
setState({ ...state, value, modeChecked: true });
}
}
useEffect(() => {
_applyModeOperation(
state.operation,
state.modeChecked,
toolbarClickCallback,
button
);
}, [state.modeChecked, state.operation]);
useEffect(() => {
_applySlabThickness(
state.value,
state.modeChecked,
toolbarClickCallback,
button
);
}, [state.operation, state.modeChecked, state.value]);
return (
<div className={_className}>
<div className="container">
<ToolbarSlider
value={state.value}
min={state.sliderMin}
max={state.sliderMax}
onChange={onChangeSlider}
/>
<ToolbarLabel key="toolbar-label" label={label} />
</div>
<div className="controller">
<Checkbox
label="mode"
checked={state.modeChecked}
onChange={onChangeCheckbox}
></Checkbox>
<OldSelect
key="toolbar-select"
options={selectOptions}
value={selectOptions[INITIAL_OPTION_INDEX].value}
onChange={onChangeSelect}
></OldSelect>
</div>
</div>
);
}
SlabThicknessToolbarComponent.propTypes = {
parentContext: PropTypes.object.isRequired,
toolbarClickCallback: PropTypes.func.isRequired,
button: PropTypes.object.isRequired,
activeButtons: PropTypes.array.isRequired,
isActive: PropTypes.bool,
className: PropTypes.string,
};
export default SlabThicknessToolbarComponent;
@@ -0,0 +1,55 @@
.slab-thickness
display: flex;
flex-direction: row;
align-items: flex-end;
height: 60px;
margin-top: -12px;
&:hover
color: var(--default-color);
.container
padding-right: 10px;
height: inherit;
display: flex;
flex-direction: column;
justify-content: space-between;
.toolbar-slider-container
width: 80px;
margin: 0px 2px 2px 2px;
font-size: 12px;
&:hover
color: var(--hover-color);
input
height: 20px;
padding-top: 6px;
&::-moz-range-thumb
height: 16px;
margin-top: -7px;
&::-webkit-slider-thumb
height: 16px;
margin-top: -7px;
.controller
width: 62px;
.ohif-check-container
margin-bottom: 5px;
padding-left: 18px;
.ohif-check-label
text-transform: capitalize;
&:hover
color: var(--hover-color);
.ohif-checkbox
border-radius: 3px;
background-color: white;
width: 12px;
height: 12px;
top: 22%;
&.ohif-checked:after
background-color: var(--active-color);
top: 17%;
left: 17%;
.select-ohif
color: var(--default-color);
border-color: var(--default-color);
background-color: black;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
&:hover
color: var(--hover-color);
+47
View File
@@ -1,3 +1,5 @@
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
const TOOLBAR_BUTTON_TYPES = { const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command', COMMAND: 'command',
SET_TOOL_ACTIVE: 'setToolActive', SET_TOOL_ACTIVE: 'setToolActive',
@@ -31,6 +33,7 @@ const definitions = [
commandName: 'enableRotateTool', commandName: 'enableRotateTool',
commandOptions: {}, commandOptions: {},
}, },
/*
{ {
id: 'setBlendModeToComposite', id: 'setBlendModeToComposite',
label: 'Disable MIP', label: 'Disable MIP',
@@ -68,6 +71,50 @@ const definitions = [
commandName: 'decreaseSlabThickness', commandName: 'decreaseSlabThickness',
commandOptions: {}, commandOptions: {},
}, },
*/
{
id: 'changeSlabThickness',
label: 'Slab Thickness',
icon: 'soft-tissue',
CustomComponent: SlabThicknessToolbarComponent,
commandName: 'setSlabThickness',
actionButton: {
id: 'setSlabThickness',
label: 'slider',
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'setSlabThickness',
commandOptions: {},
},
deactivateButton: {
id: 'setBlendModeToComposite',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToComposite',
commandOptions: {},
},
operationButtons: [
{
id: 'setBlendModeToMaximumIntensity',
label: 'MIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToMaximumIntensity',
commandOptions: {},
},
{
id: 'setBlendModeToMinimumIntensity',
label: 'MinIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToMinimumIntensity',
commandOptions: {},
},
{
id: 'setBlendModeToAverageIntensity',
label: 'AvgIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToAverageIntensity',
commandOptions: {},
},
],
},
]; ];
export default { export default {
+32 -36
View File
@@ -1,60 +1,56 @@
import setLayoutAndViewportData from './setLayoutAndViewportData.js'; import setLayoutAndViewportData from './setLayoutAndViewportData.js';
import setSingleLayoutData from './setSingleLayoutData.js';
export default function setMPRLayout(displaySet) { export default function setMPRLayout(
displaySet,
viewportPropsArray,
numRows = 1,
numColumns = 1
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let viewports = []; const viewports = [];
const rows = 1; const numViewports = numRows * numColumns;
const columns = 3;
const numViewports = rows * columns;
const viewportSpecificData = {};
for (let i = 0; i < numViewports; i++) {
viewports.push({
height: `${100 / rows}%`,
width: `${100 / columns}%`,
});
if (viewportPropsArray && viewportPropsArray.length !== numViewports) {
reject(
new Error(
'viewportProps is supplied but its length is not equal to numViewports'
)
);
}
const viewportSpecificData = {};
for (let i = 0; i < numViewports; i++) {
viewports.push({});
viewportSpecificData[i] = displaySet; viewportSpecificData[i] = displaySet;
viewportSpecificData[i].plugin = 'vtk'; viewportSpecificData[i].plugin = 'vtk';
} }
const layout = {
viewports,
};
const viewportIndices = [0, 1, 2];
let updatedViewports = layout.viewports;
const apis = []; const apis = [];
viewportIndices.forEach(viewportIndex => { viewports.forEach((viewport, index) => {
apis[viewportIndex] = null; apis[index] = null;
/*const currentData = layout.viewports[viewportIndex]; const viewportProps = viewportPropsArray[index];
if (currentData && currentData.plugin === 'vtk') { viewports[index] = Object.assign({}, viewports[index], {
reject(new Error('Should not have reached this point??'));
}*/
const data = {
// plugin: 'vtk',
vtk: { vtk: {
mode: 'mpr', // TODO: not used mode: 'mpr', // TODO: not used
afterCreation: api => { afterCreation: api => {
apis[viewportIndex] = api; apis[index] = api;
if (apis.every(a => !!a)) { if (apis.every(a => !!a)) {
resolve(apis); resolve(apis);
} }
}, },
...viewportProps,
}, },
}; });
updatedViewports = setSingleLayoutData(
updatedViewports,
viewportIndex,
data
);
}); });
setLayoutAndViewportData( setLayoutAndViewportData(
{ viewports: updatedViewports }, {
numRows,
numColumns,
viewports,
},
viewportSpecificData viewportSpecificData
); );
}); });
@@ -1,12 +0,0 @@
export default function setSingleLayoutData(
originalArray,
viewportIndex,
data
) {
const viewports = originalArray.slice();
const layoutData = Object.assign({}, viewports[viewportIndex], data);
viewports[viewportIndex] = layoutData;
return viewports;
}
+11 -10
View File
@@ -1,9 +1,10 @@
import setLayoutAndViewportData from './setLayoutAndViewportData.js'; import setLayoutAndViewportData from './setLayoutAndViewportData.js';
import setSingleLayoutData from './setSingleLayoutData.js';
export default function setViewportToVTK( export default function setViewportToVTK(
displaySet, displaySet,
viewportIndex, viewportIndex,
numRows,
numColumns,
layout, layout,
viewportSpecificData viewportSpecificData
) { ) {
@@ -13,7 +14,9 @@ export default function setViewportToVTK(
reject(new Error('Should not have reached this point??')); reject(new Error('Should not have reached this point??'));
}*/ }*/
const data = { const viewports = layout.viewports.slice();
viewports[viewportIndex] = Object.assign({}, viewports[viewportIndex], {
// plugin: 'vtk', // plugin: 'vtk',
vtk: { vtk: {
mode: 'mpr', // TODO: not used mode: 'mpr', // TODO: not used
@@ -21,18 +24,16 @@ export default function setViewportToVTK(
resolve(api); resolve(api);
}, },
}, },
}; });
const updatedViewports = setSingleLayoutData(
layout.viewports,
viewportIndex,
data
);
const updatedViewportData = viewportSpecificData; const updatedViewportData = viewportSpecificData;
setLayoutAndViewportData( setLayoutAndViewportData(
{ viewports: updatedViewports }, {
numRows,
numColumns,
viewports,
},
updatedViewportData updatedViewportData
); );
}); });
+3 -21
View File
@@ -3,7 +3,7 @@
# TOML Reference: # TOML Reference:
# https://www.netlify.com/docs/netlify-toml-reference/ # https://www.netlify.com/docs/netlify-toml-reference/
# #
# We use Netlify for deploy previews and deploy previews only. # We use Netlify for deploy previews and for publishing docs (gh-pages branch).
# https://viewer.ohif.org is created using a different process that is # https://viewer.ohif.org is created using a different process that is
# managed by CircleCI and deployed to our Google Hosting # managed by CircleCI and deployed to our Google Hosting
# #
@@ -38,26 +38,8 @@
[context.deploy-preview] [context.deploy-preview]
base = "" base = ""
publish = ".netlify/www/" publish = ".netlify/www/"
command = "yarn config set workspaces-experimental true && chmod +x .netlify/build-deploy-preview.sh && .netlify/build-deploy-preview.sh" command = "chmod +x .netlify/build-deploy-preview.sh && .netlify/build-deploy-preview.sh"
# command = "yarn config set workspaces-experimental true && chmod +x .netlify/build-deploy-preview.sh && .netlify/build-deploy-preview.sh"
# Redirects and headers are GLOBAL for all builds – they do not get scoped to
# contexts no matter where you define them in the file.
# For context-specific rules, use _headers or _redirects files, which are
# PER-DEPLOY.
# The following redirect is intended for use with most SPAs that handle
# routing internally.
# By default, redirects won't be applied if there's a file with the same
# path as the one defined in the `from` property. Setting `force` to `true`
# will make the redirect rule take precedence over any existing files.
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[redirects]]
from = "/pwa/*"
to = "/pwa/index.html"
status = 200
[[headers]] [[headers]]
# Define which paths this specific [[headers]] block will cover. # Define which paths this specific [[headers]] block will cover.
+27 -3
View File
@@ -21,14 +21,20 @@
"dev:project": ".scripts/dev.sh", "dev:project": ".scripts/dev.sh",
"dev:orthanc": "lerna run dev:orthanc --stream", "dev:orthanc": "lerna run dev:orthanc --stream",
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up", "orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
"start": "yarn run dev",
"test": "yarn run test:unit", "test": "yarn run test:unit",
"test:unit": "jest --collectCoverage", "test:unit": "jest --collectCoverage",
"test:unit:ci": "lerna run test:unit:ci --parallel --stream", "test:unit:ci": "lerna run test:unit:ci --parallel --stream",
"test:e2e": "lerna run test:e2e --stream",
"test:e2e:ci": "lerna run test:e2e:ci --stream",
"test:e2e:dist": "lerna run test:e2e:dist --stream",
"test:e2e:serve": "lerna run test:e2e:serve --stream",
"see-changed": "lerna changed", "see-changed": "lerna changed",
"docs:publish": "chmod +x ./build-and-publish-docs.sh && ./build-and-publish-docs.sh", "docs:publish": "chmod +x ./build-and-publish-docs.sh && ./build-and-publish-docs.sh",
"release": "yarn run lerna:version && yarn run lerna:publish", "release": "yarn run lerna:version && yarn run lerna:publish",
"lerna:version": "npx lerna version prerelease --force-publish", "lerna:version": "npx lerna version prerelease --force-publish",
"lerna:publish": "lerna publish from-package --canary --dist-tag canary" "lerna:publish": "lerna publish from-package --canary --dist-tag canary",
"link-list": "npm ls --depth=0 --link=true"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
@@ -44,6 +50,7 @@
"@babel/plugin-transform-runtime": "^7.5.0", "@babel/plugin-transform-runtime": "^7.5.0",
"@babel/preset-env": "^7.5.0", "@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@percy/cypress": "^2.2.0",
"babel-eslint": "9.x", "babel-eslint": "9.x",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-plugin-inline-react-svg": "1.1.0", "babel-plugin-inline-react-svg": "1.1.0",
@@ -52,16 +59,20 @@
"copy-webpack-plugin": "^5.0.3", "copy-webpack-plugin": "^5.0.3",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^3.2.0", "css-loader": "^3.2.0",
"dotenv": "^8.1.0",
"eslint": "5.16.0", "eslint": "5.16.0",
"eslint-config-prettier": "^6.4.0",
"eslint-config-react-app": "^4.0.1", "eslint-config-react-app": "^4.0.1",
"eslint-plugin-flowtype": "2.x", "eslint-plugin-flowtype": "2.x",
"eslint-plugin-import": "2.x", "eslint-plugin-import": "2.x",
"eslint-plugin-jsx-a11y": "6.x", "eslint-plugin-jsx-a11y": "6.x",
"eslint-plugin-node": "^9.1.0", "eslint-plugin-node": "^9.1.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "7.x", "eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "1.5.0", "eslint-plugin-react-hooks": "1.5.0",
"extract-css-chunks-webpack-plugin": "^4.5.4", "extract-css-chunks-webpack-plugin": "^4.5.4",
"favicons-webpack-plugin": "^1.0.2",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"husky": "^3.0.0", "husky": "^3.0.0",
"jest": "^24.8.0", "jest": "^24.8.0",
@@ -78,21 +89,34 @@
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-hot-loader": "^4.12.11", "react-hot-loader": "^4.12.11",
"redux-testkit": "^1.0.6", "redux-testkit": "^1.0.6",
"serve": "^11.1.0",
"shader-loader": "^1.3.1", "shader-loader": "^1.3.1",
"start-server-and-test": "^1.10.0",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"stylelint": "^10.1.0", "stylelint": "^10.1.0",
"stylelint-config-recommended": "^2.2.0", "stylelint-config-recommended": "^2.2.0",
"stylus": "^0.54.5", "stylus": "^0.54.5",
"stylus-loader": "^3.0.2", "stylus-loader": "^3.0.2",
"terser-webpack-plugin": "^1.4.1", "terser-webpack-plugin": "^2.1.0",
"webpack": "^4.35.2", "webpack": "^4.35.2",
"webpack-cli": "^3.3.5", "webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2", "webpack-dev-server": "^3.7.2",
"webpack-hot-middleware": "^2.25.0", "webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.1", "webpack-merge": "^4.2.1",
"workbox-webpack-plugin": "^4.3.1", "workbox-webpack-plugin": "^5.0.0-beta.1",
"worker-loader": "^2.0.0" "worker-loader": "^2.0.0"
}, },
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,json,css}": [
"prettier --write",
"git add"
]
},
"resolutions": { "resolutions": {
"**/@babel/runtime": "7.5.5" "**/@babel/runtime": "7.5.5"
} }
+232
View File
@@ -3,6 +3,238 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.2...@ohif/core@1.7.0) (2019-11-05)
### Features
* 🎸 Filter by url query param for seriesInstnaceUID ([#1117](https://github.com/OHIF/Viewers/issues/1117)) ([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
## [1.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.1...@ohif/core@1.6.2) (2019-11-05)
### Bug Fixes
* [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study List before all series have finishe… ([#1090](https://github.com/OHIF/Viewers/issues/1090)) ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125))
## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.0...@ohif/core@1.6.1) (2019-10-31)
### Bug Fixes
* application crash if patientName is an object ([#1138](https://github.com/OHIF/Viewers/issues/1138)) ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa))
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.2...@ohif/core@1.6.0) (2019-10-26)
### Features
* Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.1...@ohif/core@1.5.2) (2019-10-25)
### Bug Fixes
* set SR in ActiveViewport by clicking thumb ([#1091](https://github.com/OHIF/Viewers/issues/1091)) ([986b7ae](https://github.com/OHIF/Viewers/commit/986b7ae2bf4f7d27f326e62f93285ce20eaf0a79))
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.0...@ohif/core@1.5.1) (2019-10-25)
### Bug Fixes
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.4.0...@ohif/core@1.5.0) (2019-10-25)
### Features
* 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List ([#1069](https://github.com/OHIF/Viewers/issues/1069)) ([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db))
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.2...@ohif/core@1.4.0) (2019-10-15)
### Features
* 🎸 Only allow reconstruction of datasets that make sense ([#1010](https://github.com/OHIF/Viewers/issues/1010)) ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes [#561](https://github.com/OHIF/Viewers/issues/561)
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.1...@ohif/core@1.3.2) (2019-10-14)
### Bug Fixes
* Return display sets in StudyMetadata._createDisplaySetsForSeries ([#1042](https://github.com/OHIF/Viewers/issues/1042)) ([fc01532](https://github.com/OHIF/Viewers/commit/fc01532))
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.0...@ohif/core@1.3.1) (2019-10-11)
**Note:** Version bump only for package @ohif/core
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.2.0...@ohif/core@1.3.0) (2019-10-11)
### Features
* 🎸 Improve usability of Google Cloud adapter, including direct routes to studies ([#989](https://github.com/OHIF/Viewers/issues/989)) ([2bc361c](https://github.com/OHIF/Viewers/commit/2bc361c))
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.1.0...@ohif/core@1.2.0) (2019-10-09)
### Bug Fixes
* OHIF-1002 Study lazy load should be true by default ([#1004](https://github.com/OHIF/Viewers/issues/1004)) ([66d8bc6](https://github.com/OHIF/Viewers/commit/66d8bc6))
### Features
* Allow a server requestOptions.auth to be a function that returns the Authorization header. ([#928](https://github.com/OHIF/Viewers/issues/928)) ([0064a4b](https://github.com/OHIF/Viewers/commit/0064a4b))
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.2...@ohif/core@1.1.0) (2019-10-03)
### Features
* Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.1...@ohif/core@1.0.2) (2019-10-02)
### Bug Fixes
* Temporarily sort SEG files to the end of the display set list as a workaround for several metadata issues ([#987](https://github.com/OHIF/Viewers/issues/987)) ([b3b4c10](https://github.com/OHIF/Viewers/commit/b3b4c10))
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.0...@ohif/core@1.0.1) (2019-09-27)
### Bug Fixes
* Check for Value in 00081155 sequence (Few patient protocol images doesn't have this value) and removed a duplicate declaration ([#921](https://github.com/OHIF/Viewers/issues/921)) ([d0ec9cf](https://github.com/OHIF/Viewers/commit/d0ec9cf))
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.10...@ohif/core@1.0.0) (2019-09-27)
### Bug Fixes
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
### BREAKING CHANGES
* DICOM Seg
## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.9...@ohif/core@0.50.10) (2019-09-23)
### Bug Fixes
* Avoid using variable name "module" ([#942](https://github.com/OHIF/Viewers/issues/942)) ([72427fe](https://github.com/OHIF/Viewers/commit/72427fe)), closes [#940](https://github.com/OHIF/Viewers/issues/940)
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.8...@ohif/core@0.50.9) (2019-09-17)
### Bug Fixes
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.7...@ohif/core@0.50.8) (2019-09-10)
**Note:** Version bump only for package @ohif/core
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.6...@ohif/core@0.50.7) (2019-09-10)
### Bug Fixes
* remove requestOptions when key is not needed ([32bc47d](https://github.com/OHIF/Viewers/commit/32bc47d))
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.5...@ohif/core@0.50.6) (2019-09-09)
**Note:** Version bump only for package @ohif/core
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.4...@ohif/core@0.50.5) (2019-09-04) ## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.4...@ohif/core@0.50.5) (2019-09-04)
**Note:** Version bump only for package @ohif/core **Note:** Version bump only for package @ohif/core
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/core", "name": "@ohif/core",
"version": "0.50.5", "version": "1.7.0",
"description": "Generic business logic for web-based medical imaging applications", "description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team", "author": "OHIF Core Team",
"license": "MIT", "license": "MIT",
@@ -32,7 +32,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-tools": "^3.9.0", "cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3" "dicom-parser": "^1.8.3"
}, },
@@ -6,17 +6,26 @@ import user from '../user';
/** /**
* Returns the Authorization header as part of an Object. * Returns the Authorization header as part of an Object.
* *
* @returns {Object} * @export
* @param {Object} [server={}]
* @param {Object} [server.requestOptions]
* @param {string|function} [server.requestOptions.auth]
* @returns {Object} { Authorization }
*/ */
export default function getAuthorizationHeader(server) { export default function getAuthorizationHeader({ requestOptions } = {}) {
const headers = {}; const headers = {};
// Check for OHIF.user since this can also be run on the server // Check for OHIF.user since this can also be run on the server
const accessToken = user && user.getAccessToken && user.getAccessToken(); const accessToken = user && user.getAccessToken && user.getAccessToken();
if (server && server.requestOptions && server.requestOptions.auth) { if (requestOptions && requestOptions.auth) {
// HTTP Basic Auth (user:password) if (typeof requestOptions.auth === 'function') {
headers.Authorization = `Basic ${btoa(server.requestOptions.auth)}`; // Custom Auth Header
headers.Authorization = requestOptions.auth(requestOptions);
} else {
// HTTP Basic Auth (user:password)
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
}
} else if (accessToken) { } else if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`; headers.Authorization = `Bearer ${accessToken}`;
} }
@@ -7,10 +7,7 @@ describe('getAuthorizationHeader', () => {
it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => { it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => {
const validServer = { const validServer = {
requestOptions: { requestOptions: {
auth: { auth: 'dummy_user:dummy_password',
user: 'dummy_user',
password: 'dummy_password',
},
}, },
}; };
@@ -26,9 +23,7 @@ describe('getAuthorizationHeader', () => {
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no password', () => { it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no password', () => {
const validServerWithoutPassword = { const validServerWithoutPassword = {
requestOptions: { requestOptions: {
auth: { auth: 'dummy_user',
user: 'dummy_user',
},
}, },
}; };
@@ -43,22 +38,19 @@ describe('getAuthorizationHeader', () => {
expect(authentication).toEqual(expectedAuthorizationHeader); expect(authentication).toEqual(expectedAuthorizationHeader);
}); });
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no username', () => { it('should return a HTTP Basic Auth when server contains requestOptions.auth custom function', () => {
const validServerWithoutPassword = { const validServerCustomAuth = {
requestOptions: { requestOptions: {
auth: { auth: options => `Basic ${options.token}`,
user: 'dummy_user', token: 'ZHVtbXlfdXNlcjpkdW1teV9wYXNzd29yZA==',
},
}, },
}; };
const expectedAuthorizationHeader = { const expectedAuthorizationHeader = {
Authorization: `Basic ${btoa( Authorization: `Basic ${validServerCustomAuth.requestOptions.token}`,
validServerWithoutPassword.requestOptions.auth
)}`,
}; };
const authentication = getAuthorizationHeader(validServerWithoutPassword); const authentication = getAuthorizationHeader(validServerCustomAuth);
expect(authentication).toEqual(expectedAuthorizationHeader); expect(authentication).toEqual(expectedAuthorizationHeader);
}); });
+9 -5
View File
@@ -1,11 +1,14 @@
export default function getModalities(modality, modalitiesInStudy) { export default function getModalities(modality, modalitiesInStudy) {
let modalities = {}; if (!modality && !modalitiesInStudy) {
if (modality) { return {};
modalities = modality;
} }
const modalities = modality || {
vr: 'CS',
Value: [],
};
if (modalitiesInStudy) { if (modalitiesInStudy) {
// Find vr in modalities
if (modalities.vr && modalities.vr === modalitiesInStudy.vr) { if (modalities.vr && modalities.vr === modalitiesInStudy.vr) {
for (let i = 0; i < modalitiesInStudy.Value.length; i++) { for (let i = 0; i < modalitiesInStudy.Value.length; i++) {
const value = modalitiesInStudy.Value[i]; const value = modalitiesInStudy.Value[i];
@@ -14,8 +17,9 @@ export default function getModalities(modality, modalitiesInStudy) {
} }
} }
} else { } else {
modalities = modalitiesInStudy; return modalitiesInStudy;
} }
} }
return modalities; return modalities;
} }
@@ -1,10 +1,12 @@
// import { api } from 'dicomweb-client' // import { api } from 'dicomweb-client'
const api = { const api = {
DICOMwebClient: jest.fn().mockImplementation(() => { DICOMwebClient: jest.fn().mockImplementation(function() {
return { this.retrieveStudyMetadata = jest.fn().mockResolvedValue([]);
retrieveStudyMetadata: jest.fn().mockResolvedValue([]), this.retrieveSeriesMetadata = jest.fn(function(options) {
}; const { studyInstanceUID, seriesInstanceUID } = options;
return Promise.resolve([{ studyInstanceUID, seriesInstanceUID }]);
});
}), }),
}; };
-167
View File
@@ -1,167 +0,0 @@
# Table of contents
In this document, some important objects are described. In the files there are comments that can help better undestand their methods and properties.
- [ResizeViewportManager object](#the-resize-viewport-manager-object)
- [ImageSet object](#the-image-set-object)
- [Layout Manager](#the-layout-manager-object)
- [Type Safe Collections](#the-type-safe-collections)
# The Resize Viewport Manager object
This object has multiple functions to manage window resize event. It relocates Dialogs, resizes viewport elements and scrollbars and some other UI components such as Study and Series Quick Switch, when available.
## Usage
It's only necessary to bind **handleResize** function to the window resize event as follows. The **ohif:viewerbase** package needs to be imported by the referring code as well.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
const ResizeViewportManager = new Viewerbase.ResizeViewportManager();
window.addEventListener('resize', ResizeViewportManager.getResizeHandler());
```
An example os its usage can be found in **ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js**.
# The Image Set object
An object that represents a list of images that are associated by any arbitrary criteria being thus content agnostic. Besides the main attributes (**images** and **uid**) it allows additional attributes to be appended to it (currently indiscriminately, but this should be changed).
## Usage
ImageSet constructor requires an array of SOP instances like in the example below. It's necessary to import **ohif:viewerbase**.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
const imageSet = new Viewerbase.ImageSet(sopInstances);
imageSet.setAttributes({
displaySetInstanceUid: imageSet.uid,
seriesInstanceUid: seriesData.seriesInstanceUid,
seriesNumber: seriesData.seriesNumber,
seriesDescription: seriesData.seriesDescription,
numImageFrames: instances.length,
frameRate: instance.getRawValue('x00181063'),
modality: seriesData.modality,
isMultiFrame: isMultiFrame(instance)
});
// Sort instances by InstanceNumber (0020,0013)
imageSet.sortBy((a, b) => {
return (parseInt(a.getRawValue('x00200013', 0)) || 0) - (parseInt(b.getRawValue('x00200013', 0)) || 0);
});
```
Each SOP instance in this example is an instance of **OHIFInstanceMetadata** object, which is a specialization of **InstanceMetadata**. To read more about the **Metadata API** click [here](metadata/).
# The Layout Manager object
Objects of this class are responsible for creating, organizing and maintaining (manage) viewport rendering. It creates a grid, positioning viewports accordingly to it's configuration keeping all viewports data (in **viewportData** property) for easy access from other components. It support many layout configurations and some of them were fully tested: 1x1, 1x2, 1x3, 2x1, 2x2, 2x3, 3x1, 3x2, 3x3. Other configurations may work as well.
Finally it provides some useful functions to move through viewports and zoom it.
## Usage
In order to use _LayoutManager_ the **ohif:viewerbase** package needs to be imported by the referring code and instantiated as follows. An example os its usage is in **ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js**.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Get an array of studies object. This function needs to be implemented, it does not exist.
const studies = getArrayOfStudiesObjects();
const parentElement = document.getElementById('layoutManagerTarget');
const LayoutManager = new Viewerbase.LayoutManager(parentElement, studies);
```
The default configuration is 1x1, and to change it just set **layoutProps** and call **updateViewports** to update the layout as follows.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Get an array of studies object. This function needs to be implemented, it does not exist.
const studies = getArrayOfStudiesObjects();
const parentElement = document.getElementById('layoutManagerTarget');
const LayoutManager = new LayoutManager(parentElement, studies);
// Set the layout proprerties to 2x2 layout
LayoutManager.layoutProps = {
rows: 2,
columns: 2
};
// It will render four viewports: two in each row.
LayoutManager.updateViewports();
```
The layoutManagerTarget element will have a new class **layout-2-2** (to allow further styling) and it's inner content will a new div#imageViewerViewports that has four inner elements like the following (some elements and attributes were removed for example purpose):
```html
<div class="viewportContainer active" style="height:50%; width:50%;">
<div class="removable">
<div class="imageViewerViewport">
<canvas></canvas>
</div>
<div class="imageViewerViewportOverlay"></div>
<div class="imageViewerLoadingIndicator"></div>
<div class="imageViewerErrorLoadingIndicator"></div>
<div class="viewportOrientationMarkers"></div>
</div>
</div>
```
Each of this _div.viewportContainer_ will have some classes to help CSS specific styling accordingly to the element's position in the grid: **top**, **middle** and **bottom**. This classes are added by **viewer/components/gridLayout/** component in ohif-viewerbase package.
# The Type Safe Collections
With the introduction of the new _Study Metadata API_ in which study metadata is represented by class hierarchies (using prototype-based inheritance), the usage of standard _Minimongo_ collections as a central client-side storage for this data became no longer an option. Standard _Mongo_ and _Minimongo_ collections internally _flatten_ data (in other words, data gets serialized) before storage hence no functions or prototype chains are preserved. In that scenario, when an object is restored (fetched), what is returned is actually a flattened copy of the original object with no functions or prototype (it's no longer an instance of it's original class). As an attempt to overcome this limitation a new type of collection was intruduced: the *TypeSafeCollection*.
The `TypeSafeCollection` is a simple list-like collection which tries to implement an API _similar_ but not compatible with _Mongo_'s API. It supports basic features like search by attribute map and ID, retrieval by index, sorting of result sets, insertion, removal and reactive operations but, unlike _Mongo_'s API, it (still) lacks support to advanced functionality like complex search criterea or flexible sorting options.
## Implementation
The `TypeSafeCollection` is implemented on top of the _JavaScript_ `Array` object. Each element inserted in the collection is appended to the end of its internal array as a _key-value pair (KVP)_ object where the _key_ is a unique randomly generated ID string and the _value_ is the element itself. Once the object has been successfully stored, the generated ID (its ID) is returned to the client code and can later be used to access that specific element. At this point, an important difference to the _Minimongo_ API can be highlighted: a _TypeSafeCollection_ instance will never make any changes to the stored element (e.g., no "\_id" property will ever be assigned to the original object). Another relevant feature that is supported by this design decision is that _not only objects_ can be stored in this collections, but literally _anything_.
Inside the codebase, the _value_ attribute of each _KVP_ entry in the collection is refered to as _the **payload** of the entry_ since it's what really matters to the user. Hence, this term will also be used here to refer to the _value that has been stored in the collection_. That being said, we can approach another important feature of these collections: A single _payload_ cannot be stored more than once in a given collection. When an attempt of inserting a _payload_ which is already present in the collection is detected, the insert operation will fail and `null` will be returned. In that regard, the collection behaves like `Set` object not permitting a payload to be stored more than once. Strict equality is used when comparing payloads, thus cloned objects are not considered the same. This feature adds an additional garantee that a given study/series/instance will not be listed more than once (it was designed as a replacement for central study collections which were always checked for duplicates).
Please refer to the codebase for the full `TypeSafeCollection` API.
## Usage
In order to use the `TypeSafeCollection` class, the **ohif:viewerbase** package needs to be imported by the referring code and instantiated as follows:
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase'; // i.e., Viewerbase.TypeSafeCollection
OR
import 'meteor/ohif:viewerbase'; // i.e., OHIF.viewerbase.TypeSafeCollection
// The later is preferred when the client code already makes use of the "OHIF" namespace making the second
// "import" a garantee that the ".viewerbase" namespace has been properly loaded.
```
A few usage examples:
```javascript
const Users = new OHIF.viewerbase.TypeSafeCollection();
[[ ... ]]
// Insert a User object...
let userId = Users.insert({
data: {
firstName: 'John',
lastName: 'Doe',
age: 45
},
getFullName() {
return `${this.data.firstName} ${this.data.lastName}`;
},
getAge() {
return this.data.age;
}
});
[[ ... ]]
let theUserWeJustStored = Users.findById(userId); // ;-)
[[ ... ]]
// Retrieve a single user with "Doe" as `lastName`...
let myUser = Users.findBy({ 'data.lastName': 'Doe' });
// Or all users with "Doe" as `lastName`, sorted by `firstName` in ascending
// order and using the `age` attribute to break ties in descending order...
let myUsers = Users.findAllBy({ 'data.lastName': 'Doe' }, {
sort: [ [ 'data.firstName', 'asc' ], [ 'data.age', 'desc' ] ]
});
```
@@ -8,7 +8,9 @@ import { SeriesMetadata } from './SeriesMetadata';
// - createStacks // - createStacks
import { api } from 'dicomweb-client'; import { api } from 'dicomweb-client';
// - createStacks // - createStacks
import { isImage } from './../../utils/isImage'; import { isImage } from '../../utils/isImage';
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
export class StudyMetadata extends Metadata { export class StudyMetadata extends Metadata {
constructor(data, uid) { constructor(data, uid) {
@@ -88,6 +90,101 @@ export class StudyMetadata extends Metadata {
return this._displaySets.slice(); return this._displaySets.slice();
} }
/**
* Split a series metadata object into display sets
* @param {Array} sopClassHandlerModules List of SOP Class Modules
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
* @param {Array} [givenDisplaySets] An optional list to which the display sets will be appended
* @returns {Array} The list of display sets created for the given series object
*/
_createDisplaySetsForSeries(
sopClassHandlerModules,
series,
givenDisplaySets
) {
const study = this;
const displaySets = Array.isArray(givenDisplaySets) ? givenDisplaySets : [];
const anyInstances = series.getInstanceCount() > 0;
if (!anyInstances) {
return;
}
const sopClassUids = getSopClassUids(series);
if (sopClassHandlerModules && sopClassHandlerModules.length > 0) {
const displaySet = _getDisplaySetFromSopClassModule(
sopClassHandlerModules,
series,
study,
sopClassUids
);
if (displaySet) {
displaySet.sopClassModule = true;
displaySets.push(displaySet);
return displaySets;
}
}
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
// Search through the instances (InstanceMetadata object) of this series
// Split Multi-frame instances and Single-image modalities
// into their own specific display sets. Place the rest of each
// series into another display set.
const stackableInstances = [];
series.forEachInstance(instance => {
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
if (
!isImage(instance.getRawValue('x00080016')) &&
!instance.getRawValue('x00280010')
) {
return;
}
let displaySet;
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
isClip: true,
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.modality)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
seriesInstanceUid: series.getSeriesInstanceUID(),
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else {
stackableInstances.push(instance);
}
});
if (stackableInstances.length) {
const displaySet = makeDisplaySet(series, stackableInstances);
displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID());
displaySet.setAttributes({
sopClassUids,
});
displaySets.push(displaySet);
}
return displaySets;
}
/** /**
* Creates a set of series to be placed in the Study Metadata * Creates a set of series to be placed in the Study Metadata
* The series that appear in the Study Metadata must represent * The series that appear in the Study Metadata must represent
@@ -101,103 +198,44 @@ export class StudyMetadata extends Metadata {
* @returns {Array} An array of series to be placed in the Study Metadata * @returns {Array} An array of series to be placed in the Study Metadata
*/ */
createDisplaySets(sopClassHandlerModules) { createDisplaySets(sopClassHandlerModules) {
const study = this;
const displaySets = []; const displaySets = [];
const anyDisplaySets = study.getSeriesCount(); const anyDisplaySets = this.getSeriesCount();
const anySopClassHandlerModules =
sopClassHandlerModules && sopClassHandlerModules.length > 0;
if (!anyDisplaySets) { if (!anyDisplaySets) {
return displaySets; return displaySets;
} }
// Loop through the series (SeriesMetadata) // Loop through the series (SeriesMetadata)
this.forEachSeries(series => { this.forEachSeries(
const anyInstances = series.getInstanceCount() > 0; series =>
if (!anyInstances) { void this._createDisplaySetsForSeries(
return;
}
const sopClassUids = getSopClassUids(series);
if (anySopClassHandlerModules) {
const displaySet = _getDisplaySetFromSopClassModule(
sopClassHandlerModules, sopClassHandlerModules,
series, series,
study, displaySets
sopClassUids )
); );
if (displaySet) { return sortDisplaySetList(displaySets);
displaySets.push(displaySet); }
return; sortDisplaySets() {
} sortDisplaySetList(this._displaySets);
} }
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING /**
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN * Method to append display sets from a given series to the internal list of display sets
* @param {Array} sopClassHandlerModules A list of SOP Class Handler Modules
// Search through the instances (InstanceMetadata object) of this series * @param {SeriesMetadata} series The series metadata object from which the display sets will be created
// Split Multi-frame instances and Single-image modalities * @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
// into their own specific display sets. Place the rest of each */
// series into another display set. createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
const stackableInstances = []; if (this.containsSeries(series)) {
series.forEachInstance(instance => { this.setDisplaySets(
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010) this._createDisplaySetsForSeries(sopClassHandlerModules, series)
if ( );
!isImage(instance.getRawValue('x00080016')) && return true;
!instance.getRawValue('x00280010') }
) { return false;
return;
}
let displaySet;
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
isClip: true,
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.modality)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
seriesInstanceUid: series.getSeriesInstanceUID(),
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else {
stackableInstances.push(instance);
}
});
if (stackableInstances.length) {
const displaySet = makeDisplaySet(series, stackableInstances);
displaySet.setAttribute(
'studyInstanceUid',
study.getStudyInstanceUID()
);
displaySet.setAttributes({
sopClassUids,
});
displaySets.push(displaySet);
}
});
// TODO
displaySets.sort(_sortBySeriesNumber);
return displaySets;
} }
/** /**
@@ -205,7 +243,10 @@ export class StudyMetadata extends Metadata {
* @param {Array} displaySets Array of display sets (ImageSet[]) * @param {Array} displaySets Array of display sets (ImageSet[])
*/ */
setDisplaySets(displaySets) { setDisplaySets(displaySets) {
displaySets.forEach(displaySet => this.addDisplaySet(displaySet)); if (Array.isArray(displaySets) && displaySets.length > 0) {
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
this.sortDisplaySets();
}
} }
/** /**
@@ -214,7 +255,7 @@ export class StudyMetadata extends Metadata {
* @returns {boolean} True on success, false on failure. * @returns {boolean} True on success, false on failure.
*/ */
addDisplaySet(displaySet) { addDisplaySet(displaySet) {
if (displaySet instanceof ImageSet) { if (displaySet instanceof ImageSet || displaySet.sopClassModule) {
this._displaySets.push(displaySet); this._displaySets.push(displaySet);
return true; return true;
} }
@@ -319,6 +360,12 @@ export class StudyMetadata extends Metadata {
return found; return found;
} }
containsSeries(series) {
return (
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
);
}
/** /**
* Retrieve the number of series within the current study. * Retrieve the number of series within the current study.
* @returns {number} The number of series in the current study. * @returns {number} The number of series in the current study.
@@ -575,6 +622,16 @@ const makeDisplaySet = (series, instances) => {
imageSet.getImage(0).getRawValue('x00200013') imageSet.getImage(0).getRawValue('x00200013')
); );
const isReconstructable = isDisplaySetReconstructable(series, instances);
imageSet.isReconstructable = isReconstructable.value;
if (isReconstructable.missingFrames) {
// TODO -> This is currently unused, but may be used for reconstructing
// Volumes with gaps later on.
imageSet.missingFrames = isReconstructable.missingFrames;
}
return imageSet; return imageSet;
}; };
@@ -649,13 +706,51 @@ function _getDisplaySetFromSopClassModule(
} }
/** /**
* Sort series primarily by modality (i.e., series with references to other
* series like SEG, KO or PR are grouped in the end of the list) and then by
* series number:
*
* --------
* | CT #3 |
* | CT #4 |
* | CT #5 |
* --------
* | SEG #1 |
* | SEG #2 |
* --------
* *
* @param {*} a - DisplaySet * @param {*} a - DisplaySet
* @param {*} b - DisplaySet * @param {*} b - DisplaySet
*/ */
function _sortBySeriesNumber(a, b) {
function seriesSortingCriteria(a, b) {
const isLowPriorityA = isLowPriorityModality(a.modality);
const isLowPriorityB = isLowPriorityModality(b.modality);
if (!isLowPriorityA && isLowPriorityB) {
return -1;
}
if (isLowPriorityA && !isLowPriorityB) {
return 1;
}
return sortBySeriesNumber(a, b);
}
/**
* Sort series by series number. Series with low
* @param {*} a - DisplaySet
* @param {*} b - DisplaySet
*/
function sortBySeriesNumber(a, b) {
const seriesNumberAIsGreaterOrUndefined = const seriesNumberAIsGreaterOrUndefined =
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber); a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
return seriesNumberAIsGreaterOrUndefined ? 1 : -1; return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
} }
/**
* Sorts a list of display set objects
* @param {Array} list A list of display sets to be sorted
*/
function sortDisplaySetList(list) {
return list.sort(seriesSortingCriteria);
}
@@ -504,12 +504,12 @@ export default class ProtocolEngine {
/** /**
* Sets the current layout * Sets the current layout
* *
* @param rows * @param {number} numRows
* @param columns * @param {number} numColumns
*/ */
setLayout(rows, columns) { setLayout(numRows, numColumns) {
if (rows < 1 && columns < 1) { if (numRows < 1 && numColumns < 1) {
log.error(`Invalid layout ${rows} x ${columns}`); log.error(`Invalid layout ${numRows} x ${numColumns}`);
return; return;
} }
@@ -519,16 +519,13 @@ export default class ProtocolEngine {
} }
let viewports = []; let viewports = [];
const numViewports = rows * columns; const numViewports = numRows * numColumns;
for (let i = 0; i < numViewports; i++) { for (let i = 0; i < numViewports; i++) {
viewports.push({ viewports.push({});
height: `${100 / rows}%`,
width: `${100 / columns}%`,
});
} }
this.options.setLayout({ viewports }); this.options.setLayout({ numRows, numColumns, viewports });
} }
/** /**
@@ -634,14 +631,10 @@ export default class ProtocolEngine {
//console.log('renderedCallback for ' + element.id); //console.log('renderedCallback for ' + element.id);
customSettings.forEach(customSetting => { customSettings.forEach(customSetting => {
log.trace( log.trace(
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${ `ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
customSetting.id
}`
); );
log.trace( log.trace(
`ProtocolEngine::currentViewportData.renderedCallback with value: ${ `ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
customSetting.value
}`
); );
const setting = CustomViewportSettings[customSetting.id]; const setting = CustomViewportSettings[customSetting.id];
+27 -8
View File
@@ -26,9 +26,34 @@ export const setViewportActive = viewportIndex => ({
viewportIndex, viewportIndex,
}); });
export const setLayout = layout => ({ /**
* @param {object} layout
* @param {number} layout.numRows
* @param {number} layout.numColumns
* @param {array} layout.viewports
*/
export const setLayout = ({ numRows, numColumns, viewports }) => ({
type: SET_VIEWPORT_LAYOUT, type: SET_VIEWPORT_LAYOUT,
layout, numRows,
numColumns,
viewports,
});
/**
* @param {object} layout
* @param {number} layout.numRows
* @param {number} layout.numColumns
* @param {array} layout.viewports
*/
export const setViewportLayoutAndData = (
{ numRows, numColumns, viewports },
viewportSpecificData
) => ({
type: SET_VIEWPORT_LAYOUT_AND_DATA,
numRows,
numColumns,
viewports,
viewportSpecificData,
}); });
export const clearViewportSpecificData = viewportIndex => ({ export const clearViewportSpecificData = viewportIndex => ({
@@ -87,12 +112,6 @@ export const setServers = servers => ({
servers, servers,
}); });
export const setViewportLayoutAndData = (layout, viewportSpecificData) => ({
type: SET_VIEWPORT_LAYOUT_AND_DATA,
layout,
viewportSpecificData,
});
const actions = { const actions = {
// VIEWPORT // VIEWPORT
setViewportActive, setViewportActive,
+11 -10
View File
@@ -94,19 +94,20 @@ describe('actions', () => {
}); });
it('should create an action to set the viewport layout', () => { it('should create an action to set the viewport layout', () => {
const layout = { const numRows = 1;
viewports: [ const numColumns = 2;
{ const viewports = [{ plugin: 'vtk' }, { plugin: 'vtk' }];
height: '100%',
width: '100%',
},
],
};
const expectedAction = { const expectedAction = {
type: types.SET_VIEWPORT_LAYOUT, type: types.SET_VIEWPORT_LAYOUT,
layout, numRows,
numColumns,
viewports,
}; };
expect(actions.setLayout(layout)).toEqual(expectedAction);
expect(actions.setLayout({ numRows, numColumns, viewports })).toEqual(
expectedAction
);
}); });
}); });
}); });
+2
View File
@@ -1,11 +1,13 @@
import actions from './actions.js'; import actions from './actions.js';
import reducers from './reducers'; import reducers from './reducers';
import localStorage from './localStorage.js'; import localStorage from './localStorage.js';
import sessionStorage from './sessionStorage.js';
const redux = { const redux = {
reducers, reducers,
actions, actions,
localStorage, localStorage,
sessionStorage,
}; };
export default redux; export default redux;
+1 -1
View File
@@ -2,7 +2,7 @@ import redux from './index.js';
describe('redux exports', () => { describe('redux exports', () => {
test('have not changed', () => { test('have not changed', () => {
const expectedExports = ['actions', 'reducers', 'localStorage'].sort(); const expectedExports = ['actions', 'reducers', 'localStorage', 'sessionStorage'].sort();
const exports = Object.keys(redux).sort(); const exports = Object.keys(redux).sort();
+4 -2
View File
@@ -1,6 +1,8 @@
const LocalStorageApi = window.localStorage;
const localStorageKey = 'state';
export const loadState = () => { export const loadState = () => {
try { try {
const serializedState = window.localStorage.getItem('state'); const serializedState = LocalStorageApi.getItem(localStorageKey);
if (!serializedState) { if (!serializedState) {
return undefined; return undefined;
} }
@@ -14,7 +16,7 @@ export const loadState = () => {
export const saveState = state => { export const saveState = state => {
try { try {
const serializedState = JSON.stringify(state); const serializedState = JSON.stringify(state);
localStorage.setItem('state', serializedState); LocalStorageApi.setItem(localStorageKey, serializedState);
} catch (e) {} } catch (e) {}
}; };
+37 -14
View File
@@ -12,13 +12,13 @@ import cloneDeep from 'lodash.clonedeep';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
const defaultState = { const defaultState = {
numRows: 1,
numColumns: 1,
activeViewportIndex: 0, activeViewportIndex: 0,
layout: { layout: {
viewports: [ viewports: [
{ {
// plugin: 'cornerstone', // plugin: 'cornerstone',
height: '100%',
width: '100%',
}, },
], ],
}, },
@@ -34,21 +34,34 @@ const defaultState = {
* @param {Object} [action.viewportSpecificData] * @param {Object} [action.viewportSpecificData]
*/ */
const viewports = (state = defaultState, action) => { const viewports = (state = defaultState, action) => {
let currentData;
let viewportSpecificData; let viewportSpecificData;
let useActiveViewport = false; let useActiveViewport = false;
switch (action.type) { switch (action.type) {
case SET_VIEWPORT_LAYOUT_AND_DATA:
return Object.assign({}, state, {
viewportSpecificData: action.viewportSpecificData,
layout: action.layout,
});
case SET_VIEWPORT_ACTIVE: case SET_VIEWPORT_ACTIVE:
return Object.assign({}, state, { return Object.assign({}, state, {
activeViewportIndex: action.viewportIndex, activeViewportIndex: action.viewportIndex,
}); });
case SET_VIEWPORT_LAYOUT: case SET_VIEWPORT_LAYOUT: {
return Object.assign({}, state, { layout: action.layout }); const { numRows, numColumns, viewports } = action;
const layout = {
viewports: [...viewports],
};
return Object.assign({}, state, { numRows, numColumns, layout });
}
case SET_VIEWPORT_LAYOUT_AND_DATA: {
const { numRows, numColumns, viewports, viewportSpecificData } = action;
const layout = {
viewports: [...viewports],
};
return Object.assign({}, state, {
numRows,
numColumns,
layout,
viewportSpecificData: cloneDeep(viewportSpecificData),
});
}
case SET_VIEWPORT: { case SET_VIEWPORT: {
const layout = cloneDeep(state.layout); const layout = cloneDeep(state.layout);
const hasPlugin = action.data && action.data.plugin; const hasPlugin = action.data && action.data.plugin;
@@ -71,14 +84,24 @@ const viewports = (state = defaultState, action) => {
// Allow fall-through // Allow fall-through
// eslint-disable-next-line // eslint-disable-next-line
case SET_SPECIFIC_DATA: { case SET_SPECIFIC_DATA: {
const layout = cloneDeep(state.layout);
const hasPlugin = action.data && action.data.plugin;
const viewportIndex = useActiveViewport const viewportIndex = useActiveViewport
? state.activeViewportIndex ? state.activeViewportIndex
: action.viewportIndex; : action.viewportIndex;
currentData = cloneDeep(state.viewportSpecificData[viewportIndex]) || {}; const { dom } = state.viewportSpecificData[viewportIndex];
viewportSpecificData = cloneDeep(state.viewportSpecificData);
viewportSpecificData[viewportIndex] = merge({}, currentData, action.data);
return Object.assign({}, state, { viewportSpecificData }); viewportSpecificData = cloneDeep(state.viewportSpecificData);
viewportSpecificData[viewportIndex] = {
dom,
...action.data,
};
if (hasPlugin) {
layout.viewports[viewportIndex].plugin = action.data.plugin;
}
return Object.assign({}, state, { layout, viewportSpecificData });
} }
case CLEAR_VIEWPORT: case CLEAR_VIEWPORT:
viewportSpecificData = cloneDeep(state.viewportSpecificData); viewportSpecificData = cloneDeep(state.viewportSpecificData);
@@ -6,13 +6,10 @@ describe('viewports reducer', () => {
it('should return the initial state', () => { it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual({ expect(reducer(undefined, {})).toEqual({
activeViewportIndex: 0, activeViewportIndex: 0,
numRows: 1,
numColumns: 1,
layout: { layout: {
viewports: [ viewports: [{}],
{
height: '100%',
width: '100%',
},
],
}, },
viewportSpecificData: {}, viewportSpecificData: {},
}); });
@@ -34,23 +31,25 @@ describe('viewports reducer', () => {
it('should handle SET_VIEWPORT_LAYOUT', () => { it('should handle SET_VIEWPORT_LAYOUT', () => {
const setViewportLayoutAction = { const setViewportLayoutAction = {
type: types.SET_VIEWPORT_LAYOUT, type: types.SET_VIEWPORT_LAYOUT,
layout: { numRows: 1,
viewports: [ numColumns: 2,
{ viewports: [
height: '100%', {
width: '50%', plugin: 'cornerstone',
}, },
{ {
height: '100%', plugin: 'vtk',
width: '50%', },
}, ],
],
},
}; };
const updatedState = reducer({}, setViewportLayoutAction); const updatedState = reducer({}, setViewportLayoutAction);
expect(updatedState.layout).toEqual(setViewportLayoutAction.layout); expect(updatedState.numRows).toEqual(setViewportLayoutAction.numRows);
expect(updatedState.numColumns).toEqual(setViewportLayoutAction.numColumns);
expect(updatedState.layout.viewports).toEqual(
setViewportLayoutAction.viewports
);
}); });
// If there were previous keys, this would have // If there were previous keys, this would have
+28
View File
@@ -0,0 +1,28 @@
const SessionStorageApi = window.sessionStorage;
const sessionStorageKey = 'state';
export const loadState = () => {
try {
const serializedState = SessionStorageApi.getItem(sessionStorageKey);
if (!serializedState) {
return undefined;
}
return JSON.parse(serializedState);
} catch (e) {
return undefined;
}
};
export const saveState = state => {
try {
const serializedState = JSON.stringify(state);
SessionStorageApi.setItem(sessionStorageKey, serializedState);
} catch (e) {}
};
const sessionStorage = {
saveState,
loadState,
};
export default sessionStorage;
@@ -0,0 +1,25 @@
import DICOMWeb from '../DICOMWeb/';
import isLowPriorityModality from '../utils/isLowPriorityModality';
const INFO = Symbol('INFO');
/**
* Creates an object with processed series information and saves its reference
* inside the series object itself.
* @param {Object} series The raw series object
* @returns {Object} object containing some useful info from given series
*/
export default function getSeriesInfo(series) {
let info = series[INFO];
if (!info) {
const modality = DICOMWeb.getString(series['00080060'], '').toUpperCase();
info = Object.freeze({
modality,
isLowPriority: isLowPriorityModality(modality),
seriesInstanceUid: DICOMWeb.getString(series['0020000E']),
seriesNumber: DICOMWeb.getNumber(series['00200011'], 0) || 0,
});
series[INFO] = info;
}
return info;
}
@@ -7,13 +7,16 @@ import { retrieveStudyMetadata } from './retrieveStudyMetadata';
* This function calls retrieveStudyMetadata several times, asynchronously, * This function calls retrieveStudyMetadata several times, asynchronously,
* and waits for all of the results to be returned. * and waits for all of the results to be returned.
* *
* @param studyInstanceUids The UIDs of the Studies to be retrieved * @param {Object} server Object with server configuration parameters
* @return Promise * @param {Array} studyInstanceUids The UIDs of the Studies to be retrieved
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @returns {Promise} that will be resolved with the metadata or rejected with the error
*/ */
export default function retrieveStudiesMetadata( export default function retrieveStudiesMetadata(
server, server,
studyInstanceUids, studyInstanceUids,
seriesInstanceUids filters
) { ) {
// Create an empty array to store the Promises for each metaData retrieval call // Create an empty array to store the Promises for each metaData retrieval call
const promises = []; const promises = [];
@@ -21,11 +24,7 @@ export default function retrieveStudiesMetadata(
// Loop through the array of studyInstanceUids // Loop through the array of studyInstanceUids
studyInstanceUids.forEach(function(studyInstanceUid) { studyInstanceUids.forEach(function(studyInstanceUid) {
// Send the call and resolve or reject the related promise based on its outcome // Send the call and resolve or reject the related promise based on its outcome
const promise = retrieveStudyMetadata( const promise = retrieveStudyMetadata(server, studyInstanceUid, filters);
server,
studyInstanceUid,
seriesInstanceUids
);
// Add the current promise to the array of promises // Add the current promise to the array of promises
promises.push(promise); promises.push(promise);
@@ -1,27 +1,29 @@
import RetrieveMetadata from './services/wado/retrieveMetadata.js'; import RetrieveMetadata from './services/wado/retrieveMetadata.js';
const module = 'RetrieveStudyMetadata'; const moduleName = 'RetrieveStudyMetadata';
// Cache for promises. Prevents unnecessary subsequent calls to the server // Cache for promises. Prevents unnecessary subsequent calls to the server
const StudyMetaDataPromises = new Map(); const StudyMetaDataPromises = new Map();
/** /**
* Retrieves study metadata * Retrieves study metadata
* *
* @param {Object} server * @param {Object} server Object with server configuration parameters
* @param {string} studyInstanceUid The UID of the Study to be retrieved * @param {string} studyInstanceUid The UID of the Study to be retrieved
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @returns {Promise} that will be resolved with the metadata or rejected with the error * @returns {Promise} that will be resolved with the metadata or rejected with the error
*/ */
export function retrieveStudyMetadata(server, studyInstanceUid) { export function retrieveStudyMetadata(server, studyInstanceUid, filters) {
// @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all // @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all
// and further requests for that metadata will always fail. On failure, we probably need to remove the // and further requests for that metadata will always fail. On failure, we probably need to remove the
// corresponding promise from the "StudyMetaDataPromises" map... // corresponding promise from the "StudyMetaDataPromises" map...
if (!server) { if (!server) {
throw new Error(`${module}: Required 'server' parameter not provided.`); throw new Error(`${moduleName}: Required 'server' parameter not provided.`);
} }
if (!studyInstanceUid) { if (!studyInstanceUid) {
throw new Error( throw new Error(
`${module}: Required 'studyInstanceUid' parameter not provided.` `${moduleName}: Required 'studyInstanceUid' parameter not provided.`
); );
} }
@@ -32,7 +34,7 @@ export function retrieveStudyMetadata(server, studyInstanceUid) {
// Create a promise to handle the data retrieval // Create a promise to handle the data retrieval
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
RetrieveMetadata(server, studyInstanceUid).then(function(data) { RetrieveMetadata(server, studyInstanceUid, filters).then(function(data) {
resolve(data); resolve(data);
}, reject); }, reject);
}); });
@@ -30,7 +30,7 @@ function dateToString(date) {
function getQIDOQueryParams(filter, serverSupportsQIDOIncludeField) { function getQIDOQueryParams(filter, serverSupportsQIDOIncludeField) {
const commaSeparatedFields = [ const commaSeparatedFields = [
'00081030', // Study Description '00081030', // Study Description
'00080060', //Modality '00080060', // Modality
// Add more fields here if you want them in the result // Add more fields here if you want them in the result
].join(','); ].join(',');
@@ -1,469 +1,30 @@
import { api } from 'dicomweb-client'; import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync';
import DICOMWeb from '../../../DICOMWeb/'; import RetrieveMetadataLoaderAsync from './retrieveMetadataLoaderAsync';
const WADOProxy = {
convertURL: (url, server) => {
// TODO: Remove all WADOProxy stuff from this file
return url;
},
};
function parseFloatArray(obj) {
const result = [];
if (!obj) {
return result;
}
const objs = obj.split('\\');
for (let i = 0; i < objs.length; i++) {
result.push(parseFloat(objs[i]));
}
return result;
}
/** /**
* Simple cache schema for retrieved color palettes. * Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series
*/ * will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed.
const paletteColorCache = {
count: 0,
maxAge: 24 * 60 * 60 * 1000, // 24h cache?
entries: {},
isValidUID: function(paletteUID) {
return typeof paletteUID === 'string' && paletteUID.length > 0;
},
get: function(paletteUID) {
let entry = null;
if (this.entries.hasOwnProperty(paletteUID)) {
entry = this.entries[paletteUID];
// check how the entry is...
if (Date.now() - entry.time > this.maxAge) {
// entry is too old... remove entry.
delete this.entries[paletteUID];
this.count--;
entry = null;
}
}
return entry;
},
add: function(entry) {
if (this.isValidUID(entry.uid)) {
let paletteUID = entry.uid;
if (this.entries.hasOwnProperty(paletteUID) !== true) {
this.count++; // increment cache entry count...
}
entry.time = Date.now();
this.entries[paletteUID] = entry;
// @TODO: Add logic to get rid of old entries and reduce memory usage...
}
},
};
/** Returns a WADO url for an instance
* *
* @param studyInstanceUid * @param {Object} server Object with server configuration parameters
* @param seriesInstanceUid * @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
* @param sopInstanceUid * @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @returns {string} * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @returns {Object} A study descriptor object
*/ */
function buildInstanceWadoUrl( async function RetrieveMetadata(server, studyInstanceUid, filters = {}) {
server, const RetrieveMetadataLoader =
studyInstanceUid, server.enableStudyLazyLoad !== false
seriesInstanceUid, ? RetrieveMetadataLoaderAsync
sopInstanceUid : RetrieveMetadataLoaderSync;
) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO'); const retrieveMetadataLoader = new RetrieveMetadataLoader(
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
return `${
server.wadoRoot
}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frame
) {
const baseWadoRsUri = buildInstanceWadoRsUri(
server, server,
studyInstanceUid, studyInstanceUid,
seriesInstanceUid, filters
sopInstanceUid
); );
frame = frame != null || 1; const studyMetadata = retrieveMetadataLoader.execLoad();
return `${baseWadoRsUri}/frames/${frame}`; return studyMetadata;
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0]
const byteArray = new Uint8Array(arraybuffer)
const lut = []
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
let entry;
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
const promises = [r, g, b];
Promise.all(promises).then(args => {
entry = {
red: args[0],
green: args[1],
blue: args[2],
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime',
};
if (!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if (
radiopharmaceuticalInfo === undefined ||
!radiopharmaceuticalInfo.Value ||
!radiopharmaceuticalInfo.Value.length
) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(
firstPetRadiopharmaceuticalInfo['00181072']
),
radionuclideTotalDose: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181074']
),
radionuclideHalfLife: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181075']
),
};
}
/**
* Parses result data from a WADO search into Study MetaData
* Returns an object populated with study metadata, including the
* series list.
*
* @param server
* @param studyInstanceUid
* @param resultData
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
if (!resultData.length) {
return;
}
const anInstance = resultData[0];
if (!anInstance) {
return;
}
// TODO: Pass a reference ID to the server instead of including the URLs here
const studyData = {
seriesList: [],
studyInstanceUid,
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(anInstance['00100010']),
patientId: DICOMWeb.getString(anInstance['00100020']),
patientAge: DICOMWeb.getNumber(anInstance['00101010']),
patientSize: DICOMWeb.getNumber(anInstance['00101020']),
patientWeight: DICOMWeb.getNumber(anInstance['00101030']),
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
studyDate: DICOMWeb.getString(anInstance['00080020']),
modalities: DICOMWeb.getString(anInstance['00080061']),
studyDescription: DICOMWeb.getString(anInstance['00081030']),
imageCount: DICOMWeb.getString(anInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
institutionName: DICOMWeb.getString(anInstance['00080080']),
};
const seriesMap = {};
await Promise.all(
resultData.map(async function(instance) {
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesInstanceUid: seriesInstanceUid,
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
seriesMap[seriesInstanceUid] = series;
studyData.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const instanceSummary = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(
DICOMWeb.getString(instance['00181065'])
),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (instanceSummary.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
instanceSummary.paletteColorLookupTableUID = palettes.uid;
}
instanceSummary.redPaletteColorLookupTableData = palettes.red;
instanceSummary.greenPaletteColorLookupTableData = palettes.green;
instanceSummary.bluePaletteColorLookupTableData = palettes.blue;
instanceSummary.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
instanceSummary.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
instanceSummary.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(instanceSummary);
})
);
return studyData;
}
/**
* Retrieve Study MetaData from a DICOM server using a WADO call
*
* @param server
* @param studyInstanceUid
* @returns {Promise}
*/
async function RetrieveMetadata(server, studyInstanceUid) {
const config = {
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
studyInstanceUID: studyInstanceUid,
};
return dicomWeb.retrieveStudyMetadata(options).then(result => {
return resultDataToStudyMetadata(server, studyInstanceUid, result);
});
} }
export default RetrieveMetadata; export default RetrieveMetadata;
@@ -0,0 +1,58 @@
/**
* Class to define inheritance of load retrieve strategy.
* The process can be async load (lazy) or sync load
*
* There are methods that must be implemented at consumer level
* To retrieve study call execLoad
*/
export default class RetrieveMetadataLoader {
/**
* @constructor
* @param {Object} server Object with server configuration parameters
* @param {Array} studyInstanceUID Study instance ui to be retrieved
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
*/
constructor(server, studyInstanceUID, filters = {}) {
this.server = server;
this.studyInstanceUID = studyInstanceUID;
this.filters = filters;
}
async execLoad() {
await this.configLoad();
const preLoadData = await this.preLoad();
const loadData = await this.load(preLoadData);
const postLoadData = await this.posLoad(loadData);
return postLoadData;
}
/**
* It iterates over given loaders running each one. Loaders parameters must be bind when getting it.
* @param {Array} loaders - array of loader to retrieve data.
*/
async runLoaders(loaders) {
let result;
for (const loader of loaders) {
try {
result = await loader();
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
} catch (e) {}
}
if (loaders.next().done && !result) {
throw 'cant find data';
}
return result;
}
// Methods to be overwrite
async configLoad() {}
async preLoad() {}
async load(preLoadData) {}
async posLoad(loadData) {}
}
@@ -0,0 +1,145 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import RetrieveMetadataLoader from './retrieveMetadataLoader';
import { sortStudySeries, sortingCriteria } from '../../sortStudy';
import getSeriesInfo from '../../getSeriesInfo';
import {
createStudyFromSOPInstanceList,
addInstancesToStudy,
} from './studyInstanceHelpers';
/**
* Map seriesList to an array of seriesInstanceUid
* @param {Arrays} seriesList list of Series Instance UIDs
* @returns {Arrays} A list of Series Instance UIDs
*/
function mapStudySeries(seriesList) {
return seriesList.map(series => getSeriesInfo(series).seriesInstanceUid);
}
function attachSeriesLoader(server, study, seriesLoader) {
study.seriesLoader = Object.freeze({
hasNext() {
return seriesLoader.hasNext();
},
async next() {
const series = await seriesLoader.next();
await addInstancesToStudy(server, study, series.sopInstances);
return study.seriesMap[series.seriesInstanceUID];
},
});
}
/**
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
* @param {string} studyInstanceUID The Study Instance UID from which series will be loaded
* @param {Array} seriesInstanceUIDList A list of Series Instance UIDs
* @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID
*/
function makeSeriesAsyncLoader(
dicomWebClient,
studyInstanceUID,
seriesInstanceUIDList
) {
return Object.freeze({
hasNext() {
return seriesInstanceUIDList.length > 0;
},
async next() {
const seriesInstanceUID = seriesInstanceUIDList.shift();
const sopInstances = await dicomWebClient.retrieveSeriesMetadata({
studyInstanceUID,
seriesInstanceUID,
});
return { studyInstanceUID, seriesInstanceUID, sopInstances };
},
});
}
/**
* Class for async load of study metadata.
* It inherits from RetrieveMetadataLoader
*
* It loads the one series and then append to seriesLoader the others to be consumed/loaded
*/
export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader {
configLoad() {
const { server } = this;
const client = new api.DICOMwebClient({
url: server.qidoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
this.client = client;
}
/**
* @returns {Array} Array of preLoaders. To be consumed as queue
*/
*getPreLoaders() {
const preLoaders = [];
const {
studyInstanceUID,
filters: { seriesInstanceUID } = {},
client,
} = this;
if (seriesInstanceUID) {
const options = {
studyInstanceUID,
queryParams: { SeriesInstanceUID: seriesInstanceUID },
};
preLoaders.push(client.searchForSeries.bind(client, options));
}
// Fallback preloader
preLoaders.push(client.searchForSeries.bind(client, { studyInstanceUID }));
yield* preLoaders;
}
async preLoad() {
const preLoaders = this.getPreLoaders();
const result = await this.runLoaders(preLoaders);
const seriesSorted = sortStudySeries(
result,
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
);
const seriesInstanceUidsMap = mapStudySeries(seriesSorted);
return seriesInstanceUidsMap;
}
async load(preLoadData) {
const { client, studyInstanceUID } = this;
const seriesAsyncLoader = makeSeriesAsyncLoader(
client,
studyInstanceUID,
preLoadData
);
const firstSeries = await seriesAsyncLoader.next();
return {
sopInstances: firstSeries.sopInstances,
asyncLoader: seriesAsyncLoader,
};
}
async posLoad(loadData) {
const { server } = this;
const { sopInstances, asyncLoader } = loadData;
const study = await createStudyFromSOPInstanceList(server, sopInstances);
if (asyncLoader.hasNext()) {
attachSeriesLoader(server, study, asyncLoader);
}
return study;
}
}
@@ -0,0 +1,76 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import { createStudyFromSOPInstanceList } from './studyInstanceHelpers';
import RetrieveMetadataLoader from './retrieveMetadataLoader';
/**
* Class for sync load of study metadata.
* It inherits from RetrieveMetadataLoader
*
* A list of loaders (getLoaders) can be created so, it will be applied a fallback load strategy.
* I.e Retrieve metadata using all loaders possibilities.
*/
export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader {
getOptions() {
const { studyInstanceUID, filters } = this;
const options = {
studyInstanceUID,
};
const { seriesInstanceUID } = filters;
if (seriesInstanceUID) {
options['seriesInstanceUID'] = seriesInstanceUID;
}
return options;
}
/**
* @returns {Array} Array of loaders. To be consumed as queue
*/
*getLoaders() {
const loaders = [];
const {
studyInstanceUID,
filters: { seriesInstanceUID } = {},
client,
} = this;
if (seriesInstanceUID) {
loaders.push(
client.retrieveSeriesMetadata.bind(client, {
studyInstanceUID,
seriesInstanceUID,
})
);
}
loaders.push(
client.retrieveStudyMetadata.bind(client, { studyInstanceUID })
);
yield* loaders;
}
configLoad() {
const { server } = this;
const client = new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
this.client = client;
}
async load(preLoadData) {
const loaders = this.getLoaders();
const result = this.runLoaders(loaders);
return result;
}
async posLoad(loadData) {
const { server } = this;
return createStudyFromSOPInstanceList(server, loadData);
}
}
@@ -0,0 +1,413 @@
import DICOMWeb from '../../../DICOMWeb';
const WADOProxy = {
convertURL: (url, server) => {
// TODO: Remove all WADOProxy stuff from this file
return url;
},
};
function parseFloatArray(obj) {
const result = [];
if (!obj) {
return result;
}
const objs = obj.split('\\');
for (let i = 0; i < objs.length; i++) {
result.push(parseFloat(objs[i]));
}
return result;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration parameters
* @param {Object} aSopInstance a SOP Instance from which study information will be added
*/
function createStudy(server, aSopInstance) {
// TODO: Pass a reference ID to the server instead of including the URLs here
return {
seriesList: [],
seriesMap: Object.create(null),
seriesLoader: null,
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(aSopInstance['00100010']),
patientId: DICOMWeb.getString(aSopInstance['00100020']),
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
modalities: DICOMWeb.getString(aSopInstance['00080061']),
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
};
}
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
* @param seriesInstanceUid
* @param sopInstanceUid
* @returns {string}
*/
function buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frame
) {
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
frame = frame != null || 1;
return `${baseWadoRsUri}/frames/${frame}`;
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime',
};
if (!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if (
radiopharmaceuticalInfo === undefined ||
!radiopharmaceuticalInfo.Value ||
!radiopharmaceuticalInfo.Value.length
) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(
firstPetRadiopharmaceuticalInfo['00181072']
),
radionuclideTotalDose: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181074']
),
radionuclideHalfLife: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181075']
),
};
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length &&
SourceImageSequence.Value[0]['00081155'].Value
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
async function makeSOPInstance(server, study, instance) {
const { studyInstanceUid } = study;
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = study.seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesInstanceUid,
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
study.seriesMap[seriesInstanceUid] = series;
study.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const sopInstance = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
sopInstance.paletteColorLookupTableUID = palettes.uid;
}
sopInstance.redPaletteColorLookupTableData = palettes.red;
sopInstance.greenPaletteColorLookupTableData = palettes.green;
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(sopInstance);
return sopInstance;
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
let entry;
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
const promises = [r, g, b];
Promise.all(promises).then(args => {
entry = {
red: args[0],
green: args[1],
blue: args[2],
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration parameters
* @param {Object} study The study descriptor to which the given SOP instances will be added
* @param {Array} sopInstanceList A list of SOP instance objects
*/
async function addInstancesToStudy(server, study, sopInstanceList) {
return Promise.all(
sopInstanceList.map(function(sopInstance) {
return makeSOPInstance(server, study, sopInstance);
})
);
}
const createStudyFromSOPInstanceList = async (server, sopInstanceList) => {
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
const firstSopInstance = sopInstanceList[0];
const study = createStudy(server, firstSopInstance);
await addInstancesToStudy(server, study, sopInstanceList);
return study;
}
throw new Error('Failed to create study out of provided SOP instance list');
};
export { createStudyFromSOPInstanceList, addInstancesToStudy };
+86 -7
View File
@@ -1,17 +1,96 @@
import getSeriesInfo from './getSeriesInfo';
/** /**
* Sorts the series and instances inside a study instance by their series * Series sorting criteria: series considered low priority are moved to the end
* and instance numbers in ascending order. * of the list and series number is used to break ties
* @param {Object} firstSeries
* @param {Object} secondSeries
*/
function seriesInfoSortingCriteria(firstSeries, secondSeries) {
const a = getSeriesInfo(firstSeries);
const b = getSeriesInfo(secondSeries);
if (!a.isLowPriority && b.isLowPriority) {
return -1;
}
if (a.isLowPriority && !b.isLowPriority) {
return 1;
}
return a.seriesNumber - b.seriesNumber;
}
const seriesSortCriteria = {
default: (a, b) => a.seriesNumber - b.seriesNumber,
seriesInfoSortingCriteria,
};
const instancesSortCriteria = {
default: (a, b) => a.instanceNumber - b.instanceNumber,
};
const sortingCriteria = {
seriesSortCriteria,
instancesSortCriteria,
};
/**
* Sorts given seriesList (given param is modified)
* The default criteria is based on series number in ascending order.
*
* @param {Array} seriesList List of series
* @param {function} seriesSortingCriteria method for sorting
* @returns {Array} sorted seriesList object
*/
const sortStudySeries = (
seriesList,
seriesSortingCriteria = seriesSortCriteria.default
) => {
return seriesList.sort(seriesSortingCriteria);
};
/**
* Sorts given instancesList (given param is modified)
* The default criteria is based on instance number in ascending order.
*
* @param {Array} instancesList List of series
* @param {function} instancesSortingCriteria method for sorting
* @returns {Array} sorted instancesList object
*/
const sortStudyInstances = (
instancesList,
instancesSortingCriteria = instancesSortCriteria.default
) => {
return instancesList.sort(instancesSortingCriteria);
};
/**
* Sorts the series and instances (by default) inside a study instance based on sortingCriteria (given param is modified)
* The default criteria is based on series and instance numbers in ascending order.
* *
* @param {Object} study The study instance * @param {Object} study The study instance
* @param {boolean} [deepSort = true] to sort instance also
* @param {function} [seriesSortingCriteria = seriesSortCriteria.default] method for sorting series
* @param {function} [instancesSortingCriteria = instancesSortCriteria.default] method for sorting instances
* @returns {Object} sorted study object
*/ */
export default function sortStudy(study) { export default function sortStudy(
study,
deepSort = true,
seriesSortingCriteria = seriesSortCriteria.default,
instancesSortingCriteria = instancesSortCriteria.default
) {
if (!study || !study.seriesList) { if (!study || !study.seriesList) {
throw new Error('Insufficient study data was provided to sortStudy'); throw new Error('Insufficient study data was provided to sortStudy');
} }
study.seriesList.sort((a, b) => a.seriesNumber - b.seriesNumber); sortStudySeries(study.seriesList, seriesSortingCriteria);
study.seriesList.forEach(series => { if (deepSort) {
series.instances.sort((a, b) => a.instanceNumber - b.instanceNumber); study.seriesList.forEach(series => {
}); sortStudyInstances(series.instances, instancesSortingCriteria);
});
}
return study;
} }
export { sortStudySeries, sortStudyInstances, sortingCriteria };
@@ -11,9 +11,6 @@ describe('addServers', () => {
qidoSupportsIncludeField: true, qidoSupportsIncludeField: true,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
requestOptions: {
requestFromBrowser: true,
},
}, },
], ],
oidc: [ oidc: [
@@ -52,7 +49,6 @@ describe('addServers', () => {
name: 'DCM4CHEE', name: 'DCM4CHEE',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true, qidoSupportsIncludeField: true,
requestOptions: { requestFromBrowser: true },
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
type: 'dicomWeb', type: 'dicomWeb',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
Loaded 100 of 350 files, more files were not shown because too many files have changed in this diff. Show more