Compare commits

...
Author SHA1 Message Date
ohif-bot c4adcfd39e chore(release): publish [skip ci]
- @ohif/viewer@2.7.1
2019-11-12 12:23:55 +00:00
ladeirarodolfo 0fbaf95971 fix: 🐛 Fix for JS breaking on header (#1164) 2019-11-12 07:13:59 -05:00
fossabot a2a7e05e7d docs: Add license scan report and status (#1161)
Signed-off-by: fossabot <badges@fossa.io>
2019-11-11 21:43:24 -05:00
ohif-bot 9da8647c84 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.4
 - @ohif/core@1.8.0
 - @ohif/ui@0.61.0
 - @ohif/viewer@2.7.0
2019-11-12 02:17:40 +00:00
Igor Octaviano e62f5f8dd2 feat: 🎸 Update hotkeys and user preferences modal (#1135)
* feat: 🎸 Update hotkeys and user preferences modal

This feature fix incompatibility with existent hotkeys component and
adds user preferences modal back

Closes: #923

* Update preferences structure in store

* Hide window level section of user preferences

* Update modal to reflect current hotkey value

* Clone object with hotkeys before passing to manager

* CR Update: Extract hotkeys manager format code to manager

* Fix broken cypress test

* Use new modal provider

* Rename hotkeyDefinitions in hotkeyspreferences and use array as representation

* Update study test and remove unused styles
2019-11-11 21:03:36 -05:00
ohif-bot dc6e9d6bb5 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.3
 - @ohif/viewer@2.6.4
2019-11-11 09:48:18 +00:00
Danny Brown 84a097212b fix: mpr2d vtkjs viewport does not render if range is set to NaN values (#1157)
* chore: fix proptypes declaration

* fix: seperate helper method to calculate our initial range

* chore: remove unused import

* chore: update studies propType from object to array
2019-11-11 09:38:36 +00:00
ohif-bot 2381810ba9 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.4.1
 - @ohif/extension-vtk@0.53.2
 - @ohif/core@1.7.1
 - @ohif/viewer@2.6.3
2019-11-08 13:07:35 +00:00
Erik Ziegler 31b1adfa59 fix: Add a fallback metadata provider which pulls metadata from WADO-… (#1158)
* fix: Add a fallback metadata provider which pulls metadata from WADO-RS cache when WADO-URI is used.

* cleanup
2019-11-08 13:55:56 +01:00
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
247 changed files with 10984 additions and 4239 deletions

No files matched your search

+66 -4
View File
@@ -13,7 +13,7 @@ version: 2.1
##
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@1.10.0
cypress: cypress-io/cypress@1.11.0
defaults: &defaults
docker:
@@ -198,7 +198,7 @@ jobs:
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
- run: ~/repo/.circleci/build-and-publish-docs.sh
DOCKER_PUBLISH:
DOCKER_MASTER_PUBLISH:
<<: *defaults
steps:
- attach_workspace:
@@ -227,6 +227,43 @@ jobs:
docker push ohif/$IMAGE_NAME:latest
fi
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
steps:
@@ -288,10 +325,13 @@ workflows:
# E2E: PWA
- cypress/run:
name: 'E2E: PWA'
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
@@ -307,14 +347,18 @@ workflows:
# 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
@@ -324,6 +368,17 @@ workflows:
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
RELEASE:
jobs:
@@ -334,16 +389,19 @@ workflows:
# E2E: PWA + Persist
- cypress/run:
name: 'E2E: PWA'
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: true
record: false
store_artifacts: true
working_directory: platform/viewer
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:
@@ -356,14 +414,18 @@ workflows:
# 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
@@ -393,6 +455,6 @@ workflows:
requires:
- VIEWER_DEMO_BUILD
# Update hub.docker.org
- DOCKER_PUBLISH:
- DOCKER_MASTER_PUBLISH:
requires:
- NPM_PUBLISH
+1
View File
@@ -0,0 +1 @@
PERCY_TOKEN=<your token here>
+1
View File
@@ -16,6 +16,7 @@
},
"globals": {
"cy": true,
"before": true,
"context": true,
"Cypress": 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 -->
+1
View File
@@ -21,6 +21,7 @@ npm-debug.log
package-lock.json
yarn-error.log
.DS_Store
.env
# Common Example Data Directories
sampledata/
+9 -7
View File
@@ -1,10 +1,8 @@
{
"editor.rulers": [80, 120],
// ===
// Spacing
// ===
"editor.insertSpaces": true,
"editor.tabSize": 2,
"editor.trimAutoWhitespace": true,
@@ -12,19 +10,23 @@
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
// ===
// Event Triggers
// ===
"editor.formatOnSave": true,
"eslint.autoFixOnSave": true,
"eslint.run": "onSave",
"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",
"workbench.colorCustomizations": {}
}
+10
View File
@@ -1,5 +1,9 @@
// ~~ ENV
const dotenv = require('dotenv');
//
const path = require('path');
const webpack = require('webpack');
const PACKAGE = require('../platform/viewer/package.json');
// ~~ RULES
const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
@@ -9,6 +13,10 @@ 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 }) => {
if (!process.env.NODE_ENV) {
@@ -68,6 +76,8 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'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
+1
View File
@@ -44,6 +44,7 @@ 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
+7
View File
@@ -22,10 +22,12 @@
[![NPM downloads][npm-downloads-image]][npm-url]
[![Pulls][docker-pulls-img]][docker-image-url]
[![MIT License][license-image]][license-url]
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_shield)
[![Netlify Status][netlify-image]][netlify-url]
[![CircleCI][circleci-image]][circleci-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)
<!-- prettier-ignore-end -->
@@ -288,6 +290,8 @@ MIT © [OHIF](https://github.com/OHIF)
[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-url]: LICENSE
[percy-image]: https://percy.io/static/images/percy-badge.svg
[percy-url]: https://percy.io/Open-Health-Imaging-Foundation/OHIF-Viewer
<!-- Links -->
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
@@ -321,3 +325,6 @@ MIT © [OHIF](https://github.com/OHIF)
[extension-vtk]: extensions/vtk/README.md
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
<!-- prettier-ignore-end -->
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
@@ -48,7 +48,7 @@ If everything worked as expected, you should have a new `dist/` directory in the
project's folder. It should roughly resemble the following:
```bash
dist/
<root>platform/viewer/dist/
├── app-config.js
├── app.bundle.js
├── app.css
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.0...@ohif/extension-cornerstone@1.4.1) (2019-11-08)
### Bug Fixes
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
# [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)
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "1.3.1",
"version": "1.4.1",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -48,6 +48,7 @@
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"lodash.throttle": "^4.1.1",
"query-string": "^6.8.3",
"react-cornerstone-viewport": "2.x.x"
}
}
+19
View File
@@ -2,6 +2,25 @@ import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
import queryString from 'query-string';
function fallbackMetaDataProvider (type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) {
return
}
// If you call for an WADO-URI imageId and get no
// metadata, try reformatting to WADO-RS imageId
const qs = queryString.parse(imageId);
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${qs.seriesUID}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
return cornerstone.metaData.get(type, wadoRsImageId);
}
// Add this fallback provider with a low priority so it is handled last
cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
/**
*
@@ -213,6 +213,16 @@ const definitions = [
commandName: 'setToolActive',
commandOptions: { toolName: 'Eraser' },
},
{
id: 'Download',
label: 'Download',
icon: 'create-screen-capture',
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'DOWNLOAD_SCREEN_SHOT',
},
},
],
},
];
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-html",
"version": "1.0.0",
"version": "1.0.1",
"description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF",
"license": "MIT",
@@ -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 PropTypes from 'prop-types';
import * as dcmjs from 'dcmjs';
import TypedArrayProp from './TypedArrayProp';
import './DicomHtmlViewport.css';
function getRelationshipString(data) {
@@ -30,7 +32,7 @@ function getValueString(data) {
const {
CodeMeaning,
CodeValue,
CodingSchemeDesignator
CodingSchemeDesignator,
} = data.ConceptNameCodeSequence;
return `${CodeMeaning} (${CodeValue}, ${CodingSchemeDesignator})`;
@@ -75,7 +77,7 @@ function constructContentSequence(data, header) {
}
const result = {
items
items,
};
if (header) {
@@ -106,9 +108,7 @@ const { DicomMetaDictionary, DicomMessage } = dcmjs.data;
function getMainData(data) {
const root = [];
const patientValue = `${data.PatientName} (${data.PatientSex}, #${
data.PatientID
})`;
const patientValue = `${data.PatientName} (${data.PatientSex}, #${data.PatientID})`;
root.push(getMainDataItem('Patient', patientValue));
const studyValue = data.StudyDescription;
@@ -117,15 +117,13 @@ function getMainData(data) {
const seriesValue = `${data.SeriesDescription} (#${data.SeriesNumber})`;
root.push(getMainDataItem('Series', seriesValue));
const manufacturerValue = `${data.Manufacturer} (${
data.ManufacturerModelName
}, #${data.DeviceSerialNumber})`;
const manufacturerValue = `${data.Manufacturer} (${data.ManufacturerModelName}, #${data.DeviceSerialNumber})`;
root.push(getMainDataItem('Manufacturer', manufacturerValue));
const mainDataObjects = {
CompletionFlag: 'Completion flag',
VerificationFlag: 'Verification flag'
VerificationFlag: 'Verification flag',
};
Object.keys(mainDataObjects).forEach(key => {
@@ -154,7 +152,7 @@ const getContentSequence = (data, level = 1) => {
const {
CodeMeaning,
CodeValue,
CodingSchemeDesignator
CodingSchemeDesignator,
} = data.ConceptNameCodeSequence;
header = `${CodeMeaning} (${CodeValue} - ${CodingSchemeDesignator})`;
@@ -198,11 +196,14 @@ function getMainDataItem(key, value) {
class DicomHtmlViewport extends Component {
state = {
content: null,
error: null
error: null,
};
static propTypes = {
byteArray: TypedArrayProp.uint8
byteArray: TypedArrayProp.uint8,
setViewportActive: PropTypes.func.isRequired,
viewportIndex: PropTypes.number.isRequired,
activeViewportIndex: PropTypes.number.isRequired,
};
componentDidMount() {
@@ -225,15 +226,34 @@ class DicomHtmlViewport extends Component {
);
this.setState({
content
content,
});
}
setViewportActiveHandler = () => {
const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
if (viewportIndex !== activeViewportIndex) {
setViewportActive();
}
};
render() {
const { content, error } = this.state;
return (
<div className={'DicomHtmlViewport'}>
{this.state.content}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
<div
data-cy="dicom-html-viewport"
className="DicomHtmlViewport"
onClick={this.setViewportActiveHandler}
onScroll={this.setViewportActiveHandler}
>
{content}
{error && <h2>{JSON.stringify(error)}</h2>}
</div>
);
}
@@ -1,7 +1,8 @@
import React, { Component } from 'react';
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 {
@@ -9,6 +10,7 @@ class OHIFDicomHtmlViewport extends Component {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
viewportData: PropTypes.object,
};
state = {
@@ -40,7 +42,10 @@ class OHIFDicomHtmlViewport extends Component {
return (
<>
{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>}
</>
+262
View File
@@ -3,6 +3,268 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.53.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.3...@ohif/extension-vtk@0.53.4) (2019-11-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.2...@ohif/extension-vtk@0.53.3) (2019-11-11)
### Bug Fixes
* mpr2d vtkjs viewport does not render if range is set to `NaN` values ([#1157](https://github.com/OHIF/Viewers/issues/1157)) ([84a0972](https://github.com/OHIF/Viewers/commit/84a097212babc0f98198b9ced1def9743973a5a8))
## [0.53.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.1...@ohif/extension-vtk@0.53.2) (2019-11-08)
**Note:** Version bump only for package @ohif/extension-vtk
## [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
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.52.7",
"version": "0.53.4",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -48,11 +48,12 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"react-vtkjs-viewport": "^0.1.6"
"lodash.throttle": "^4.1.1",
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^1.3.1",
"@ohif/ui": "^0.53.4",
"@ohif/core": "^1.8.0",
"@ohif/ui": "^0.61.0",
"cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
+7 -7
View File
@@ -1,6 +1,6 @@
import OHIF from "@ohif/core";
import { View2D } from "react-vtkjs-viewport";
import { connect } from "react-redux";
import OHIF from '@ohif/core';
import { View2D } from 'react-vtkjs-viewport';
import { connect } from 'react-redux';
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
// activeTool: activeButton && activeButton.command,
...dataFromStore,
enableStackPrefetch: isActive
enableStackPrefetch: isActive,
};
};
@@ -38,7 +38,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
setViewportSpecificData: data => {
dispatch(setViewportSpecificData(viewportIndex, data));
}
},
};
};
@@ -63,10 +63,10 @@ const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
// Store the API details for later
//setViewportSpecificData({ vtkApi: api });
if (afterCreation && typeof afterCreation === "function") {
if (afterCreation && typeof afterCreation === 'function') {
afterCreation(api);
}
}
},
};
return props;
};
+4 -4
View File
@@ -3,15 +3,15 @@
}
.loadingIndicator {
background-color: rgba(0, 0, 0, 0.75);
font-size: 18px;
height: 100%;
background-color: rgba(0, 0, 0, 0);
font-size: 8px;
height: 20%;
overflow: hidden;
position: absolute;
top: 0;
width: 100%;
z-index: 1;
/* Necessary for click-through to cornerstone element below */
/* Necessary for click-through to vtkjs element below */
pointer-events: none;
}
+4 -3
View File
@@ -1,7 +1,7 @@
import './LoadingIndicator.css';
import React, { PureComponent } from 'react';
import { withTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
class LoadingIndicator extends PureComponent {
@@ -35,7 +35,8 @@ class LoadingIndicator extends PureComponent {
<div className="imageViewerLoadingIndicator loadingIndicator">
<div className="indicatorContents">
<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}
</p>
</div>
@@ -46,4 +47,4 @@ class LoadingIndicator extends PureComponent {
}
}
export default LoadingIndicator;
export default withTranslation('Common')(LoadingIndicator);
+151 -54
View File
@@ -11,7 +11,6 @@ import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
const { StackManager } = OHIF.utils;
@@ -66,8 +65,16 @@ class OHIFVTKViewport extends Component {
};
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportData: PropTypes.shape({
studies: PropTypes.array,
displaySet: PropTypes.shape({
studyInstanceUid: PropTypes.string,
displaySetInstanceUid: PropTypes.string,
sopClassUids: PropTypes.arrayOf(PropTypes.string),
sopInstanceUid: PropTypes.string,
frameIndex: PropTypes.number,
}),
}),
viewportIndex: PropTypes.number,
children: PropTypes.node,
};
@@ -172,56 +179,64 @@ class OHIFVTKViewport extends Component {
default:
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
const loadImageDataPromise = loadImageData(imageDataObject);
return loadImageDataPromise.then(() => {
return {
data: imageDataObject.vtkImageData,
};
});
return imageDataObject;
}
};
getOrCreateVolume(data, displaySetInstanceUid) {
/**
*
*
* @param {object} imageDataObject
* @param {object} imageDataObject.vtkImageData
* @param {object} imageDataObject.imageMetaData0
* @param {number} [imageDataObject.imageMetaData0.windowWidth] - The volume's initial windowWidth
* @param {number} [imageDataObject.imageMetaData0.windowCenter] - The volume's initial windowCenter
* @param {string} imageDataObject.imageMetaData0.modality - CT, MR, PT, etc
* @param {string} displaySetInstanceUid
* @returns vtkVolumeActor
* @memberof OHIFVTKViewport
*/
getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
if (volumeCache[displaySetInstanceUid]) {
return volumeCache[displaySetInstanceUid];
}
const { vtkImageData, imageMetaData0 } = imageDataObject;
const { windowWidth, windowCenter, modality } = imageMetaData0;
const { lower, upper } = _getRangeFromWindowLevels(
windowWidth,
windowCenter,
modality
);
const volumeActor = vtkVolume.newInstance();
const volumeMapper = vtkVolumeMapper.newInstance();
volumeActor.setMapper(volumeMapper);
volumeMapper.setInputData(data);
volumeMapper.setInputData(vtkImageData);
const range = data
.getPointData()
.getScalars()
.getRange();
// TODO: For PET we might want to just set this to 0-5 SUV
volumeActor
.getProperty()
.getRGBTransferFunction(0)
.setRange(range[0], range[1]);
.setRange(lower, upper);
// TODO: Should look into implementing autoAdjustSampleDistance in vtk
const sampleDistance =
1.2 *
Math.sqrt(
data
.getSpacing()
.map(v => v * v)
.reduce((a, b) => a + b, 0)
);
const spacing = vtkImageData.getSpacing();
// Set the sample distance to half the mean length of one side. This is where the divide by 6 comes from.
// https://github.com/Kitware/VTK/blob/6b559c65bb90614fb02eb6d1b9e3f0fca3fe4b0b/Rendering/VolumeOpenGL2/vtkSmartVolumeMapper.cxx#L344
const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6;
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;
return volumeActor;
}
async setStateFromProps() {
setStateFromProps() {
const { studies, displaySet } = this.props.viewportData;
const {
studyInstanceUid,
@@ -238,8 +253,7 @@ class OHIFVTKViewport extends Component {
}
const sopClassUid = sopClassUids[0];
let { data, labelmap } = await this.getViewportData(
const imageDataObject = this.getViewportData(
studies,
studyInstanceUid,
displaySetInstanceUid,
@@ -248,29 +262,51 @@ class OHIFVTKViewport extends Component {
frameIndex
);
this.imageDataObject = imageDataObject;
// TODO: Temporarily disabling this since it is not yet
// being used and hurts performance significantly.
/*if (!labelmap) {
labelmap = createLabelMapImageData(data);
}*/
const volumeActor = this.getOrCreateVolume(data, displaySetInstanceUid);
const volumeActor = this.getOrCreateVolume(
imageDataObject,
displaySetInstanceUid
);
this.setState({
volumes: [volumeActor],
paintFilterBackgroundImageData: data,
paintFilterLabelMapImageData: labelmap,
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
paintFilterLabelMapImageData: null, // TODO
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() {
this.setStateFromProps();
}
componentWillUnmount() {
vtkViewportSubscriptionManager.unsubscribe(this.props.viewportIndex);
}
componentDidUpdate(prevProps) {
const { studies, displaySet } = this.props.viewportData;
const prevDisplaySet = prevProps.viewportData.displaySet;
@@ -285,6 +321,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() {
let childrenWithProps = null;
@@ -302,21 +369,23 @@ class OHIFVTKViewport extends Component {
return (
<>
{this.state.volumes ? (
<ConnectedVTKViewport
volumes={this.state.volumes}
paintFilterLabelMapImageData={
this.state.paintFilterLabelMapImageData
}
paintFilterBackgroundImageData={
this.state.paintFilterBackgroundImageData
}
viewportIndex={this.props.viewportIndex}
/>
) : (
<div style={style}>
<LoadingIndicator />
</div>
<div style={style}>
{!this.state.isLoaded && (
<LoadingIndicator percentComplete={this.state.percentComplete} />
)}
{this.state.volumes && (
<ConnectedVTKViewport
volumes={this.state.volumes}
paintFilterLabelMapImageData={
this.state.paintFilterLabelMapImageData
}
paintFilterBackgroundImageData={
this.state.paintFilterBackgroundImageData
}
viewportIndex={this.props.viewportIndex}
/>
)}
</div>
)}
{childrenWithProps}
</>
@@ -324,4 +393,32 @@ class OHIFVTKViewport extends Component {
}
}
/**
* Takes window levels and converts them to a range (lower/upper)
* for use with VTK RGBTransferFunction
*
* @private
* @param {number} [width] - the width of our window
* @param {number} [center] - the center of our window
* @param {string} [modality] - 'PT', 'CT', etc.
* @returns { lower, upper } - range
*/
function _getRangeFromWindowLevels(width, center, modality = undefined) {
const levelsAreNotNumbers = isNaN(center) || isNaN(width);
if (levelsAreNotNumbers) {
return { lower: 0, upper: 512 };
}
// For PET just set the range to 0-5 SUV
if (modality === 'PT') {
return { lower: 0, upper: 5 };
}
return {
lower: center - width / 2.0,
upper: center + width / 2.0,
};
}
export default OHIFVTKViewport;
+114 -176
View File
@@ -1,69 +1,19 @@
import {
vtkInteractorStyleMPRCrosshairs,
vtkInteractorStyleMPRWindowLevel,
vtkInteractorStyleMPRSlice,
vtkInteractorStyleMPRRotate,
vtkSVGCrosshairsWidget,
vtkSVGWidgetManager,
} from 'react-vtkjs-viewport';
import setMPRLayout from './utils/setMPRLayout.js';
import setViewportToVTK from './utils/setViewportToVTK.js';
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.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 throttle from 'lodash.throttle';
const { BlendMode } = Constants;
// TODO: Put this somewhere else
let apis = {};
let currentSlabThickness = 0.1;
let defaultSlabThickness = 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) {
const {
@@ -105,8 +55,6 @@ async function _getActiveViewportVTKApi(viewports) {
function _setView(api, sliceNormal, viewUp) {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const istyle = renderWindow.getInteractor().getInteractorStyle();
istyle.setSliceNormal(...sliceNormal);
istyle.setViewUp(...viewUp);
@@ -114,41 +62,6 @@ function _setView(api, sliceNormal, viewUp) {
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 currentViewport;
if (currentIStyle.getViewport && istyle.getViewport) {
currentViewport = currentIStyle.getViewport();
}
let currentSlabThickness;
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
currentSlabThickness = currentIStyle.getSlabThickness();
defaultSlabThickness = currentSlabThickness;
}
renderWindow.getInteractor().setInteractorStyle(istyle);
// TODO: Not sure why this is required the second time this function is called
istyle.setInteractor(renderWindow.getInteractor());
if (currentViewport) {
istyle.setViewport(currentViewport);
}
if (istyle.getVolumeMapper() !== api.volumes[0]) {
if (currentSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
istyle.setVolumeMapper(api.volumes[0]);
}
}
const actions = {
axial: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
@@ -175,52 +88,56 @@ const actions = {
apis.forEach(api => {
const istyle = vtkInteractorStyleMPRRotate.newInstance();
switchMPRInteractors(api, istyle);
api.setInteractorStyle({ istyle });
});
},
enableCrosshairsTool: () => {
apis.forEach((api, index) => {
apis.forEach((api, apiIndex) => {
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
switchMPRInteractors(api, istyle);
istyle.setCallback(getCrosshairCallbackForIndex(index));
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
});
},
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 => {
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
switchMPRInteractors(api, istyle);
api.setInteractorStyle({ istyle, callbacks });
});
},
setSlabThickness: ({ slabThickness }) => {
currentSlabThickness = slabThickness;
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
if (istyle.setSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
renderWindow.render();
api.setSlabThickness(slabThickness);
});
},
changeSlabThickness: ({ change }) => {
currentSlabThickness += change;
currentSlabThickness = Math.max(currentSlabThickness, 0.1);
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
if (istyle.setSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
renderWindow.render();
api.setSlabThickness(slabThickness);
});
},
setBlendModeToComposite: () => {
@@ -228,13 +145,15 @@ const actions = {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const slabThickness = api.getSlabThickness();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToComposite) {
mapper.setBlendModeToComposite();
}
if (istyle.setSlabThickness) {
istyle.setSlabThickness(defaultSlabThickness);
istyle.setSlabThickness(slabThickness);
}
renderWindow.render();
});
@@ -252,7 +171,6 @@ const actions = {
setBlendMode: ({ blendMode }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
api.volumes[0].getMapper().setBlendMode(blendMode);
@@ -260,78 +178,60 @@ const actions = {
});
},
mpr2d: async ({ viewports }) => {
// TODO push a lot of this backdoor logic lower down to the library level.
const displaySet =
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 {
apiByViewport = await setMPRLayout(displaySet);
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
} catch (error) {
throw new Error(error);
}
apis = apiByViewport;
if (cornerstoneVOI) {
setVOI(cornerstoneVOI);
}
const rgbTransferFunction = apiByViewport[0].volumes[0]
.getProperty()
.getRGBTransferFunction(0);
const onModifiedSubscription = rgbTransferFunction.onModified(() => {
apiByViewport.forEach(a => {
const renderWindow = a.genericRenderWindow.getRenderWindow();
renderWindow.render();
});
});
vtkViewportSubscriptionManager.pushSubscription(0, onModifiedSubscription);
apiByViewport.forEach((api, index) => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
// Add widgets and set default interactorStyle of each viewport.
apis.forEach((api, apiIndex) => {
api.addSVGWidget(
vtkSVGCrosshairsWidget.newInstance(),
'crosshairsWidget'
);
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
renderWindow.getInteractor().setInteractorStyle(istyle);
istyle.setVolumeMapper(api.volumes[0]);
istyle.setCallback(getCrosshairCallbackForIndex(index));
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);
istyle.setViewUp(0, -1, 0);
break;
case 1:
// sagittal
istyle.setSliceNormal(1, 0, 0);
istyle.setViewUp(0, 0, 1);
break;
case 2:
// Coronal
istyle.setSliceNormal(0, 1, 0);
istyle.setViewUp(0, 0, 1);
break;
}
renderWindow.render();
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
});
},
};
@@ -421,3 +321,41 @@ export default {
definitions,
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);
});
}
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Range, Checkbox, Select } from '@ohif/ui';
import { Range, Checkbox, OldSelect } from '@ohif/ui';
import './slab-thickness-toolbar-button.styl';
@@ -16,6 +16,10 @@ const ToolbarLabel = 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 (
@@ -33,6 +37,13 @@ const ToolbarSlider = props => {
);
};
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 {
@@ -184,12 +195,12 @@ function SlabThicknessToolbarComponent({
checked={state.modeChecked}
onChange={onChangeCheckbox}
></Checkbox>
<Select
<OldSelect
key="toolbar-select"
options={selectOptions}
value={selectOptions[INITIAL_OPTION_INDEX].value}
onChange={onChangeSelect}
></Select>
></OldSelect>
</div>
</div>
);
+17 -4
View File
@@ -1,11 +1,23 @@
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
export default function setMPRLayout(displaySet) {
export default function setMPRLayout(
displaySet,
viewportPropsArray,
numRows = 1,
numColumns = 1
) {
return new Promise((resolve, reject) => {
const viewports = [];
const numRows = 1;
const numColumns = 3;
const numViewports = numRows * numColumns;
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++) {
@@ -17,8 +29,8 @@ export default function setMPRLayout(displaySet) {
const apis = [];
viewports.forEach((viewport, index) => {
apis[index] = null;
const viewportProps = viewportPropsArray[index];
viewports[index] = Object.assign({}, viewports[index], {
// plugin: 'vtk',
vtk: {
mode: 'mpr', // TODO: not used
afterCreation: api => {
@@ -28,6 +40,7 @@ export default function setMPRLayout(displaySet) {
resolve(apis);
}
},
...viewportProps,
},
});
});
@@ -1,28 +0,0 @@
const subscriptions = [];
// This is pretty hacky right now, but it makes sure we don't keep adding subscriptions.
// TODO -> Nuke this and move it up a layer once we have more vigorous layout support.
const vtkViewportSubscriptionManager = {
subscriptions,
pushSubscription(viewportIndex, subscription) {
if (!Array.isArray(subscriptions[viewportIndex])) {
subscriptions[viewportIndex] = [];
}
subscriptions[viewportIndex].push(subscription);
},
unsubscribe(viewportIndex) {
if (!subscriptions[viewportIndex]) {
return;
}
while (subscriptions[viewportIndex].length) {
subscriptions[viewportIndex].pop().unsubscribe();
}
subscriptions[viewportIndex] = null;
},
};
export default vtkViewportSubscriptionManager;
+2 -1
View File
@@ -38,7 +38,8 @@
[context.deploy-preview]
base = ""
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"
[[headers]]
# Define which paths this specific [[headers]] block will cover.
+7 -1
View File
@@ -26,6 +26,7 @@
"test:unit": "jest --collectCoverage",
"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",
@@ -49,6 +50,7 @@
"@babel/plugin-transform-runtime": "^7.5.0",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@percy/cypress": "^2.2.0",
"babel-eslint": "9.x",
"babel-loader": "^8.0.6",
"babel-plugin-inline-react-svg": "1.1.0",
@@ -57,16 +59,20 @@
"copy-webpack-plugin": "^5.0.3",
"cross-env": "^5.2.0",
"css-loader": "^3.2.0",
"dotenv": "^8.1.0",
"eslint": "5.16.0",
"eslint-config-prettier": "^6.4.0",
"eslint-config-react-app": "^4.0.1",
"eslint-plugin-flowtype": "2.x",
"eslint-plugin-import": "2.x",
"eslint-plugin-jsx-a11y": "6.x",
"eslint-plugin-node": "^9.1.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "1.5.0",
"extract-css-chunks-webpack-plugin": "^4.5.4",
"favicons-webpack-plugin": "^1.0.2",
"html-webpack-plugin": "^3.2.0",
"husky": "^3.0.0",
"jest": "^24.8.0",
@@ -97,7 +103,7 @@
"webpack-dev-server": "^3.7.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.1",
"workbox-webpack-plugin": "^4.3.1",
"workbox-webpack-plugin": "^5.0.0-beta.1",
"worker-loader": "^2.0.0"
},
"husky": {
+121
View File
@@ -3,6 +3,127 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.1...@ohif/core@1.8.0) (2019-11-12)
### Features
* 🎸 Update hotkeys and user preferences modal ([#1135](https://github.com/OHIF/Viewers/issues/1135)) ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), closes [#923](https://github.com/OHIF/Viewers/issues/923)
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.0...@ohif/core@1.7.1) (2019-11-08)
### Bug Fixes
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
# [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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.3.1",
"version": "1.8.0",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
+9 -5
View File
@@ -1,11 +1,14 @@
export default function getModalities(modality, modalitiesInStudy) {
let modalities = {};
if (modality) {
modalities = modality;
if (!modality && !modalitiesInStudy) {
return {};
}
const modalities = modality || {
vr: 'CS',
Value: [],
};
if (modalitiesInStudy) {
// Find vr in modalities
if (modalities.vr && modalities.vr === modalitiesInStudy.vr) {
for (let i = 0; i < modalitiesInStudy.Value.length; i++) {
const value = modalitiesInStudy.Value[i];
@@ -14,8 +17,9 @@ export default function getModalities(modality, modalitiesInStudy) {
}
}
} else {
modalities = modalitiesInStudy;
return modalitiesInStudy;
}
}
return modalities;
}
+4 -2
View File
@@ -1,3 +1,4 @@
import cloneDeep from 'lodash.clonedeep';
import hotkeys from './hotkeys';
import log from './../log.js';
@@ -51,10 +52,11 @@ export class HotkeysManager {
* @param {Boolean} [isDefaultDefinitions]
*/
setHotkeys(hotkeyDefinitions, isDefaultDefinitions = false) {
hotkeyDefinitions.forEach(definition => this.registerHotkeys(definition));
const definitions = cloneDeep(hotkeyDefinitions);
definitions.forEach(definition => this.registerHotkeys(definition));
if (isDefaultDefinitions) {
this.hotkeyDefaults = hotkeyDefinitions;
this.hotkeyDefaults = definitions;
}
}
@@ -9,6 +9,7 @@ import { SeriesMetadata } from './SeriesMetadata';
import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from '../../utils/isImage';
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
export class StudyMetadata extends Metadata {
@@ -121,7 +122,7 @@ export class StudyMetadata extends Metadata {
if (displaySet) {
displaySet.sopClassModule = true;
displaySets.push(displaySet);
return;
return displaySets;
}
}
@@ -146,6 +147,7 @@ export class StudyMetadata extends Metadata {
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
isClip: true,
@@ -620,6 +622,16 @@ const makeDisplaySet = (series, instances) => {
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;
};
@@ -1,18 +1,15 @@
import cloneDeep from 'lodash.clonedeep';
const defaultState = {
// Top level key
viewer: {
// First tab
hotKeysData: {
// hotkeyName, label, keys, column
// zoom: { label: 'Zoom', command: 'Z', column: 0 },
},
// Second tab
windowLevelData: {
// order, description, window (int), level (int)
// 0: { description: 'Soft tissue', window: '', level: '' },
},
// First tab
hotkeyDefinitions: [
// commandName, label, keys
// [{ zoom: { label: 'Zoom', keys: ['z'] }}]
],
// Second tab
windowLevelData: {
// order, description, window (int), level (int)
// 0: { description: 'Soft tissue', window: '', level: '' },
},
};
+14 -5
View File
@@ -34,7 +34,6 @@ const defaultState = {
* @param {Object} [action.viewportSpecificData]
*/
const viewports = (state = defaultState, action) => {
let currentData;
let viewportSpecificData;
let useActiveViewport = false;
switch (action.type) {
@@ -85,14 +84,24 @@ const viewports = (state = defaultState, action) => {
// Allow fall-through
// eslint-disable-next-line
case SET_SPECIFIC_DATA: {
const layout = cloneDeep(state.layout);
const hasPlugin = action.data && action.data.plugin;
const viewportIndex = useActiveViewport
? state.activeViewportIndex
: action.viewportIndex;
currentData = cloneDeep(state.viewportSpecificData[viewportIndex]) || {};
viewportSpecificData = cloneDeep(state.viewportSpecificData);
viewportSpecificData[viewportIndex] = merge({}, currentData, action.data);
const { dom } = state.viewportSpecificData[viewportIndex];
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:
viewportSpecificData = cloneDeep(state.viewportSpecificData);
@@ -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,
* and waits for all of the results to be returned.
*
* @param studyInstanceUids The UIDs of the Studies to be retrieved
* @return Promise
* @param {Object} server Object with server configuration parameters
* @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(
server,
studyInstanceUids,
seriesInstanceUids
filters
) {
// Create an empty array to store the Promises for each metaData retrieval call
const promises = [];
@@ -21,11 +24,7 @@ export default function retrieveStudiesMetadata(
// Loop through the array of studyInstanceUids
studyInstanceUids.forEach(function(studyInstanceUid) {
// Send the call and resolve or reject the related promise based on its outcome
const promise = retrieveStudyMetadata(
server,
studyInstanceUid,
seriesInstanceUids
);
const promise = retrieveStudyMetadata(server, studyInstanceUid, filters);
// Add the current promise to the array of promises
promises.push(promise);
@@ -7,11 +7,13 @@ const StudyMetaDataPromises = new Map();
/**
* 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 {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 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
// and further requests for that metadata will always fail. On failure, we probably need to remove the
// corresponding promise from the "StudyMetaDataPromises" map...
@@ -32,7 +34,7 @@ export function retrieveStudyMetadata(server, studyInstanceUid) {
// Create a promise to handle the data retrieval
const promise = new Promise((resolve, reject) => {
RetrieveMetadata(server, studyInstanceUid).then(function(data) {
RetrieveMetadata(server, studyInstanceUid, filters).then(function(data) {
resolve(data);
}, reject);
});
@@ -30,7 +30,7 @@ function dateToString(date) {
function getQIDOQueryParams(filter, serverSupportsQIDOIncludeField) {
const commaSeparatedFields = [
'00081030', // Study Description
'00080060', //Modality
'00080060', // Modality
// Add more fields here if you want them in the result
].join(',');
@@ -1,609 +1,30 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import isLowPriorityModality from '../../../utils/isLowPriorityModality';
const INFO = Symbol('INFO');
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.
*/
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 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}`;
}
/**
* 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];
}
}
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']
),
};
}
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;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration paramenters
* @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']),
};
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration paramenters
* @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);
})
);
}
/**
* Parses result data from a WADO search into Study MetaData
* Returns an object populated with study metadata, including the
* series list.
*
* @param {Object} server Object with server configuration paramenters
* @param {Array} sopInstanceList List of SOP Instances that build up to the study
* @resolves {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
async function createStudyFromSOPInstanceList(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');
}
import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync';
import RetrieveMetadataLoaderAsync from './retrieveMetadataLoaderAsync';
/**
* 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
* @param {Object} server Object with server configuration paramenters
* will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed.
*
* @param {Object} server Object with server configuration parameters
* @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
* @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 {Object} A study descriptor object
*/
async function RetrieveMetadata(server, studyInstanceUid) {
return (server.enableStudyLazyLoad !== false
? lazyLoadStudyMetadata
: loadStudyMetadata)(server, studyInstanceUid);
}
async function RetrieveMetadata(server, studyInstanceUid, filters = {}) {
const RetrieveMetadataLoader =
server.enableStudyLazyLoad !== false
? RetrieveMetadataLoaderAsync
: RetrieveMetadataLoaderSync;
/**
*
* @param {*} server
* @param {*} studyInstanceUID
*/
async function loadStudyMetadata(server, studyInstanceUID) {
const dicomWeb = new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
return dicomWeb
.retrieveStudyMetadata({ studyInstanceUID })
.then(result => createStudyFromSOPInstanceList(server, result));
}
async function lazyLoadStudyMetadata(server, studyInstanceUid) {
const seriesInstanceUids = await searchStudySeries(server, studyInstanceUid);
const dicomWeb = new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
const seriesLoader = makeSeriesLoader(
dicomWeb,
studyInstanceUid,
seriesInstanceUids
);
const firstSeries = await seriesLoader.next();
const study = await createStudyFromSOPInstanceList(
const retrieveMetadataLoader = new RetrieveMetadataLoader(
server,
firstSeries.sopInstances
studyInstanceUid,
filters
);
if (seriesLoader.hasNext()) {
attachSeriesLoader(server, study, seriesLoader);
}
return study;
}
const studyMetadata = retrieveMetadataLoader.execLoad();
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 makeSeriesLoader(
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 };
},
});
}
/**
* Search series of a given study
* @param {Object} server Object with server configuration paramenters
* @param {string} studyInstanceUID The Study Instance UID to search series from;
* @returns {Arrays} A list of Series Instance UIDs
*/
async function searchStudySeries(server, studyInstanceUID) {
const dicomWeb = new api.DICOMwebClient({
url: server.qidoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
const seriesList = await dicomWeb.searchForSeries({ studyInstanceUID });
return seriesList
.sort(seriesSortingCriteria)
.map(series => getSeriesInfo(series).seriesInstanceUid);
}
/**
* Series sorting criteria: series considered low priority are moved to the end
* of the list and series number is used to break ties
* @param {Object} firstSeries
* @param {Object} secondSeries
*/
function seriesSortingCriteria(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;
}
/**
* Creates an object with processed series information and saves its reference
* inside the series object itself to simplify sorting
* @param {Object} series The raw series object
*/
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;
return studyMetadata;
}
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
* and instance numbers in ascending order.
* Series sorting criteria: series considered low priority are moved to the end
* 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 {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) {
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 => {
series.instances.sort((a, b) => a.instanceNumber - b.instanceNumber);
});
if (deepSort) {
study.seriesList.forEach(series => {
sortStudyInstances(series.instances, instancesSortingCriteria);
});
}
return study;
}
export { sortStudySeries, sortStudyInstances, sortingCriteria };
+22
View File
@@ -0,0 +1,22 @@
/* Enabled JPEG images downloading on IE11. */
const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => {
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, { type: contentType });
return blob;
};
export default b64toBlob;
+6
View File
@@ -9,7 +9,9 @@ import studyMetadataManager from './studyMetadataManager';
import updateMetaDataManager from './updateMetaDataManager.js';
import writeScript from './writeScript.js';
import DicomLoaderService from './dicomLoaderService.js';
import b64toBlob from './b64toBlob.js';
import * as urlUtil from './urlUtil';
import makeCancelable from './makeCancelable';
const utils = {
guid,
@@ -18,6 +20,7 @@ const utils = {
addServers,
sortBy,
writeScript,
b64toBlob,
StackManager,
studyMetadataManager,
// Updates WADO-RS metaDataManager
@@ -25,6 +28,7 @@ const utils = {
DICOMTagDescriptions,
DicomLoaderService,
urlUtil,
makeCancelable,
};
export {
@@ -34,6 +38,7 @@ export {
addServers,
sortBy,
writeScript,
b64toBlob,
StackManager,
studyMetadataManager,
// Updates WADO-RS metaDataManager
@@ -41,6 +46,7 @@ export {
DICOMTagDescriptions,
DicomLoaderService,
urlUtil,
makeCancelable,
};
export default utils;
+3 -1
View File
@@ -9,13 +9,15 @@ describe('Top level exports', () => {
'addServers',
'sortBy',
'writeScript',
'b64toBlob',
'StackManager',
'studyMetadataManager',
// Updates WADO-RS metaDataManager
'updateMetaDataManager',
'DICOMTagDescriptions',
'DicomLoaderService',
'urlUtil'
'urlUtil',
'makeCancelable',
].sort();
const exports = Object.keys(utils.default).sort();
@@ -0,0 +1,157 @@
/**
* Checks if a series is reconstructable to a 3D volume.
*
* @param {Object} series The `OHIFSeriesMetadata` object.
* @param {Object[]} instances The `OHIFInstanceMetadata` object
*/
export default function isDisplaySetReconstructable(series, instances) {
// Can't reconstruct if we only have one image.
const modality = series._data.modality; // TODO -> Is there a better way to get this?
const isMultiframe = instances[0].getRawValue('x00280008') > 1;
if (!constructableModalities.includes(modality)) {
return { value: false };
}
if (!isMultiframe && instances.length === 1) {
return { values: false };
}
if (isMultiframe) {
return processMultiframe(instances[0]);
} else {
return processSingleframe(instances);
}
}
function processMultiframe(instance) {
//TODO: deal with multriframe checks! return true for now.
return { value: true };
}
function processSingleframe(instances) {
const firstImage = instances[0];
const firstImageRows = firstImage.getTagValue('x00280010');
const firstImageColumns = firstImage.getTagValue('x00280011');
const firstImageSamplesPerPixel = firstImage.getTagValue('x00280002');
// Note: No need to unpack iop, can compare string form.
const firstImageOrientationPatient = firstImage.getTagValue('x00200037');
// Can't reconstruct if we:
// -- Have a different dimensions within a displaySet.
// -- Have a different number of components within a displaySet.
// -- Have different orientations within a displaySet.
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const rows = instance.getTagValue('x00280010');
const columns = instance.getTagValue('x00280011');
const samplesPerPixel = instance.getTagValue('x00280002');
const imageOrientationPatient = instance.getTagValue('x00200037');
if (
rows !== firstImageRows ||
columns !== firstImageColumns ||
samplesPerPixel !== firstImageSamplesPerPixel ||
imageOrientationPatient !== firstImageOrientationPatient
) {
return { value: false };
}
}
let missingFrames = 0;
// Check if frame spacing is approximately equal within a tolerance.
// If spacing is on a uniform grid but we are missing frames,
// Allow reconstruction, but pass back the number of missing frames.
if (instances.length > 2) {
const firstIpp = _getImagePositionPatient(firstImage);
const lastIpp = _getImagePositionPatient(instances[instances.length - 1]);
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstIpp, lastIpp) / (instances.length - 1);
let previousIpp = firstIpp;
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const ipp = _getImagePositionPatient(instance);
const spacingBetweenFrames = _getPerpendicularDistance(ipp, previousIpp);
const spacingIssue = _getSpacingIssue(
spacingBetweenFrames,
averageSpacingBetweenFrames
);
if (spacingIssue) {
const issue = spacingIssue.issue;
if (issue === reconstructionIssues.MISSING_FRAMES) {
missingFrames += spacingIssue.missingFrames;
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
return { value: false };
}
}
previousIpp = ipp;
}
}
return { value: true, missingFrames };
}
// TODO: Is 10% a reasonable tolerance for spacing?
const tolerance = 0.1;
/**
* Checks for spacing issues.
*
* @param {number} spacing The spacing between two frames.
* @param {number} averageSpacing The average spacing between all frames.
*
* @returns {Object} An object containing the issue and extra information if necessary.
*/
function _getSpacingIssue(spacing, averageSpacing) {
const equalWithinTolerance =
Math.abs(spacing - averageSpacing) < averageSpacing * tolerance;
if (equalWithinTolerance) {
return;
}
const multipleOfAverageSpacing = spacing / averageSpacing;
const numberOfSpacings = Math.round(multipleOfAverageSpacing);
const errorForEachSpacing =
Math.abs(spacing - numberOfSpacings * averageSpacing) / numberOfSpacings;
if (errorForEachSpacing < tolerance * averageSpacing) {
return {
issue: reconstructionIssues.MISSING_FRAMES,
missingFrames: numberOfSpacings - 1,
};
}
return { issue: reconstructionIssues.IRREGULAR_SPACING };
}
function _getImagePositionPatient(instance) {
return instance
.getTagValue('x00200032')
.split('\\')
.map(element => Number(element));
}
function _getPerpendicularDistance(a, b) {
return Math.sqrt(
Math.pow(a[0] - b[0], 2) +
Math.pow(a[1] - b[1], 2) +
Math.pow(a[2] - b[2], 2)
);
}
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
const reconstructionIssues = {
MISSING_FRAMES: 'missingframes',
IRREGULAR_SPACING: 'irregularspacing',
};
+19
View File
@@ -0,0 +1,19 @@
export default function makeCancelable(thenable) {
let isCanceled = false;
const promise = Promise.resolve(thenable).then(
function(result) {
if (isCanceled) throw Object.freeze({ isCanceled });
return result;
},
function(error) {
if (isCanceled) throw Object.freeze({ isCanceled, error });
throw error;
}
);
return Object.assign(Object.create(promise), {
then: promise.then.bind(promise),
cancel() {
isCanceled = true;
},
});
}
@@ -77,14 +77,6 @@ const getWadoRsInstanceMetaData = (study, series, instance) => {
*/
function updateMetaDataManagerForSeries(study, series) {
series.instances.forEach(instance => {
// Cache just images that are going to be loaded via WADO-RS
if (
instance.imageRendering !== 'wadors' &&
instance.thumbnailRendering !== 'wadors'
) {
return;
}
const metaData = getWadoRsInstanceMetaData(study, series, instance);
const numberOfFrames = instance.numberOfFrames || 1;
@@ -94,7 +86,7 @@ function updateMetaDataManagerForSeries(study, series) {
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
const imageId = getWADORSImageId(instance, frameNumber);
// TODO Drop dependency on this
// TODO Make a metadata manager which uses study/series/instance UIDS instead of imageIds
cornerstoneWADOImageLoader.wadors.metaDataManager.add(imageId, metaData);
}
});
+10 -4
View File
@@ -6,7 +6,7 @@ const PARAM_PATTERN_IDENTIFIER = ':';
function toLowerCaseFirstLetter(word) {
return word[0].toLowerCase() + word.slice(1);
}
const getFilters = (location = {}) => {
const getQueryFilters = (location = {}) => {
const { search } = location;
if (!search) {
@@ -55,13 +55,19 @@ const replaceParam = (path = '', paramKey, paramValue) => {
return path;
};
const isValidPath = path => {
const paramPatternPiece = `/${PARAM_PATTERN_IDENTIFIER}`;
return path.indexOf(paramPatternPiece) < 0;
};
const queryString = {
getQueryFilters: getFilters,
getQueryFilters,
};
const paramString = {
parseParam: parseParam,
replaceParam: replaceParam,
isValidPath,
parseParam,
replaceParam,
};
export { parse, queryString, paramString };
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.51.0...@ohif/i18n@0.52.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.51.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.5...@ohif/i18n@0.51.0) (2019-10-15)
### Features
* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b))
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.4...@ohif/i18n@0.50.5) (2019-10-04)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.50.5",
"version": "0.52.0",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
@@ -0,0 +1,14 @@
{
"Browser": "Browser",
"Build Number": "Build Number",
"Latest Master Commits": "Latest Master Commits",
"More details": "More details",
"Name": "Name",
"OHIF Viewer - About": "OHIF Viewer - About",
"OS": "OS",
"Report an issue": "Report an issue",
"Repository URL": "Repository URL",
"Value": "Value",
"Version Information": "Version Information",
"Visit the forum": "Visit the forum"
}
+12 -10
View File
@@ -1,13 +1,15 @@
import Buttons from "./Buttons.json";
import CineDialog from "./CineDialog.json";
import Common from "./Common.json";
import Header from "./Header.json";
import MeasurementTable from "./MeasurementTable.json";
import StudyList from "./StudyList.json";
import UserPreferencesModal from "./UserPreferencesModal.json";
import AboutModal from './AboutModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
import Header from './Header.json';
import MeasurementTable from './MeasurementTable.json';
import StudyList from './StudyList.json';
import UserPreferencesModal from './UserPreferencesModal.json';
export default {
export default {
'en-US': {
AboutModal,
Buttons,
CineDialog,
Common,
@@ -15,5 +17,5 @@ export default {
MeasurementTable,
StudyList,
UserPreferencesModal,
}
};
},
};
@@ -0,0 +1,14 @@
{
"Browser": "Navegador",
"Build Number": "Número da compilação",
"Latest Master Commits": "Últimos Commits na Master",
"More details": "Mais detalhes",
"Name": "Nome",
"OHIF Viewer - About": "OHIF Viewer - Sobre",
"OS": "SO",
"Report an issue": "Informar um problema",
"Repository URL": "URL do Repositório",
"Value": "Valor",
"Version Information": "Informação da Versão",
"Visit the forum": "Visite o fórum"
}
+10 -8
View File
@@ -1,15 +1,17 @@
import Buttons from "./Buttons.json";
import CineDialog from "./CineDialog.json";
import Common from "./Common.json";
import Header from "./Header.json";
import UserPreferencesModal from "./UserPreferencesModal.json";
import AboutModal from './AboutModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
import Header from './Header.json';
import UserPreferencesModal from './UserPreferencesModal.json';
export default {
export default {
'pt-BR': {
AboutModal,
Buttons,
CineDialog,
Common,
Header,
UserPreferencesModal,
}
};
},
};
+184
View File
@@ -3,6 +3,190 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.61.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.60.1...@ohif/ui@0.61.0) (2019-11-12)
### Features
* 🎸 Update hotkeys and user preferences modal ([#1135](https://github.com/OHIF/Viewers/issues/1135)) ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), closes [#923](https://github.com/OHIF/Viewers/issues/923)
## [0.60.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.60.0...@ohif/ui@0.60.1) (2019-11-08)
### Bug Fixes
* Fix display issues with incorrect thumbnails. Change ImageThumb to functional component. ([#1148](https://github.com/OHIF/Viewers/issues/1148)) ([d70eae3](https://github.com/OHIF/Viewers/commit/d70eae3eb04fe854464f3e62316df8869bba6f11))
# [0.60.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.59.1...@ohif/ui@0.60.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.59.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.59.0...@ohif/ui@0.59.1) (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))
# [0.59.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.5...@ohif/ui@0.59.0) (2019-11-04)
### Features
* 🎸 New modal provider ([#1110](https://github.com/OHIF/Viewers/issues/1110)) ([5ee832b](https://github.com/OHIF/Viewers/commit/5ee832b19505a4e8e5756660ce6ed03a7f18dec3)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116)
## [0.58.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.4...@ohif/ui@0.58.5) (2019-11-04)
### Bug Fixes
* 🐛 Minor issues measurement panel related to description ([#1142](https://github.com/OHIF/Viewers/issues/1142)) ([681384b](https://github.com/OHIF/Viewers/commit/681384b7425c83b02a0ed83371ca92d78ca7838c))
## [0.58.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.3...@ohif/ui@0.58.4) (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))
## [0.58.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.2...@ohif/ui@0.58.3) (2019-10-30)
### Bug Fixes
* 🐛 Fix ghost shadow on thumb ([#1113](https://github.com/OHIF/Viewers/issues/1113)) ([caaa032](https://github.com/OHIF/Viewers/commit/caaa032c4bc24fd69fdb01a15a8feb2721c321db))
## [0.58.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.1...@ohif/ui@0.58.2) (2019-10-29)
### Bug Fixes
* 🐛 Limit image download size to avoid browser issues ([#1112](https://github.com/OHIF/Viewers/issues/1112)) ([5716b71](https://github.com/OHIF/Viewers/commit/5716b71d409ee1c6f13393c8cb7f50222415e198)), closes [#1099](https://github.com/OHIF/Viewers/issues/1099)
## [0.58.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.0...@ohif/ui@0.58.1) (2019-10-29)
### Bug Fixes
* **StudyList:** camel case colSpan ([#1123](https://github.com/OHIF/Viewers/issues/1123)) ([0d498ba](https://github.com/OHIF/Viewers/commit/0d498ba17ddde8d8f0c51d742770e1574041eec0))
# [0.58.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.57.1...@ohif/ui@0.58.0) (2019-10-28)
### Features
* responsive study list ([#1068](https://github.com/OHIF/Viewers/issues/1068)) ([2cdef4b](https://github.com/OHIF/Viewers/commit/2cdef4b9844cc2ce61e9ce76b5a942ba7051fe16))
## [0.57.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.57.0...@ohif/ui@0.57.1) (2019-10-28)
### Bug Fixes
* MIP styling ([#1109](https://github.com/OHIF/Viewers/issues/1109)) ([0d21cc6](https://github.com/OHIF/Viewers/commit/0d21cc6ad0c47706b9e62e05fe2a0f1d86339760))
# [0.57.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.56.1...@ohif/ui@0.57.0) (2019-10-26)
### Features
* Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
## [0.56.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.56.0...@ohif/ui@0.56.1) (2019-10-26)
**Note:** Version bump only for package @ohif/ui
# [0.56.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.55.0...@ohif/ui@0.56.0) (2019-10-22)
### Features
* 🎸 Load spinner when selecting gcloud store. Add key on td ([#1034](https://github.com/OHIF/Viewers/issues/1034)) ([e62f403](https://github.com/OHIF/Viewers/commit/e62f403fe9e3df56713128e3d59045824b086d8d)), closes [#1057](https://github.com/OHIF/Viewers/issues/1057)
# [0.55.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.54.0...@ohif/ui@0.55.0) (2019-10-15)
### Features
* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b))
# [0.54.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.4...@ohif/ui@0.54.0) (2019-10-14)
### Features
* Notification Service ([#1011](https://github.com/OHIF/Viewers/issues/1011)) ([92c8996](https://github.com/OHIF/Viewers/commit/92c8996))
## [0.53.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.3...@ohif/ui@0.53.4) (2019-10-11)
**Note:** Version bump only for package @ohif/ui
+6 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.53.4",
"version": "0.61.0",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -39,6 +39,7 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/i18n": "^0.2.3",
"browser-detect": "^0.2.28",
"classnames": "2.2.6",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
@@ -47,10 +48,10 @@
"moment": "2.24.0",
"prop-types": "15.6.2",
"react-bootstrap-modal": "4.2.0",
"react-dates": "18.4.1",
"react-dnd": "9.3.4",
"react-dnd-html5-backend": "^9.3.4",
"react-dnd-touch-backend": "^9.3.4",
"react-dates": "21.2.1",
"react-dnd": "9.4.0",
"react-dnd-html5-backend": "^9.4.0",
"react-dnd-touch-backend": "^9.4.0",
"react-i18next": "^10.11.0",
"react-with-direction": "1.3.0"
},
@@ -0,0 +1,107 @@
import React from 'react';
import detect from 'browser-detect';
import { useTranslation } from 'react-i18next';
import './AboutContent.styl';
const AboutContent = () => {
const { t } = useTranslation('AboutContent');
const { os, version, name } = detect();
const capitalize = s =>
s.substr(0, 1).toUpperCase() + s.substr(1).toLowerCase();
const itemsPreset = () => {
return [
{
name: t('Repository URL'),
value: 'https://github.com/OHIF/Viewers/',
link: 'https://github.com/OHIF/Viewers/',
},
{
name: t('Latest Master Commits'),
value: 'https://github.com/OHIF/Viewers/commits/master',
link: 'https://github.com/OHIF/Viewers/commits/master',
},
{
name: 'Version Number',
value: process.env.VERSION_NUMBER,
},
{
name: t('Build Number'),
value: process.env.BUILD_NUM,
},
{
name: t('Browser'),
value: `${capitalize(name)} ${version}`,
},
{
name: t('OS'),
value: os,
},
];
};
const renderTableRow = ({ name, value, link }) => (
<tr key={name} style={{ backgroundColor: 'transparent' }}>
<td>{name}</td>
<td>
{link ? (
<a target="_blank" rel="noopener noreferrer" href={link}>
{value}
</a>
) : (
value
)}
</td>
</tr>
);
return (
<div data-cy="about-modal">
<div className="btn-group">
<a
className="btn btn-default"
target="_blank"
rel="noopener noreferrer"
href="https://groups.google.com/forum/#!forum/cornerstone-platform"
>
{t('Visit the forum')}
</a>
{` `}
<a
className="btn btn-default"
target="_blank"
rel="noopener noreferrer"
href="https://github.com/OHIF/Viewers/issues/new/choose"
>
{t('Report an issue')}
</a>
{` `}
<a
className="btn btn-default"
target="_blank"
rel="noopener noreferrer"
href="http://ohif.org"
>
{t('More details')}
</a>
</div>
<div>
<h3>{t('Version Information')}</h3>
<table className="table table-responsive">
<thead>
<tr>
<th>{t('Name')}</th>
<th>{t('Value')}</th>
</tr>
</thead>
<tbody>{itemsPreset().map(item => renderTableRow(item))}</tbody>
</table>
</div>
</div>
);
};
export { AboutContent };
export default AboutContent;
@@ -1,7 +1,7 @@
@import './../../design/styles/common/button.styl'
@import './../../design/styles/common/table.styl'
@import '../../../design/styles/common/button.styl'
@import '../../../design/styles/common/table.styl'
.AboutModal
.AboutContent
.btn
border-color: #ccc;
@@ -0,0 +1,325 @@
import React, { useEffect, useState, createRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import './ViewportDownloadForm.styl';
import { TextInput, Select } from '@ohif/ui';
const FILE_TYPE_OPTIONS = [
{
key: 'jpg',
value: 'jpg',
},
{
key: 'png',
value: 'png',
},
];
const DEFAULT_FILENAME = 'image';
const ViewportDownloadForm = ({
activeViewport,
onClose,
updateViewportPreview,
enableViewport,
disableViewport,
toggleAnnotations,
loadImage,
downloadBlob,
defaultSize,
minimumSize,
maximumSize,
canvasClass,
}) => {
const [t] = useTranslation('ViewportDownloadForm');
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
const [height, setHeight] = useState(defaultSize);
const [width, setWidth] = useState(defaultSize);
const [showAnnotations, setShowAnnotations] = useState(true);
const [keepAspect, setKeepAspect] = useState(true);
const [lastImage, setLastImage] = useState();
const [viewportElement, setViewportElement] = useState();
const [viewportElementHeight, setViewportElementHeight] = useState(
minimumSize
);
const [viewportElementWidth, setViewportElementWidth] = useState(minimumSize);
const [downloadCanvas, setDownloadCanvas] = useState({
ref: createRef(),
width: minimumSize,
height: minimumSize,
});
const [viewportPreview, setViewportPreview] = useState({
src: null,
width: minimumSize,
height: minimumSize,
});
useEffect(() => {
enableViewport(viewportElement);
return () => {
disableViewport(viewportElement);
setHeight(defaultSize);
setWidth(defaultSize);
};
}, [defaultSize, disableViewport, enableViewport, viewportElement]);
useEffect(() => {
const validSize = value => (value >= minimumSize ? value : minimumSize);
const loadAndUpdateViewports = async () => {
const {
image,
width: scaledWidth,
height: scaledHeight,
} = await loadImage(activeViewport, viewportElement, width, height);
setLastImage(image);
toggleAnnotations(showAnnotations, viewportElement);
setViewportElementHeight(validSize(scaledHeight));
setViewportElementWidth(validSize(scaledWidth));
setDownloadCanvas(state => ({
...state,
height: validSize(scaledHeight),
width: validSize(scaledWidth),
}));
const {
dataUrl,
width: viewportElementWidth,
height: viewportElementHeight,
} = await updateViewportPreview(
viewportElement,
downloadCanvas.ref.current,
fileType
);
setViewportPreview(state => ({
...state,
src: dataUrl,
width: validSize(viewportElementWidth),
height: validSize(viewportElementHeight),
}));
};
loadAndUpdateViewports();
}, [
activeViewport,
viewportElement,
showAnnotations,
height,
width,
loadImage,
toggleAnnotations,
updateViewportPreview,
fileType,
downloadCanvas.ref,
minimumSize,
maximumSize,
]);
const onHeightChange = event => {
const newHeight = Math.min(event.target.value, maximumSize);
setHeight(newHeight);
setViewportElementHeight(newHeight);
setDownloadCanvas(state => ({
...state,
height: newHeight,
}));
if (keepAspect) {
const multiplier = newHeight / lastImage.height;
const newWidth = Math.round(lastImage.width * multiplier);
setWidth(newWidth);
setViewportElementWidth(newWidth);
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
}
};
const onWidthChange = event => {
const newWidth = Math.min(event.target.value, maximumSize);
setWidth(newWidth);
setViewportElementWidth(newWidth);
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
if (keepAspect) {
const multiplier = newWidth / lastImage.width;
const newHeight = Math.round(lastImage.height * multiplier);
setHeight(newHeight);
setViewportElementHeight(newHeight);
setDownloadCanvas(state => ({
...state,
height: newHeight,
}));
}
};
const downloadImage = () => {
downloadBlob(
filename || DEFAULT_FILENAME,
fileType,
viewportElement,
downloadCanvas.ref.current
);
};
return (
<>
<div className="title">
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
)}
</div>
<div className="file-info-container">
<div className="col">
<div className="width">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={width}
label={t('Image width (px)')}
onChange={onWidthChange}
/>
</div>
<div className="height">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={height}
label={t('Image height (px)')}
onChange={onHeightChange}
/>
</div>
</div>
<div className="col">
<div className="file-name">
<TextInput
type="text"
value={filename}
onChange={event => setFilename(event.target.value)}
label={t('File name')}
id="file-name"
/>
</div>
<div className="file-type">
<Select
value={fileType}
onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS}
label={t('File type')}
/>
</div>
</div>
<div className="col">
<div className="show-annotations">
<label htmlFor="show-annotations" className="form-check-label">
<input
id="show-annotations"
type="checkbox"
className="form-check-input"
checked={showAnnotations}
onChange={event => setShowAnnotations(event.target.checked)}
/>
{t('Show Annotations')}
</label>
</div>
</div>
</div>
<div
style={{
height: viewportElementHeight,
width: viewportElementWidth,
position: 'absolute',
left: '9999px',
}}
ref={ref => setViewportElement(ref)}
>
<canvas
className={canvasClass}
style={{
height: downloadCanvas.height,
width: downloadCanvas.width,
display: 'block',
}}
width={downloadCanvas.width}
height={downloadCanvas.height}
ref={downloadCanvas.ref}
></canvas>
</div>
<div className="preview">
<h4> {t('Image Preview')}</h4>
<img
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
style={{
height: viewportPreview.height,
width: viewportPreview.width,
}}
/>
</div>
<div className="actions">
<div className="action-cancel">
<button type="button" className="btn btn-danger" onClick={onClose}>
{t('Cancel')}
</button>
</div>
<div className="action-save">
<button onClick={downloadImage} className="btn btn-primary">
{t('Download')}
</button>
</div>
</div>
</>
);
};
ViewportDownloadForm.propTypes = {
onClose: PropTypes.func.isRequired,
activeViewport: PropTypes.object,
updateViewportPreview: PropTypes.func.isRequired,
enableViewport: PropTypes.func.isRequired,
disableViewport: PropTypes.func.isRequired,
toggleAnnotations: PropTypes.func.isRequired,
loadImage: PropTypes.func.isRequired,
downloadBlob: PropTypes.func.isRequired,
defaultSize: PropTypes.number.isRequired,
minimumSize: PropTypes.number.isRequired,
maximumSize: PropTypes.number.isRequired,
canvasClass: PropTypes.string.isRequired,
};
export default ViewportDownloadForm;
@@ -0,0 +1,107 @@
@import '../../../design/styles/common/global.styl'
@import '../../../design/styles/common/form.styl'
@import '../../../design/styles/common/button.styl'
.ViewportDownloadForm
color: var(--text-secondary-color);
filter: drop-shadow(0 0 3px var(--ui-gray-darkest));
border: none;
border-radius: 8px;
width: inherit;
padding: 15px;
background: transparent;
z-index: 1080 !important;
.title
margin: 0;
font-weight: bold;
.file-info-container
display: flex;
flex-direction: row;
margin: 20px 0;
border-radius: 5px;
padding: 20px 10px 0;
background-color: #16202b;
.form-control.input-ohif
padding: 6px 12px;
@media screen and (max-width: 1023px)
flex-direction: column;
.col
flex-grow: 1;
.input-ohif
margin-left: 15px;
@media screen and (max-width: 1023px)
margin-left: 0;
margin-top: 5px;
width: 100%;
.file-type
.select-ohif
margin-left: 17px;
@media screen and (max-width: 1023px)
margin-left: 0;
width: 100%;
.show-annotations
font-weight: bold;
line-height: 30px;
input
margin-right: 7px;
vertical-align: middle;
label
display: flex;
justify-content: center;
align-items: center;
.preview
display: flex;
flex-direction column;
height: fit-content;
background-color: #16202b;
width: fit-content;
padding: 10px;
border-radius: 5px;
align-self: center;
@media screen and (max-width: 1023px)
width: 100%;
justify-content: center;
align-items: center;
.viewport-preview
max-height: 512px;
max-width: 512px;
h4
width: 100%;
text-align center;
font-size: 1.3em;
margin: 0 0 10px;
.preview-container
width: auto;
height: 100%;
max-height: 400px;
object-fit contain;
.actions
display: flex;
height: 60px;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
.action-cancel
margin: 0 20px;
.actions-save
margin: 0 0 0 10px;
.modal-dialog
height: 100%;
.modal-body
display: flex;
flex-direction: column;
@@ -0,0 +1,39 @@
---
name: Viewport Download Form
menu: Components
route: /components/viewport-download-form
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { ViewportDownloadForm } from './../index'
import NameSpace from '../../../__docs__/NameSpace'
# Viewport Download Form
## Basic usage
<Playground>
<State
initial={{}}
>
{({ state, setState }) => (
<React.Fragment>
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<ViewportDownloadForm />
</div>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={ViewportDownloadForm} />
## Translation Namespace
<NameSpace name="ViewportDownloadForm" />
@@ -0,0 +1,2 @@
import ViewportDownloadForm from './ViewportDownloadForm';
export { ViewportDownloadForm };
+20 -8
View File
@@ -1,27 +1,35 @@
import { StudyBrowser, ThumbnailEntry } from './studyBrowser';
import { StudyBrowser, Thumbnail } from './studyBrowser';
import { LayoutButton, LayoutChooser } from './layoutButton';
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
import { Overlay, OverlayTrigger } from './overlayTrigger';
import { TableList, TableListItem } from './tableList';
import {
AboutModal,
AboutContent,
UserPreferences,
UserPreferencesModal,
} from './userPreferencesModal';
UserPreferencesForm,
} from './userPreferencesForm';
import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog';
import { ViewportDownloadForm } from './content/viewportDownloadForm';
import { QuickSwitch } from './quickSwitch';
import { RoundedButtonGroup } from './roundedButtonGroup';
import { SelectTree } from './selectTree';
import { SimpleDialog } from './simpleDialog';
import { StudyList } from './studyList';
import { OHIFModal } from './ohifModal';
import {
PageToolbar,
StudyList,
TableSearchFilter,
TablePagination,
} from './studyList';
import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip';
export {
Checkbox,
CineDialog,
ViewportDownloadForm,
LayoutButton,
LayoutChooser,
MeasurementTable,
@@ -30,16 +38,20 @@ export {
OverlayTrigger,
QuickSwitch,
RoundedButtonGroup,
PageToolbar,
SelectTree,
SimpleDialog,
StudyBrowser,
StudyList,
TableList,
TableListItem,
ThumbnailEntry,
Thumbnail,
TableSearchFilter,
TablePagination,
ToolbarSection,
Tooltip,
AboutModal,
AboutContent,
UserPreferences,
UserPreferencesModal,
UserPreferencesForm,
OHIFModal,
};
@@ -30,6 +30,11 @@
.measurementLocation
margin-left: 9px;
margin-top: 9px;
overflow: hidden;
max-width: calc(100% - 50px);
text-overflow: ellipsis;
overflow-wrap: normal;
white-space: nowrap;
.measurementDisplayText
display: inline-block;
@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactBootstrapModal from 'react-bootstrap-modal';
import classNames from 'classnames';
const OHIFModal = ({
className,
closeButton,
backdrop,
keyboard,
show,
title,
onHide,
footer: Footer,
header: Header,
children: Component,
}) => (
<ReactBootstrapModal
className={classNames('modal fade themed in', className)}
backdrop={backdrop}
keyboard={keyboard}
show={show}
large={true}
title={title}
onHide={onHide}
>
{(Header || title) && (
<ReactBootstrapModal.Header closeButton={closeButton}>
{title && (
<ReactBootstrapModal.Title>{title}</ReactBootstrapModal.Title>
)}
{Header && <Header hide={onHide} />}
</ReactBootstrapModal.Header>
)}
<ReactBootstrapModal.Body>
{Component && <Component hide={onHide} />}
</ReactBootstrapModal.Body>
{Footer && (
<ReactBootstrapModal.Footer>
{' '}
<Footer hide={onHide} />
</ReactBootstrapModal.Footer>
)}
</ReactBootstrapModal>
);
OHIFModal.propTypes = {
className: PropTypes.string,
closeButton: PropTypes.bool,
backdrop: PropTypes.bool,
keyboard: PropTypes.bool,
show: PropTypes.bool,
title: PropTypes.string,
onHide: PropTypes.func,
footer: PropTypes.node,
header: PropTypes.node,
children: PropTypes.node,
};
export default OHIFModal;
@@ -0,0 +1,39 @@
---
name: OHIFModal
menu: Components
route: /components/ohif-modal
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { OHIFModal } from './../index'
import NameSpace from '../../../__docs__/NameSpace'
# OHIF Modal
## Basic usage
<Playground>
<State
initial={{}}
>
{({ state, setState }) => (
<React.Fragment>
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<OHIFModal />
</div>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={OHIFModal} />
## Translation Namespace
<NameSpace name="OHIFModal" />
@@ -0,0 +1,2 @@
import OHIFModal from './OHIFModal';
export { OHIFModal };
@@ -45,7 +45,7 @@
.thumbnails-wrapper
transform-origin: 0% 0%
.ThumbnailEntry
.thumbnail
float: left
.series-item
@@ -98,7 +98,7 @@
transition: transform 0.3s ease
transform-origin: 100% 0%
.ThumbnailEntry
.thumbnail
float: right
.series-details
@@ -179,7 +179,7 @@
.thumbnails-wrapper
transform: scale(1) translateY(0)
.ThumbnailEntry .series-details
.thumbnail .series-details
opacity: 1
transform: translateY(0)
transition-delay: 0.3s
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ThumbnailEntry } from './../studyBrowser';
import { Thumbnail } from './../studyBrowser';
import './SeriesList.styl';
export class SeriesList extends Component {
@@ -24,7 +24,7 @@ export class SeriesList extends Component {
getSeriesItems = () => {
return this.props.seriesItems.map((seriesData, index) => {
return (
<ThumbnailEntry
<Thumbnail
key={seriesData.displaySetInstanceUid}
id={`series_thumb_${index}`}
{...seriesData}
@@ -0,0 +1,196 @@
.sb-container {
position: fixed;
width: var(--snackbar-size);
padding: 20px;
z-index: var(--snackbar-zIndex);
box-sizing: border-box;
height: auto;
}
.sb-topLeft {
top: 0;
bottom: auto;
left: 0;
right: auto;
}
.sb-topCenter {
left: 50%;
top: 0;
bottom: auto;
transform: translateX(-50%);
}
.sb-topRight {
top: 0;
bottom: auto;
left: auto;
right: 0;
}
.sb-bottomLeft {
top: auto;
bottom: 0px;
left: 0px;
right: auto;
}
.sb-bottomCenter {
left: 50%;
bottom: 0;
top: auto;
transform: translateX(-50%);
}
.sb-bottomRight {
top: auto;
bottom: 0px;
left: auto;
right: 0px;
margin: 10px 0 0;
}
.sb-topLeft .sb-item,
.sb-topCenter .sb-item,
.sb-topRight .sb-item {
margin: 10px 0 0;
}
.sb-bottomLeft .sb-item,
.sb-bottomCenter .sb-item,
.sb-bottomRight .sb-item {
margin: 0 0 10px;
}
.sb-closeBtn {
height: 20px;
opacity: 1;
overflow: hidden;
padding: 2px;
text-align: center;
text-shadow: none;
width: 20px;
cursor: pointer;
position: absolute;
right: 5px;
top: 5px;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.6);
border-radius: 100%;
}
.sb-closeBtn:hover {
background: #fff;
}
.sb-closeIcon {
display: block;
font-size: 0;
height: 100%;
line-height: 0;
overflow: hidden;
position: relative;
width: 100%;
}
.sb-closeIcon:after,
.sb-closeIcon:before {
content: ' ';
display: block;
height: 2px;
transition: all 0.3s ease;
width: 12px;
background-color: #222;
opacity: 1;
position: absolute;
}
.sb-closeIcon:before {
left: 4px;
top: 3px;
transform: rotate(45deg);
transform-origin: 0px 50%;
}
.sb-closeIcon:after {
right: 3px;
top: 5px;
transform: rotate(-45deg);
transform-origin: calc(100% - 3px) 50%;
}
.sb-title {
font-size: 16px;
font-weight: bold;
}
.sb-message {
font-size: 14px;
word-break: break-all;
}
.sb-item {
position: relative;
transition: height 300ms ease;
animation: fadein 1s;
padding: 20px;
color: white;
overflow: hidden;
border-radius: 4px;
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12),
0 3px 5px -1px rgba(0, 0, 0, 0.14);
}
.sb-item a {
color: white;
text-decoration: underline;
}
.sb-container .sb-hidden {
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
height: 0;
opacity: 0;
transition: all 300ms ease;
}
.sb-success {
background-color: var(--snackbar-success);
}
.sb-error {
background-color: var(--snackbar-error);
}
.sb-warning {
background-color: var(--snackbar-warning);
}
.sb-info {
background-color: var(--snackbar-info);
}
@keyframes fadein {
from {
opacity: 0;
top: 30px;
}
to {
opacity: 1;
top: 0;
}
}
/* Internet Explorer */
@-ms-keyframes fadein {
from {
opacity: 0;
top: 30px;
}
to {
opacity: 1;
top: 0;
}
}
@@ -0,0 +1,53 @@
import React from 'react';
import SnackbarItem from './SnackbarItem';
import './Snackbar.css';
import { useSnackbarContext } from '../../utils/SnackbarProvider';
const SnackbarContainer = () => {
const { snackbarItems, hide } = useSnackbarContext();
const renderItem = item => {
return <SnackbarItem key={item.itemId} options={item} onClose={hide} />;
};
if (!snackbarItems) {
return null;
}
const renderItems = () => {
const items = {
topLeft: [],
topCenter: [],
topRight: [],
bottomLeft: [],
bottomCenter: [],
bottomRight: [],
};
snackbarItems.map(item => {
items[item.position].push(item);
});
return (
<div>
{Object.keys(items).map(pos => {
if (!items[pos].length) {
return null;
}
return (
<div key={pos} className={`sb-container sb-${pos}`}>
{items[pos].map(item => (
<div key={item.id}>{renderItem(item)}</div>
))}
</div>
);
})}
</div>
);
};
return <>{renderItems()}</>;
};
export default SnackbarContainer;
@@ -0,0 +1,31 @@
import React, { useState, useEffect } from 'react';
const SnackbarItem = ({ options, onClose }) => {
const handleClose = () => {
onClose(options.id);
};
useEffect(() => {
if (options.autoClose) {
setTimeout(() => {
handleClose();
}, options.duration);
}
}, []);
return (
<div
className={`${options.visible ? '' : 'sb-hidden'} sb-${
options.type
} sb-item`}
>
<span className="sb-closeBtn" onClick={handleClose}>
<span className="sb-closeIcon">x</span>
</span>
{options.title && <div className="sb-title">{options.title}</div>}
{options.message && <div className="sb-message">{options.message}</div>}
</div>
);
};
export default SnackbarItem;
@@ -0,0 +1,6 @@
export default {
INFO: 'info',
WARNING: 'warning',
SUCCESS: 'success',
ERROR: 'error',
};
@@ -1,85 +0,0 @@
import React, { PureComponent, memo } from 'react';
import { DragLayer } from 'react-dnd';
import PropTypes from 'prop-types';
import { ThumbnailEntry } from './ThumbnailEntry';
import './DragPreview.styl';
let subscribedToOffsetChange = false;
let dragPreviewRef = null;
const collector = monitor => {
if (!subscribedToOffsetChange) {
monitor.subscribeToOffsetChange(onOffsetChange(monitor));
subscribedToOffsetChange = true;
}
if (dragPreviewRef) {
const offset =
monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset();
if (offset) {
const transform = `translate(${offset.x}px, ${offset.y}px)`;
dragPreviewRef.style['transform'] = transform;
dragPreviewRef.style['-webkit-transform'] = transform;
}
}
const item = monitor.getItem();
let newItem = {};
if (item) {
newItem = {
active: item.active,
altImageText: item.altImageText,
id: item.id,
imageSrc: item.imageSrc,
imageId: item.imageId,
instanceNumber: item.instanceNumber,
error: item.error,
numImageFrames: item.numImageFrames,
seriesDescription: item.seriesDescription,
seriesNumber: item.seriesNumber,
stackPercentComplete: item.stackPercentComplete,
};
}
return {
...newItem,
isDragging: monitor.isDragging(),
};
};
const onOffsetChange = monitor => () => {
if (!dragPreviewRef) return;
const offset =
monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset();
if (!offset) return;
const transform = `translate(${offset.x}px, ${offset.y}px)`;
dragPreviewRef.style['transform'] = transform;
dragPreviewRef.style['-webkit-transform'] = transform;
};
const updateRef = ref => {
dragPreviewRef = ref;
};
class DragPreview extends PureComponent {
render() {
const { isDragging } = this.props;
if (!isDragging) return null;
return (
<div className="DragPreview">
<div className="source-preview" ref={updateRef}>
<ThumbnailEntry {...this.props} />
</div>
</div>
);
}
}
DragPreview.propTypes = {
isDragging: PropTypes.bool,
};
export default DragLayer(collector)(memo(DragPreview));
@@ -1,14 +0,0 @@
.DragPreview
.source-preview {
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
opacity: 0.5;
z-index: 9999;
left: 0;
top: 0;
transition: none;
pointer-events: none;
-webkit-touch-callout: none;
}
@@ -1,7 +1,8 @@
/* global cornerstone */
import './ImageThumbnail.styl';
import React, { PureComponent } from 'react';
import { utils } from '@ohif/core';
import React, { useState, useEffect, createRef } from 'react';
import PropTypes from 'prop-types';
import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator';
@@ -12,100 +13,132 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
// - Set as external dependency?
// - Pass in the entire load and render function as a prop?
//import cornerstone from 'cornerstone-core';
function ImageThumbnail(props) {
const {
width,
height,
imageSrc,
imageId,
stackPercentComplete,
error: propsError,
} = props;
export default class ImageThumbnail extends PureComponent {
static propTypes = {
imageSrc: PropTypes.string,
imageId: PropTypes.string,
error: PropTypes.bool.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
stackPercentComplete: PropTypes.number.isRequired,
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [image, setImage] = useState({});
const canvasRef = createRef();
let loadingOrError;
let cancelablePromise;
if (propsError || error) {
loadingOrError = <ViewportErrorIndicator />;
} else if (isLoading) {
loadingOrError = <ViewportLoadingIndicator />;
}
const showStackLoadingProgressBar = stackPercentComplete !== undefined;
const shouldRenderToCanvas = () => {
return imageId && !imageSrc;
};
static defaultProps = {
error: false,
stackPercentComplete: 0,
width: 217,
height: 123,
const fetchImagePromise = () => {
if (!cancelablePromise) {
return;
}
setLoading(true);
cancelablePromise
.then(response => {
setImage(response);
})
.catch(error => {
if (error.isCanceled) return;
setLoading(false);
setError(true);
throw new Error(error);
});
};
constructor(props) {
super(props);
const setImagePromise = () => {
if (shouldRenderToCanvas()) {
cancelablePromise = utils.makeCancelable(
cornerstone.loadAndCacheImage(imageId)
);
}
};
this.canvas = React.createRef();
this.state = {
loading: this.shouldRenderToCanvas(),
const purgeCancelablePromise = () => {
if (cancelablePromise) {
cancelablePromise.cancel();
}
};
useEffect(() => {
return () => {
purgeCancelablePromise();
};
}
}, []);
shouldRenderToCanvas() {
return this.props.imageId && !this.props.imageSrc;
}
componentDidMount() {
if (this.shouldRenderToCanvas()) {
cornerstone
.loadAndCacheImage(this.props.imageId)
.then(image => {
cornerstone.renderToCanvas(this.canvas.current, image);
this.setState({
loading: false,
});
})
.catch(error => {
this.setState({
loading: false,
error: true,
});
throw new Error(error);
});
useEffect(() => {
if (image.imageId) {
cornerstone.renderToCanvas(canvasRef.current, image);
setLoading(false);
}
}
}, [image.imageId]);
render() {
let loadingOrError;
if (this.props.error) {
loadingOrError = <ViewportErrorIndicator />;
} else if (this.state.loading) {
loadingOrError = <ViewportLoadingIndicator />;
useEffect(() => {
if (!image.imageId || image.imageId !== imageId) {
purgeCancelablePromise();
setImagePromise();
fetchImagePromise();
}
}, [imageId]);
const showStackLoadingProgressBar =
this.props.stackPercentComplete !== undefined;
return (
<div className="ImageThumbnail">
<div className="image-thumbnail-canvas">
{this.shouldRenderToCanvas() ? (
<canvas
ref={this.canvas}
width={this.props.width}
height={this.props.height}
/>
) : (
<img
className="static-image"
src={this.props.imageSrc}
//width={this.props.width}
height={this.props.height}
alt={''}
/>
)}
</div>
{loadingOrError}
{showStackLoadingProgressBar && (
<div className="image-thumbnail-progress-bar">
<div
className="image-thumbnail-progress-bar-inner"
style={{ width: `${this.props.stackPercentComplete}%` }}
/>
</div>
)}
{this.state.loading && (
<div className="image-thumbnail-loading-indicator"></div>
return (
<div className="ImageThumbnail">
<div className="image-thumbnail-canvas">
{shouldRenderToCanvas() ? (
<canvas ref={canvasRef} width={width} height={height} />
) : (
<img
className="static-image"
src={imageSrc}
//width={this.props.width}
height={height}
alt={''}
/>
)}
</div>
);
}
{loadingOrError}
{showStackLoadingProgressBar && (
<div className="image-thumbnail-progress-bar">
<div
className="image-thumbnail-progress-bar-inner"
style={{ width: `${stackPercentComplete}%` }}
/>
</div>
)}
{isLoading && <div className="image-thumbnail-loading-indicator"></div>}
</div>
);
}
ImageThumbnail.propTypes = {
imageSrc: PropTypes.string,
imageId: PropTypes.string,
error: PropTypes.bool,
width: PropTypes.number,
height: PropTypes.number,
stackPercentComplete: PropTypes.number.isRequired,
};
ImageThumbnail.defaultProps = {
error: false,
stackPercentComplete: 0,
width: 217,
height: 123,
};
export default ImageThumbnail;
@@ -1,62 +1,103 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { ThumbnailEntry } from './ThumbnailEntry';
import ThumbnailEntryDragSource from './ThumbnailEntryDragSource.js';
import { Thumbnail } from './Thumbnail.js';
import './StudyBrowser.styl';
class StudyBrowser extends Component {
static defaultProps = {
studies: [],
supportsDragAndDrop: true,
};
function StudyBrowser(props) {
const {
studies,
onThumbnailClick,
onThumbnailDoubleClick,
supportsDrag,
} = props;
static propTypes = {
studies: PropTypes.array.isRequired,
supportsDragAndDrop: PropTypes.bool.isRequired,
onThumbnailClick: PropTypes.func,
onThumbnailDoubleClick: PropTypes.func,
};
return (
<div className="study-browser">
<div className="scrollable-study-thumbnails">
{studies
.map((study, studyIndex) => {
const { studyInstanceUid } = study;
return study.thumbnails.map((thumb, thumbIndex) => {
// TODO: Thumb has more props than we care about?
const {
altImageText,
displaySetInstanceUid,
imageId,
instanceNumber,
numImageFrames,
seriesDescription,
seriesNumber,
stackPercentComplete,
} = thumb;
render() {
const studies = this.props.studies;
const thumbnails = studies.map((study, studyIndex) => {
return study.thumbnails.map((thumb, thumbIndex) => {
if (this.props.supportsDragAndDrop) {
return (
<ThumbnailEntryDragSource
key={thumb.displaySetInstanceUid}
{...study}
{...thumb}
id={`${studyIndex}_${thumbIndex}`}
onClick={this.props.onThumbnailClick}
onDoubleClick={this.props.onThumbnailDoubleClick}
/>
);
} else {
return (
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
<ThumbnailEntry
key={thumb.displaySetInstanceUid}
{...study}
{...thumb}
id={`${studyIndex}_${thumbIndex}`}
onClick={this.props.onThumbnailClick}
onDoubleClick={this.props.onThumbnailDoubleClick}
/>
</div>
);
}
});
});
const components = thumbnails.flat();
return (
<div className="StudyBrowser">
<div className="scrollable-study-thumbnails">{components}</div>
return (
<div
key={thumb.displaySetInstanceUid}
className="thumbnail-container"
data-cy="thumbnail-list"
>
<Thumbnail
supportsDrag={supportsDrag}
key={`${studyIndex}_${thumbIndex}`}
id={`${studyIndex}_${thumbIndex}`} // Unused?
// Study
studyInstanceUid={studyInstanceUid} // used by drop
// Thumb
altImageText={altImageText}
imageId={imageId}
instanceNumber={instanceNumber}
displaySetInstanceUid={displaySetInstanceUid} // used by drop
numImageFrames={numImageFrames}
seriesDescription={seriesDescription}
seriesNumber={seriesNumber}
stackPercentComplete={stackPercentComplete}
// Events
onClick={onThumbnailClick.bind(
undefined,
displaySetInstanceUid
)}
onDoubleClick={onThumbnailDoubleClick}
/>
</div>
);
});
})
.flat()}
</div>
);
}
</div>
);
}
const noop = () => {};
StudyBrowser.propTypes = {
studies: PropTypes.arrayOf(
PropTypes.shape({
studyInstanceUid: PropTypes.string.isRequired,
thumbnails: PropTypes.arrayOf(
PropTypes.shape({
altImageText: PropTypes.string,
displaySetInstanceUid: PropTypes.string.isRequired,
imageId: PropTypes.string,
instanceNumber: PropTypes.number,
numImageFrames: PropTypes.number,
seriesDescription: PropTypes.string,
seriesNumber: PropTypes.number,
stackPercentComplete: PropTypes.number,
})
),
})
).isRequired,
supportsDrag: PropTypes.bool,
onThumbnailClick: PropTypes.func,
onThumbnailDoubleClick: PropTypes.func,
};
StudyBrowser.defaultProps = {
studies: [],
supportsDrag: true,
onThumbnailClick: noop,
onThumbnailDoubleClick: noop,
};
export { StudyBrowser };
@@ -1,4 +1,4 @@
.StudyBrowser
.study-browser
float: left
height: 100%
width: 100%
@@ -22,7 +22,7 @@
&::-webkit-scrollbar
display: none
.ThumbnailEntryContainer
.thumbnail-container
margin: 0 auto;
padding-bottom: 1.5rem;
@@ -0,0 +1,152 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDrag } from 'react-dnd';
import ImageThumbnail from './ImageThumbnail';
import classNames from 'classnames';
import './Thumbnail.styl';
function ThumbnailFooter({
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames,
}) {
const infoOnly = !seriesDescription;
const getInfo = (value, icon, className = '') => {
return (
<div className={classNames('item item-series', className)}>
<div className="icon">{icon}</div>
<div className="value">{value}</div>
</div>
);
};
const getSeriesInformation = (
seriesNumber,
instanceNumber,
numImageFrames
) => {
if (!seriesNumber && !instanceNumber && !numImageFrames) {
return;
}
return (
<div className="series-information">
{getInfo(seriesNumber, 'S:')}
{getInfo(instanceNumber, 'I:')}
{getInfo(numImageFrames, '', 'image-frames')}
</div>
);
};
return (
<div className={classNames('series-details', { 'info-only': infoOnly })}>
<div className="series-description">{seriesDescription}</div>
{getSeriesInformation(seriesNumber, instanceNumber, numImageFrames)}
</div>
);
}
function Thumbnail(props) {
const {
active,
altImageText,
error,
displaySetInstanceUid,
imageId,
imageSrc,
instanceNumber,
numImageFrames,
seriesDescription,
seriesNumber,
stackPercentComplete,
studyInstanceUid,
onClick,
onDoubleClick,
onMouseDown,
supportsDrag,
} = props;
const [collectedProps, drag, dragPreview] = useDrag({
// `droppedItem` in `dropTarget`
// The only data it will have access to
item: {
studyInstanceUid,
displaySetInstanceUid,
type: 'thumbnail', // Has to match `dropTarget`'s type
},
canDrag: function(monitor) {
return supportsDrag;
},
});
const hasImage = imageSrc || imageId;
const hasAltText = altImageText !== undefined;
return (
<div
ref={drag}
className={classNames('thumbnail', { active: active })}
onClick={onClick}
onDoubleClick={onDoubleClick}
onMouseDown={onMouseDown}
>
{/* SHOW IMAGE */}
{hasImage && (
<ImageThumbnail
imageSrc={imageSrc}
imageId={imageId}
error={error}
stackPercentComplete={stackPercentComplete}
/>
)}
{/* SHOW TEXT ALTERNATIVE */}
{!hasImage && hasAltText && (
<div className={'alt-image-text p-x-1'}>
<h1>{altImageText}</h1>
</div>
)}
{ThumbnailFooter(props)}
</div>
);
}
const noop = () => {};
Thumbnail.propTypes = {
supportsDrag: PropTypes.bool,
id: PropTypes.string.isRequired,
displaySetInstanceUid: PropTypes.string.isRequired,
studyInstanceUid: PropTypes.string.isRequired,
imageSrc: PropTypes.string,
imageId: PropTypes.string,
error: PropTypes.bool,
active: PropTypes.bool,
stackPercentComplete: PropTypes.number,
/**
altImageText will be used when no imageId or imageSrc is provided.
It will be displayed inside the <div>. This is useful when it is difficult
to make a preview for a type of DICOM series (e.g. DICOM-SR)
*/
altImageText: PropTypes.string,
seriesDescription: PropTypes.string,
seriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
instanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
numImageFrames: PropTypes.number,
onDoubleClick: PropTypes.func,
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
};
Thumbnail.defaultProps = {
supportsDrag: false,
active: false,
error: false,
stackPercentComplete: 0,
onDoubleClick: noop,
onClick: noop,
onMouseDown: noop,
};
export { Thumbnail };
@@ -4,12 +4,12 @@
--series-count-background-color: #678696;
}
.ThumbnailEntry
.thumbnail
cursor: pointer
display: table
.p-x-1
padding: 0 1rem
margin: 0 auto;
min-width: 90%;
padding-bottom: 1.5rem;
.alt-image-text
align-items: center;
@@ -31,6 +31,8 @@
color: var(--text-primary-color);
.series-details
display: flex;
flex-direction: column
color: var(--text-primary-color);
font-size: 14px
line-height: 1.3em
@@ -38,64 +40,30 @@
margin-left: 1rem
margin-right: 0.5rem
max-width: 217px
min-height: 36px
position: relative
word-wrap: break-word
&.info-only
.series-description
display: none
.series-information
display: flex
flex-grow: 1
float: none
max-width: none
padding-right: 0
.item
flex: 1
text-align: center
.icon, .value
display: inline
float: none
line-height: 25px
.series-description
flex-grow: 1
&.info-only .series-description
display: none
.series-information
display: flex
flex-direction: row
align-items: center
.item
flex: 1
text-align: center
justify-content: space-between
.item-frames .icon
height: 18px
.value
color: var(--text-secondary-color);
display: inline
font-size: 12px
margin-left: 4px
text-overflow: ellipsis
white-space: nowrap
.icon
color: var(--active-color);
display: inline
font-size: 10px
font-weight: 900
text-align: right
div
.item-series.image-frames .icon
background-color: var(--series-count-background-color);
margin-top: 6px
position: relative
&:after
content: ''
background-color: var(--active-color);
box-shadow: 1px 1px rgba(0, 0, 0, .115)
left: -4px
@@ -104,16 +72,27 @@
&
&:after
border: 0px solid var(--series-count-background-color)
content: ''
border: 0px solid var(--series-count-background-color)
display: inline-block
height: 11px
width: 11px
.image-thumbnail-clone
z-index: 100000
pointer-events: none
.value
color: var(--text-secondary-color);
display: inline-block
font-size: 12px
margin-left: 4px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
width: calc(100% - 15px)
vertical-align: middle
&:hover
border-color: var(--hover-color);
box-shadow: none
.icon
color: var(--active-color);
display: inline-block
font-size: 10px
font-weight: 900
text-align: right
width: 11px
@@ -1,114 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ImageThumbnail from './ImageThumbnail';
import './ThumbnailEntry.styl';
import classnames from 'classnames';
class ThumbnailEntry extends Component {
static defaultProps = {
active: false,
error: false,
stackPercentComplete: 0,
};
static propTypes = {
id: PropTypes.string.isRequired,
imageSrc: PropTypes.string,
imageId: PropTypes.string,
error: PropTypes.bool.isRequired,
active: PropTypes.bool.isRequired,
stackPercentComplete: PropTypes.number,
/**
altImageText will be used when no imageId or imageSrc is provided.
It will be displayed inside the <div>. This is useful when it is difficult
to make a preview for a type of DICOM series (e.g. DICOM-SR)
*/
altImageText: PropTypes.string,
seriesDescription: PropTypes.string,
seriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
instanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
numImageFrames: PropTypes.number,
onDoubleClick: PropTypes.func,
onClick: PropTypes.func,
};
render() {
const hasInstanceNumber = this.props.instanceNumber !== undefined;
let className = classnames('ThumbnailEntry noselect', {
active: this.props.active,
});
let contents = null;
if (this.props.imageSrc || this.props.imageId) {
contents = (
<div className="p-x-1">
<ImageThumbnail
imageSrc={this.props.imageSrc}
imageId={this.props.imageId}
error={this.props.error}
stackPercentComplete={this.props.stackPercentComplete}
/>
</div>
);
} else if (this.props.altImageText) {
contents = (
<div className="p-x-1">
<div className="alt-image-text">
<h1>{this.props.altImageText}</h1>
</div>
</div>
);
}
return (
<div
className={className}
onClick={this.onClick}
onDoubleClick={this.onDoubleClick}
onMouseDown={this.onMouseDown}
>
{contents}
<div
className="series-details"
>
<div className="series-description">
{this.props.seriesDescription}
</div>
<div className="series-information">
<div className="item item-series">
<div className="icon">S:</div>
<div className="value">{this.props.seriesNumber}</div>
</div>
{hasInstanceNumber && (
<div className="item item-series">
<div className="icon">I:</div>
<div className="value">{this.props.instanceNumber}</div>
</div>
)}
<div className="item item-frames">
<div className="icon">
<div />
</div>
<div className="value">{this.props.numImageFrames}</div>
</div>
</div>
</div>
</div>
);
}
onClick = () => {
if (this.props.onClick) {
this.props.onClick(this.props.displaySetInstanceUid);
}
};
onDoubleClick = () => {
if (this.props.onDoubleClick) {
this.props.onDoubleClick(this.props.displaySetInstanceUid);
}
};
}
export { ThumbnailEntry };
@@ -1,64 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { DragSource } from 'react-dnd';
import DragPreview from './DragPreview';
import { ThumbnailEntry } from './ThumbnailEntry.js';
// Drag sources and drop targets only interact
// if they have the same string type.
const Types = {
THUMBNAIL: 'thumbnail',
};
const thumbnailSource = {
/*canDrag(props) {
return props.error === false;
},*/
beginDrag(props) {
return props;
},
endDrag(props, monitor) {
//const item = monitor.getItem();
const dropResult = monitor.getDropResult();
if (dropResult) {
//console.log(`You dropped ${item.id} into ${dropResult.id}!`);
//console.log(item);
}
},
};
class ThumbnailEntryDragSource extends Component {
static propTypes = {
connectDragSource: PropTypes.func.isRequired,
isDragging: PropTypes.bool.isRequired,
};
static defaultProps = {
isDragging: false,
};
render() {
const { connectDragSource } = this.props;
const dropEffect = 'copy';
return connectDragSource(
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
<DragPreview {...this.props} />
<ThumbnailEntry {...this.props} />
</div>,
{ dropEffect }
);
}
}
const collect = (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
});
export default DragSource(Types.THUMBNAIL, thumbnailSource, collect)(
ThumbnailEntryDragSource
);
@@ -1,2 +1,2 @@
export { StudyBrowser } from './StudyBrowser.js';
export { ThumbnailEntry } from './ThumbnailEntry.js';
export { Thumbnail } from './Thumbnail.js';
@@ -0,0 +1,83 @@
/* NOTE: the order of these styles DO matter */
/* Will edit everything selected including everything between a range of dates */
.CalendarDay__selected_span {
background: var(--table-text-secondary-color);
color: #fff;
border-color: #e4e7e7;
}
/* Will edit selected date or the endpoints of a range of dates */
.CalendarDay__selected {
background: var(--table-text-secondary-color);
color: #fff;
border-color: #e4e7e7;
}
/* Will edit when hovered over. _span style also has this property */
.CalendarDay__selected:hover {
background: var(--table-text-secondary-color);
color: #fff;
border-color: #e4e7e7;
}
/* Will edit when the second date (end date) in a range of dates
is not yet selected. Edits the dates between your mouse and said date */
.CalendarDay__hovered_span:hover,
.CalendarDay__hovered_span {
background: var(--table-text-secondary-color);
color: #fff;
border-color: #e4e7e7;
}
/* EXTERIOR INPUT STYLE */
/* Container - placement */
.DateRangePicker {
height: 40px;
margin: 0 5px 20px 5px;
cursor: pointer;
border: none;
width: 100%;
}
/* Container - visual */
.DateRangePickerInput {
width: calc(100% - 10px); /* Just use padding? */
background-color: var(--input-background-color);
border-color: var(--input-background-color);
color: var(--input-placeholder-color);
height: 40px;
}
.DateRangePickerInput.DateRangePickerInput__withBorder {
border-radius: 4px;
background-color: var(--input-background-color);
}
/* Input Container */
.DateInput {
width: 97px;
height: 38px;
border-radius: 4px;
background-color: var(--input-background-color);
}
/* Actual Input Element */
.DateInput > .DateInput_input {
border-color: transparent;
background-color: transparent;
color: var(--input-placeholder-color);
height: 38px;
font-size: 10pt;
padding: 0;
}
/* PRESETS */
.PresetDateRangePicker_panel {
display: flex;
justify-content: space-between;
}
.PresetDateRangePicker_button {
margin: 0;
padding: 4px 8px;
}
@@ -2,13 +2,12 @@
// https://github.com/airbnb/react-dates#initialize
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';
import './CustomDateRangePicker.css';
import React from 'react';
import PropTypes from 'prop-types';
import { DateRangePicker } from 'react-dates';
import './CustomDateRangePicker.styl';
export default class CustomDateRangePicker extends React.Component {
static propTypes = {
presets: PropTypes.arrayOf(
@@ -85,15 +84,13 @@ export default class CustomDateRangePicker extends React.Component {
} = this.props;
return (
<div>
<DateRangePicker
{...dateRangePickerProps}
startDate={this.state.startDate}
endDate={this.state.endDate}
renderCalendarInfo={this.renderDatePresets}
onDatesChange={this.onDatesChange}
/>
</div>
<DateRangePicker
{...dateRangePickerProps}
startDate={this.state.startDate}
endDate={this.state.endDate}
renderCalendarInfo={this.renderDatePresets}
onDatesChange={this.onDatesChange}
/>
);
}
}
@@ -1,84 +0,0 @@
.CalendarDay__selected_span {
background: var(--calendar-main-color);
color: var(--calendar-day-color);
border: 1px solid;
border-color: var(--calendar-day-border-color);
}
.CalendarDay__default:hover {
color: var(--calendar-main-color);
}
.CalendarDay__selected_span:active, .CalendarDay__selected_span:hover {
background: var(--calendar-day-active-hover-background-color);
border-color: var(--calendar-day-border-color);
color: var(--calendar-day-color);
}
.CalendarDay__selected {
background: var(--calendar-main-color);
color: var(--calendar-day-color);
border-color: var(--calendar-day-border-color);
}
.CalendarDay__selected:hover {
background: var(--calendar-main-color);
color: var(--calendar-day-color);
border-color: var(--calendar-day-border-color);
}
.CalendarDay__hovered_span:hover,
.CalendarDay__hovered_span {
background: var(--calendar-main-color);
color: var(--calendar-day-color);
border-color:var(--calendar-day-color);
}
.DateInput {
width: 97px
}
.DateInput_input {
background-color: var(--input-background-color)
border-color: var(--input-background-color)
color: var(--input-placeholder-color)
height: 38px
font-size: 10pt
}
.DateRangePickerInput {
background-color: var(--input-background-color)
border-color: var(--input-background-color)
color: var(--input-placeholder-color)
height: 40px
}
.PresetDateRangePicker_panel: {
padding: 0 22px 11px 22px;
}
.PresetDateRangePicker_button {
position: relative;
height: 100%;
textAlign: center;
background: none;
border: 2px solid var(--calendar-main-color);
color: var(--calendar-main-color);
padding: 4px 12px;
marginRight: 8;
font: inherit;
fontWeight: 700;
lineHeight: normal;
overflow: visible;
boxSizing: border-box;
cursor: pointer;
:active: {
outline: 0;
}
}
.PresetDateRangePicker_button__selected: {
color: var(--calendar-day-color);
background: var(--calendar-main-color);
};
@@ -1,11 +1,9 @@
import './StudyListToolbar.styl';
import React, { PureComponent } from 'react';
import { Icon } from './../../elements/Icon';
import PropTypes from 'prop-types';
class StudylistToolbar extends PureComponent {
class PageToolbar extends PureComponent {
static propTypes = {
onImport: PropTypes.func,
};
@@ -37,4 +35,4 @@ class StudylistToolbar extends PureComponent {
}
}
export { StudylistToolbar };
export { PageToolbar };
@@ -1,64 +1,63 @@
.pagination-area
color: var(--text-secondary-color);
font-size: 13px
font-weight: normal !important
display: flex;
color: var(--text-secondary-color);
font-size: 13px
font-weight: normal !important
label
font-weight: normal
select
margin: 5px
background-color: var(--primary-background-color)
color: white
.row
display: flex;
.rows-dropdown
width: 25%;
padding-right: 15px;
padding-left: 15px;
.pagination-buttons
width: 75%;
padding-right: 15px;
padding-left: 15px;
.form-group
margin-bottom: 15px;
.rows-per-page label.wrapperLabel
display: inline-table !important
margin: 0 4px
select
margin: 0px 4px 0px 4px
width: 42px
.page-buttons
margin: 0
text-align: right
label
font-weight: normal
ul.pagination-control
margin: 0
li
display: table-cell
padding: 5px 2px
select
margin: 5px
background-color: var(--primary-background-color)
color: white
.rows-dropdown
width: 25%;
padding-right: 15px;
padding-left: 15px;
.pagination-buttons
width: 75%;
padding-right: 15px;
padding-left: 15px;
.form-group
margin-bottom: 15px;
.rows-per-page label.wrapperLabel
display: inline-table !important
margin: 0 4px
select
margin: 0px 4px 0px 4px
width: 42px
.page-buttons
margin: 0
text-align: right
font-weight: normal
ul.pagination-control
margin: 0
li
display: table-cell
padding: 5px 2px
button
padding: 4px 8px
background-color: var(--primary-background-color)
border-color: var(--ui-gray)
color: var(--ui-gray-darkest)
color: white
text-decoration: none
&:hover:enabled
color: var(--active-color)
.active
button
padding: 4px 8px
background-color: var(--primary-background-color)
border-color: var(--ui-gray)
color: var(--ui-gray-darkest)
background-color: var(--ui-gray)
border-color: #ddd
color: white
text-decoration: none
&:hover:enabled
color: var(--active-color)
.active
button
background-color: var(--ui-gray)
border-color: #ddd
color: white
+365 -403
View File
@@ -1,428 +1,390 @@
import './StudyList.styl';
import React, { Component } from 'react';
import CustomDateRangePicker from './CustomDateRangePicker.js';
import { Icon } from './../../elements/Icon';
import { PaginationArea } from './PaginationArea.js';
import React from 'react';
import classNames from 'classnames';
import TableSearchFilter from './TableSearchFilter.js';
import useMedia from '../../hooks/useMedia.js';
import PropTypes from 'prop-types';
import { StudyListLoadingText } from './StudyListLoadingText.js';
import { StudylistToolbar } from './StudyListToolbar.js';
import { isInclusivelyBeforeDay } from 'react-dates';
import moment from 'moment';
import debounce from 'lodash.debounce';
import { withTranslation } from '../../utils/LanguageProvider';
const today = moment();
const lastWeek = moment().subtract(7, 'day');
const lastMonth = moment().subtract(1, 'month');
/**
*
*
* @param {*} props
* @returns
*/
function StudyList(props) {
const {
isLoading,
hasError,
studies,
sort,
onSort: handleSort,
filterValues,
onFilterChange: handleFilterChange,
onSelectItem: handleSelectItem,
t,
//
studyListDateFilterNumDays,
} = props;
class StudyList extends Component {
static propTypes = {
studies: PropTypes.array.isRequired,
onSelectItem: PropTypes.func.isRequired,
onSearch: PropTypes.func.isRequired,
currentPage: PropTypes.number,
rowsPerPage: PropTypes.number,
studyListDateFilterNumDays: PropTypes.number,
studyListFunctionsEnabled: PropTypes.bool,
defaultSort: PropTypes.shape({
field: PropTypes.string.isRequired,
order: PropTypes.oneOf(['desc', 'asc']).isRequired,
}),
onImport: PropTypes.func,
pageOptions: PropTypes.array,
};
static defaultProps = {
currentPage: 0,
rowsPerPage: 25,
studyListDateFilterNumDays: 7,
};
static studyDatePresets = [
const largeTableMeta = [
{
text: 'Today',
start: today,
end: today,
displayText: t('PatientName'),
fieldName: 'patientName',
inputType: 'text',
size: 330,
},
{
text: 'Last 7 days',
start: lastWeek,
end: today,
displayText: t('MRN'),
fieldName: 'patientId',
inputType: 'text',
size: 378,
},
{
text: 'Last 30 days',
start: lastMonth,
end: today,
displayText: t('AccessionNumber'),
fieldName: 'accessionNumber',
inputType: 'text',
size: 180,
},
{
displayText: t('StudyDate'),
fieldName: 'studyDate',
inputType: 'date-range',
size: 300,
},
{
displayText: t('Modality'),
fieldName: 'modalities',
inputType: 'text',
size: 114,
},
{
displayText: t('StudyDescription'),
fieldName: 'studyDescription',
inputType: 'text',
size: 335,
},
];
constructor(props) {
super(props);
const mediumTableMeta = [
{
displayText: 'Patient / MRN',
fieldName: 'patientNameOrId',
inputType: 'text',
size: 250,
},
{
displayText: 'Description',
fieldName: 'accessionOrModalityOrDescription',
inputType: 'text',
size: 350,
},
{
displayText: t('StudyDate'),
fieldName: 'studyDate',
inputType: 'date-range',
size: 300,
},
];
const sortData = {
field: undefined,
order: undefined,
};
const smallTableMeta = [
{
displayText: 'Search',
fieldName: 'allFields',
inputType: 'text',
size: 100,
},
];
// init from props
if (props.defaultSort) {
sortData.field = props.defaultSort.field;
// todo: -1, 0, 1?
sortData.order = props.defaultSort.order; // asc, desc
}
const tableMeta = useMedia(
['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
[largeTableMeta, mediumTableMeta, smallTableMeta],
smallTableMeta
);
this.defaultStartDate = moment().subtract(
this.props.studyListDateFilterNumDays,
'days'
);
this.defaultEndDate = moment();
const totalSize = tableMeta
.map(field => field.size)
.reduce((prev, next) => prev + next);
this.state = {
loading: false,
error: false,
searchData: {
sortData,
currentPage: this.props.currentPage,
rowsPerPage: this.props.rowsPerPage,
studyDateFrom: this.defaultStartDate,
studyDateTo: this.defaultEndDate,
},
highlightedItem: '',
};
return (
<table className="table table--striped table--hoverable">
<colgroup>
{tableMeta.map((field, i) => {
const size = field.size;
const percentWidth = (size / totalSize) * 100.0;
this.getChangeHandler = this.getChangeHandler.bind(this);
this.getBlurHandler = this.getBlurHandler.bind(this);
this.onInputKeydown = this.onInputKeydown.bind(this);
this.nextPage = this.nextPage.bind(this);
this.prevPage = this.prevPage.bind(this);
this.onRowsPerPageChange = this.onRowsPerPageChange.bind(this);
this.delayedSearch = debounce(this.search, 250);
}
getChangeHandler(key) {
return event => {
this.delayedSearch.cancel();
this.setSearchData(key, event.target.value, this.delayedSearch);
};
}
getBlurHandler(key) {
return event => {
this.delayedSearch.cancel();
this.setSearchData(key, event.target.value, this.search);
};
}
setSearchData(key, value, callback) {
const searchData = this.state.searchData;
searchData[key] = value;
this.setState({ searchData }, callback);
}
setSearchDataBatch(keyValues, callback) {
const searchData = this.state.searchData;
Object.keys(keyValues).forEach(key => {
searchData[key] = keyValues[key];
});
this.setState({ searchData }, callback);
}
async onInputKeydown(event) {
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
this.delayedSearch.cancel();
// reset the page because user is doing a new search
this.setSearchData('currentPage', 0, this.search);
}
}
async search() {
try {
this.setState({ loading: true, error: false });
await this.props.onSearch(this.state.searchData);
} catch (error) {
this.setState({ error: true });
throw new Error(error);
} finally {
this.setState({ loading: false });
}
}
renderNoMachingResults() {
if (!this.props.studies.length && !this.state.error) {
return <div className="notFound">No matching results</div>;
}
}
renderHasError() {
if (this.state.error) {
return (
<div className="notFound">There was an error fetching studies</div>
);
}
}
renderIsLoading() {
if (this.state.loading) {
return (
<div className="loading">
<StudyListLoadingText />
</div>
);
}
}
nextPage(currentPage) {
currentPage = currentPage + 1;
this.delayedSearch.cancel();
this.setSearchData('currentPage', currentPage, this.search);
}
prevPage(currentPage) {
currentPage = currentPage - 1;
this.delayedSearch.cancel();
this.setSearchData('currentPage', currentPage, this.search);
}
onRowsPerPageChange(rowsPerPage) {
this.delayedSearch.cancel();
this.setSearchDataBatch({ rowsPerPage, currentPage: 0 }, this.search);
}
onSortClick(field) {
return () => {
let order;
const sort = this.state.searchData.sortData;
const isSortedField = sort.field === field;
if (isSortedField) {
if (sort.order === 'asc') {
order = 'desc';
} else {
order = undefined;
field = undefined;
}
} else {
order = 'asc';
}
this.delayedSearch.cancel();
this.setSearchData('sortData', { field, order }, this.search);
};
}
onHighlightItem(studyItemUid) {
this.setState({ highlightedItem: studyItemUid });
}
renderTableRow(study) {
return (
<tr
key={study.studyInstanceUid}
className={
this.state.highlightedItem === study.studyInstanceUid
? 'studylistStudy noselect active'
: 'studylistStudy noselect'
}
onMouseDown={event => {
// middle/wheel click
if (event.button === 1) {
this.props.onSelectItem(study.studyInstanceUid);
}
}}
onClick={() => {
this.onHighlightItem(study.studyInstanceUid);
this.props.onSelectItem(study.studyInstanceUid);
}}
>
<td className={study.patientName ? 'patientName' : 'emptyCell'}>
{study.patientName || `(${this.props.t('Empty')})`}
</td>
<td className="patientId">{study.patientId}</td>
<td className="accessionNumber">{study.accessionNumber}</td>
<td className="studyDate">{study.studyDate}</td>
<td className="modalities">{study.modalities}</td>
<td className="studyDescription">{study.studyDescription}</td>
</tr>
);
}
render() {
const tableMeta = {
patientName: {
displayText: this.props.t('PatientName'),
sort: 0,
},
patientId: {
displayText: this.props.t('MRN'),
sort: 0,
},
accessionNumber: {
displayText: this.props.t('AccessionNumber'),
sort: 0,
},
studyDate: {
displayText: this.props.t('StudyDate'),
inputType: 'date-range',
sort: 0,
},
modalities: {
displayText: this.props.t('Modality'),
sort: 0,
},
studyDescription: {
displayText: this.props.t('StudyDescription'),
sort: 0,
},
};
// Apply sort
const sortedFieldName = this.state.searchData.sortData.field;
const sortedField = tableMeta[sortedFieldName];
if (sortedField) {
const sortOrder = this.state.searchData.sortData.order;
sortedField.sort = sortOrder === 'asc' ? 1 : 2;
}
// Sort Icons
const sortIcons = ['sort', 'sort-up', 'sort-down'];
return (
<div className="StudyList">
<div className="studyListToolbar clearfix">
<div className="header pull-left">{this.props.t('StudyList')}</div>
<div className="studyCount pull-right">
{this.props.studies.length}
</div>
<div className="pull-right">
{this.props.studyListFunctionsEnabled ? (
<StudylistToolbar onImport={this.props.onImport} />
) : null}
</div>
{this.props.children}
</div>
<div className="theadBackground" />
<div id="studyListContainer">
<table id="tblStudyList" className="studylistResult table noselect">
<thead>
<tr>
{Object.keys(tableMeta).map((fieldName, i) => {
const field = tableMeta[fieldName];
return (
<React.Fragment key={i}>
<th className={fieldName}>
<div
id={`_${fieldName}`}
className="display-text"
onClick={this.onSortClick(fieldName)}
>
<span>{field.displayText}</span>
<Icon
name={sortIcons[field.sort]}
style={{ fontSize: '12px' }}
/>
</div>
{!field.inputType && (
<input
type="text"
className="form-control studylist-search"
id={fieldName}
value={this.state[fieldName]}
onKeyDown={this.onInputKeydown}
onChange={this.getChangeHandler(fieldName)}
onBlur={this.getBlurHandler(fieldName)}
/>
)}
{field.inputType === 'date-range' && (
<div style={{ display: 'block' }}>
<CustomDateRangePicker
presets={StudyList.studyDatePresets}
showClearDates={true}
startDateId="studyListStartDate"
endDateId="studyListEndDate"
startDate={this.defaultStartDate}
endDate={this.defaultEndDate}
hideKeyboardShortcutsPanel={true}
anchorDirection="left"
isOutsideRange={day =>
!isInclusivelyBeforeDay(day, moment())
}
onDatesChange={({
startDate,
endDate,
preset = false,
}) => {
if (
startDate &&
endDate &&
(this.state.focusedInput === 'endDate' ||
preset)
) {
this.setSearchDataBatch(
{
studyDateFrom: startDate.toDate(),
studyDateTo: endDate.toDate(),
},
this.search
);
this.setState({ focusedInput: false });
} else if (!startDate && !endDate) {
this.setSearchDataBatch(
{
studyDateFrom: null,
studyDateTo: null,
},
this.search
);
}
}}
focusedInput={this.state.focusedInput}
onFocusChange={focusedInput => {
this.setState({ focusedInput });
}}
/>
</div>
)}
</th>
</React.Fragment>
);
})}
</tr>
</thead>
<tbody id="studyListData">
{this.props.studies.map(study => {
return this.renderTableRow(study);
})}
</tbody>
</table>
{this.renderIsLoading()}
{this.renderHasError()}
{this.renderNoMachingResults()}
<PaginationArea
pageOptions={this.props.pageOptions}
currentPage={this.state.searchData.currentPage}
nextPageFunc={this.nextPage}
prevPageFunc={this.prevPage}
onRowsPerPageChange={this.onRowsPerPageChange}
rowsPerPage={this.state.searchData.rowsPerPage}
recordCount={this.props.studies.length}
return <col key={i} style={{ width: `${percentWidth}%` }} />;
})}
</colgroup>
<thead className="table-head">
<tr className="filters">
<TableSearchFilter
meta={tableMeta}
values={filterValues}
onSort={handleSort}
onValueChange={handleFilterChange}
sortFieldName={sort.fieldName}
sortDirection={sort.direction}
studyListDateFilterNumDays={studyListDateFilterNumDays}
/>
</div>
</div>
);
}
</tr>
</thead>
<tbody className="table-body" data-cy="study-list-results">
{/* I'm not in love with this approach, but it's the quickest way for now
*
* - Display different content based on loading, empty, results state
*
* This is not ideal because it create a jump in focus. For loading especially,
* We should keep our current results visible while we load the new ones.
*/}
{/* LOADING */}
{isLoading && (
<tr className="no-hover">
<td colSpan={tableMeta.length}>
<StudyListLoadingText />
</td>
</tr>
)}
{!isLoading && hasError && (
<tr className="no-hover">
<td colSpan={tableMeta.length}>
<div className="notFound">
{t('There was an error fetching studies')}
</div>
</td>
</tr>
)}
{/* EMPTY */}
{!isLoading && !studies.length && (
<tr className="no-hover">
<td colSpan={tableMeta.length}>
<div className="notFound">{t('No matching results')}</div>
</td>
</tr>
)}
{!isLoading &&
studies.map((study, index) => (
<TableRow
key={`${study.studyInstanceUid}-${index}`}
onClick={studyInstanceUid => handleSelectItem(studyInstanceUid)}
accessionNumber={study.accessionNumber || ''}
modalities={study.modalities}
patientId={study.patientId || ''}
patientName={study.patientName || ''}
studyDate={study.studyDate}
studyDescription={study.studyDescription || ''}
studyInstanceUid={study.studyInstanceUid}
t={t}
/>
))}
</tbody>
</table>
);
}
StudyList.propTypes = {
isLoading: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
studies: PropTypes.array.isRequired,
onSelectItem: PropTypes.func.isRequired,
// ~~ SORT
sort: PropTypes.shape({
fieldName: PropTypes.string,
direction: PropTypes.oneOf(['desc', 'asc', null]),
}).isRequired,
onSort: PropTypes.func.isRequired,
// ~~ FILTERS
filterValues: PropTypes.shape({
patientName: PropTypes.string.isRequired,
patientId: PropTypes.string.isRequired,
accessionNumber: PropTypes.string.isRequired,
studyDate: PropTypes.string.isRequired,
modalities: PropTypes.string.isRequired,
studyDescription: PropTypes.string.isRequired,
patientNameOrId: PropTypes.string.isRequired,
accessionOrModalityOrDescription: PropTypes.string.isRequired,
allFields: PropTypes.string.isRequired,
}).isRequired,
onFilterChange: PropTypes.func.isRequired,
//
studyListDateFilterNumDays: PropTypes.number,
};
StudyList.defaultProps = {};
function TableRow(props) {
const {
accessionNumber,
isHighlighted,
modalities,
patientId,
patientName,
studyDate,
studyDescription,
studyInstanceUid,
onClick: handleClick,
t,
} = props;
const largeRowTemplate = (
<tr
onClick={() => handleClick(studyInstanceUid)}
className={classNames({ active: isHighlighted })}
>
<td className={classNames({ 'empty-value': !patientName })}>
{patientName || `(${t('Empty')})`}
</td>
<td>{patientId}</td>
<td>{accessionNumber}</td>
<td>{studyDate}</td>
<td className={classNames({ 'empty-value': !modalities })}>
{modalities || `(${t('Empty')})`}
</td>
<td>{studyDescription}</td>
</tr>
);
const mediumRowTemplate = (
<tr
onClick={() => handleClick(studyInstanceUid)}
className={classNames({ active: isHighlighted })}
>
<td className={classNames({ 'empty-value': !patientName })}>
{patientName || `(${t('Empty')})`}
<div style={{ color: '#60656f' }}>{patientId}</div>
</td>
<td>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{/* DESCRIPTION */}
<div
className="hide-xs"
style={{
whiteSpace: 'pre-wrap',
flexGrow: 1,
}}
>
{studyDescription}
</div>
{/* MODALITY & ACCESSION */}
<div
style={{
display: 'flex',
flexDirection: 'column',
maxWidth: '80px',
width: '80px',
}}
>
<div
className={classNames({
modalities: modalities,
'empty-value': !modalities,
})}
aria-label={modalities}
title={modalities}
>
{modalities || `(${t('Empty')})`}
</div>
<div
style={{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
aria-label={accessionNumber}
title={accessionNumber}
>
{accessionNumber}
</div>
</div>
</div>
</td>
{/* DATE */}
<td style={{ textAlign: 'center' }}>{studyDate}</td>
</tr>
);
const smallRowTemplate = (
<tr
onClick={() => handleClick(studyInstanceUid)}
className={classNames({ active: isHighlighted })}
>
<td style={{ position: 'relative', overflow: 'hidden' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{/* NAME AND ID */}
<div
className={classNames({ 'empty-value': !patientName })}
style={{ width: '150px', minWidth: '150px' }}
>
<div style={{ fontWeight: 500, paddingTop: '3px' }}>
{patientName || `(${t('Empty')})`}
</div>
<div style={{ color: '#60656f' }}>{patientId}</div>
</div>
{/* DESCRIPTION */}
<div
className="hide-xs"
style={{
whiteSpace: 'pre-wrap',
flexGrow: 1,
paddingLeft: '35px',
}}
>
{studyDescription}
</div>
{/* MODALITY & DATE */}
<div
style={{
display: 'flex',
flexDirection: 'column',
maxWidth: '80px',
width: '80px',
}}
>
<div
className={classNames({
modalities: modalities,
'empty-value': !modalities,
})}
aria-label={modalities}
title={modalities}
>
{modalities || `(${t('Empty')})`}
</div>
<div>{studyDate}</div>
</div>
</div>
</td>
</tr>
);
const rowTemplate = useMedia(
['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
[largeRowTemplate, mediumRowTemplate, smallRowTemplate],
smallRowTemplate
);
return rowTemplate;
}
TableRow.propTypes = {
accessionNumber: PropTypes.string.isRequired,
isHighlighted: PropTypes.bool,
modalities: PropTypes.string,
patientId: PropTypes.string.isRequired,
patientName: PropTypes.string.isRequired,
studyDate: PropTypes.string.isRequired,
studyDescription: PropTypes.string.isRequired,
studyInstanceUid: PropTypes.string.isRequired,
};
TableRow.defaultProps = {
isHighlighted: false,
};
const connectedComponent = withTranslation('StudyList')(StudyList);
export { connectedComponent as StudyList };
@@ -4,6 +4,7 @@
$study-list-padding = 8%;
$study-list-toolbar-height = 75px;
$body-cell-height = 40px;
$body-cell-top-bottom-padding = 16px;
$tablePaddingMediumScreen = 5px
$tablePaddingBigScreen = 3%
@@ -20,21 +21,41 @@ placeholder-color(c)
&:-ms-input-placeholder
color: c
.DateRangePicker
height: 40px
margin: 0 5px 20px 5px
cursor: pointer
border: none
position: block
.study-list-header
.addNewStudy
margin: 0 10px
.studyListToolbar
label
font-weight: 400
cursor: pointer
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
input
width: 0.1px;
height: 0.1px
opacity: 0
overflow: hidden
position: absolute
z-index: -1
color: var(--text-secondary-color)
&:hover
color: var(--hover-color)
&:active
color: var(--active-color)
.study-list-header
display: flex;
justify-content: space-between;
background-color: var(--ui-gray-darker)
height: $study-list-toolbar-height
margin-bottom: 2px
padding: 0 $study-list-padding
&>div
display: inline-block
line-height: $study-list-toolbar-height
.header
font-size: 22px
@@ -42,17 +63,20 @@ placeholder-color(c)
color: var(--table-text-secondary-color)
line-height: $study-list-toolbar-height
.studylistToolbar
height: $study-list-toolbar-height
line-height: $study-list-toolbar-height
.actions
display: flex;
.studyCount
.study-count
color: var(--large-numbers-color)
font-size: 40px
font-weight: 100
line-height: $study-list-toolbar-height
.theadBackground
/*
* Dark gray background with blue border
* Spans width of page to create a distinct area for table filters
*/
.table-head-background
height: 121px
position: absolute
width: 100%
@@ -76,14 +100,115 @@ placeholder-color(c)
height: 1px
z-index: 2
#studyListContainer
.study-list-container
width: 100%
padding: 0 $study-list-padding
position: absolute
z-index: 2
table.table
width: 100%;
margin-bottom: 20px;
border-spacing: 0;
border-collapse: collapse;
table-layout: fixed;
color: var(--table-text-primary-color)
font-weight: 300
> tbody tr
padding: 5px
background-color: black
> tbody td
padding: $body-cell-top-bottom-padding 8px;
height: $body-cell-height
word-wrap: break-word;
/* Striped Variant */
&.table--striped > tbody tr:nth-child(even)
background-color: var(--ui-gray-darker)
/* Hover Variant */
&.table--hoverable > tbody tr
&:hover, &:active, &.active
background-color: var(--table-hover-color)
&.no-hover
&:hover, &:active, &.active
background-color: var(--ui-gray-darker)
.empty-value
color: var(--ui-gray-light)
.study-list-container > table.table > tr
height: 20px
.study-list-container > table.table > thead
::-webkit-datetime-edit-year-field:not([aria-valuenow]),
::-webkit-datetime-edit-month-field:not([aria-valuenow]),
::-webkit-datetime-edit-day-field:not([aria-valuenow])
color: transparent
.study-list-container > table.table > thead > tr > th
padding: 0;
border-bottom: 1px solid var(--ui-border-color-active);
width: 100%;
text-align: left;
border-top: 0;
.study-list-container > table.table > thead > tr > th.studyDate
min-width: 230px
.study-list-container > table.table .modalities
font-weight: 500;
min-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.study-list-container
.filters
label
display: flex;
align-items: center;
cursor: pointer;
width: 100%;
min-width: 95px;
margin: 0 auto;
color: var(--table-text-primary-color);
font-weight: 400;
padding: 20px;
user-select: none;
font-size: 15px;
&:hover
color: var(--active-color);
&.active, &:active
color: var(--active-color);
i
margin: 0 5px;
input
height: 40px
margin: 0 5px 20px 5px
padding: 0 20px
cursor: pointer
border: none
background-color: var(--input-background-color)
color: var(--input-placeholder-color)
font-size: 10pt
font-weight: normal
border-radius: 4px
width: calc(100% - 10px); /* Just use padding? */
transition(all 0.15s ease)
placeholder-color(var(--input-placeholder-color))
&:active, &:hover
background-color: var(--input-background-color)
.loading-text
color: var(--table-text-secondary-color)
text-align: center;
font-size: 30px
.notFound
@@ -92,191 +217,22 @@ placeholder-color(c)
font-weight: 200
text-align: center
table#tblStudyList
width: 100%;
max-width: 100%;
margin-bottom: 20px;
border-spacing: 0;
border-collapse: collapse;
@media only screen and (max-width: 768px)
.study-list-header
padding: 0 16px;
> tr
height: 20px
.study-list-container
padding: 0;
> thead
white-space: nowrap
.study-list-container > table.table > thead > tr > th
padding: 0 13px;
tr
th
padding: 0
border-bottom: 1px solid var(--ui-border-color-active);
width: 100%;
text-align: left;
border-top: 0;
.study-list-container > table.table > tbody > tr > td
padding: 8px;
&.studyDate
min-width: 230px
.study-list-container .filters label
padding: 8px;
.display-text
display: inline-block
cursor: pointer
width: 100%
min-width: 95px
margin: 0 auto
color: var(--table-text-primary-color)
font-weight: 400
padding: 20px
-webkit-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
span
font-size: 15px
float: left
i
margin: 0 5px
&:hover
color: var(--active-color)
&.active, &:active
color: var(--active-color)
input.studylist-search
height: 40px
margin: 0 5px 20px 5px
padding: 0 20px
cursor: pointer
border: none
background-color: var(--input-background-color)
color: var(--input-placeholder-color)
font-size: 10pt
font-weight: normal
width: calc(100% - 10px)
border-radius: 4px
transition(all 0.15s ease)
placeholder-color(var(--input-placeholder-color))
&.invisible
visibility: hidden
&:active, &:hover
background-color: var(--input-background-color)
::-webkit-datetime-edit-year-field:not([aria-valuenow]),
::-webkit-datetime-edit-month-field:not([aria-valuenow]),
::-webkit-datetime-edit-day-field:not([aria-valuenow])
color: transparent
> tbody
tr
padding: 5px
background-color: black
&:nth-child(even)
background-color: var(--ui-gray-darker)
td
padding: 8px;
height: $body-cell-height
line-height: $body-cell-height
color: var(--table-text-primary-color)
font-weight: 300
border-top: 1px solid var(--ui-gray-lighter)
border-bottom: 1px solid var(--ui-gray-lighter)
white-space: nowrap
transition(all 0.1s ease)
&.emptyCell
color: #516873
&.emptyValue
color: var(--ui-gray-light)
&:hover, &:active, &.active
background-color: var(--table-hover-color)
color: var(--text-primary-color)
td
// This selector is necessary to override bootstrap's 'table' class
border-top: 1px solid var(--ui-gray-lighter)
border-bottom: 1px solid var(--ui-gray-lighter)
background-color: var(--table-hover-color)
@media only screen and (max-width: 1362px)
#studyListContainer
padding: 0 $tablePaddingLargeScreen
table#tblStudyList
thead,
tbody
tr
th,
td
&:first-child
padding-left: $tablePaddingLargeScreen
&:last-child
padding-right: $tablePaddingLargeScreen
@media only screen and (max-width: 1161px)
#studyListContainer
padding: 0 $tablePaddingBigWidth
table#tblStudyList
thead,
tbody
tr
th,
td
&:first-child
padding-left: $tablePaddingBigScreen
&:last-child
padding-right: $tablePaddingBigScreen
@media only screen and (max-width: 1069px)
.theadBackground
height: 101px
.studylist-pagination > .row
margin-right: 0;
.studyListToolbar
padding: 0 $tablePaddingMediumScreen
#studyListContainer
padding: 0
table#tblStudyList
thead > tr > th
&:first-child
padding-left: $tablePaddingMediumScreen
&:last-child
padding-right: $tablePaddingMediumScreen
input.worklist-search
padding: 10px
.display-text
padding: 10px 5px
i
width: auto
tbody > tr > td
&:first-child
padding-left: $tablePaddingMediumScreen
&:last-child
padding-right: $tablePaddingMediumScreen
.worklistPagination
.row
margin-left: 0
margin-right: 0
@media only screen and (max-width: 500px)
.hide-xs
display: none;
@@ -1,12 +1,18 @@
import { Icon } from './../../elements/Icon';
import React from 'react';
import { Icon } from './../../elements/Icon';
// TODO: useTranslation
import { withTranslation } from '../../utils/LanguageProvider';
function StudyListLoadingText() {
function StudyListLoadingText({ t: translate }) {
return (
<div className="loading-text">
Loading... <Icon name="circle-notch" animation="pulse" />
{translate('Loading')}... <Icon name="circle-notch" animation="pulse" />
</div>
);
}
export { StudyListLoadingText };
const connectedComponent = withTranslation('StudyListLoadingText')(
StudyListLoadingText
);
export { connectedComponent as StudyListLoadingText };
@@ -1,26 +0,0 @@
.studyListToolbar
.addNewStudy
margin: 0 10px
label
font-weight: 400
cursor: pointer
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
input
width: 0.1px;
height: 0.1px
opacity: 0
overflow: hidden
position: absolute
z-index: -1
color: var(--text-secondary-color)
&:hover
color: var(--hover-color)
&:active
color: var(--active-color)
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import './PaginationArea.styl';
import { withTranslation } from '../../utils/LanguageProvider';
class PaginationArea extends PureComponent {
class TablePagination extends PureComponent {
static defaultProps = {
pageOptions: [5, 10, 25, 50, 100],
rowsPerPage: 25,
@@ -11,7 +11,8 @@ class PaginationArea extends PureComponent {
};
static propTypes = {
pageOptions: PropTypes.array.isRequired,
/* Values to show in "rows per page" select dropdown */
pageOptions: PropTypes.array,
rowsPerPage: PropTypes.number.isRequired,
currentPage: PropTypes.number.isRequired,
nextPageFunc: PropTypes.func,
@@ -89,17 +90,11 @@ class PaginationArea extends PureComponent {
render() {
return (
<div name="paginationArea">
<div className="pagination-area">
<div className="row">
<div className="rows-dropdown">
{this.renderRowsPerPageDropdown()}
</div>
<div className="pagination-buttons">
<div className="form-inline form-group page-number pull-right">
{this.renderPaginationButtons()}
</div>
</div>
<div className="pagination-area">
<div className="rows-dropdown">{this.renderRowsPerPageDropdown()}</div>
<div className="pagination-buttons">
<div className="form-inline form-group page-number pull-right">
{this.renderPaginationButtons()}
</div>
</div>
</div>
@@ -107,5 +102,5 @@ class PaginationArea extends PureComponent {
}
}
const connectedComponent = withTranslation('Common')(PaginationArea);
export { connectedComponent as PaginationArea };
const connectedComponent = withTranslation('Common')(TablePagination);
export { connectedComponent as TablePagination };
Loaded 100 of 247 files, more files were not shown because too many files have changed in this diff. Show more