Compare commits

..
Author SHA1 Message Date
ohif-bot f1a4c09b74 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.54.3
 - @ohif/core@1.13.3
 - @ohif/viewer@2.11.5
2019-12-06 21:34:12 +00:00
Danny Brown 02572b4928 docs: extensions (#1258)
* docs: process docs to include UX Stories requirement

* docs: include note regarding different environments

* Services init

* Remove unused canny logos

* docs: add ModalService diagram

* docs: GIF of notification

* docs: add ui-services page

* docs: simplify ui services call out in the general services docs

* docs: tips and tricks for UI services

* docs: moar pages

* docs: dialog gif

* docs: services in summary/sidebar

* docs: gif examples at top of dialog and notification pages

* docs: add UIModal gif

* docs: details for Dialog Service

* docs: include usage information for ui modal service

* docs: detailed information about our UI Notification Service

* docs: services diagram

* docs: remove unused links

* docs: services example and image

* docs: stubbing extension pages

* docs: UX Stories --> User Cases

* chore: eslint fix

* docs: extensions overview + diagram

* docs: update extensions in sidebar

* docs: extension skeleton and registration info

* docs: copy of maintained extensions table partial in the extensions folder

* docs: swap out old extensions structure for new
2019-12-06 16:31:46 -05:00
dannyrb fd1d29820a docs: fix services linking 2019-12-06 09:25:57 -05:00
dannyrb 6c63a917d3 docs: explanation of default services and current state 2019-12-06 09:17:20 -05:00
dannyrb caa6062c41 docs: begin breaking services docs into bite sized pages 2019-12-06 09:12:06 -05:00
Danny Brown 7f9743a020 docs: fix anchor links (#1256)
docs: fix anchor links (#1256)
2019-12-06 00:13:40 -05:00
Danny Brown a2c4161477 docs: services (#1255)
* docs: process docs to include UX Stories requirement

* docs: include note regarding different environments

* Services init

* Remove unused canny logos

* docs: add ModalService diagram

* docs: GIF of notification

* docs: add ui-services page

* docs: simplify ui services call out in the general services docs

* docs: tips and tricks for UI services

* docs: moar pages

* docs: dialog gif

* docs: services in summary/sidebar

* docs: gif examples at top of dialog and notification pages

* docs: add UIModal gif

* docs: details for Dialog Service

* docs: include usage information for ui modal service

* docs: detailed information about our UI Notification Service

* docs: services diagram

* docs: remove unused links

* docs: services example and image
2019-12-05 15:47:46 -05:00
Danny Brown 42cfce60f5 docs: simplify PR checklist; add UX stories requirement 2019-12-04 16:25:52 -05:00
Danny Brown 244949bab6 docs: process updates labels deploy (#1245)
* docs: process docs to include UX Stories requirement

* docs: include note regarding different environments
2019-12-04 16:21:11 -05:00
ohif-bot e87ec7b7f5 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.7.2
 - @ohif/viewer@2.11.4
2019-12-02 19:04:21 +00:00
Danny Brown d98889f1eb refactor: viewports store (#1239)
* Organize viewports reducers

* Organize viewports actions

* Add local state to store dom node and remove hack

* Comment usage of dom in vtk

* Fix set of enabledElements

* Fix warning in html viewport

* Update docs for state

* Add commandsmanager to commandsmodule

* refactor: Shift ConnectedViewportDownloadForm to extension-cornerstone (#1224)

* refactor: 💡 Shift ConnectedViewportDownloadForm to its own proj

Shift ConnectedViewportDownloadForm to its own project and use local
state to get enabledElement

Closes: #1206

* Refactor connected component to cornerstone component

* Remove builtin behaviour from toolbarrow
2019-12-02 14:02:35 -05:00
ohif-bot 817d43ee62 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.7.1
 - @ohif/extension-dicom-html@1.0.2
 - @ohif/extension-vtk@0.54.2
 - @ohif/core@1.13.2
 - @ohif/viewer@2.11.3
2019-12-02 16:30:32 +00:00
Igor Octaviano f396b30166 refactor: Viewports state (#1218)
* Organize viewports reducers

* Organize viewports actions

* Add local state to store dom node and remove hack

* Comment usage of dom in vtk

* Fix set of enabledElements

* Fix warning in html viewport

* Update docs for state

* Add commandsmanager to commandsmodule
2019-12-02 11:29:04 -05:00
ohif-bot 0bb3eeb068 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.54.1
 - @ohif/core@1.13.1
 - @ohif/i18n@0.52.2
 - @ohif/ui@0.65.1
 - @ohif/viewer@2.11.2
2019-11-28 05:50:21 +00:00
Rodrigo Antinarelli 1df21a9e07 fix: User Preferences Issues (#1207)
* fix: study list translations

* Don't render until translations are ready

* Try to wait for translations to load

* Use null to render "nothing"

* Try toggling useSuspense off up a layer

* logging

* Remove useSuspense false flags

* DO NOT OPEN A PR WITH IT.
Experimental changes only.
Try to solve issues with useTranslation hooks
 #Please enter the commit message for your changes. Lines starting

* Remove unecessary changes

* feat: 🎸 useMedia hook to not use one more prop for upd state vl

* docs: Add license scan report and status (#1161)

Signed-off-by: fossabot <badges@fossa.io>

* fix: 🐛 Fix for JS breaking on header (#1164)

* feat: 🎸 Code review and refact

Revised code based on PRs(variable alias,...). Changed hook for
useMedia. Now, it provides two hooks: one to get displayMediaSize and
other to get Entity(component, objects...) based on displayMediaSize.
Implemented a similar solution for state manager to store
mediaQueryList(s) and displaySize for app.

* feat: 🎸 Fine tunning on mediaQuery value, fixed issue about it

* fix: 🐛 Fixed issue and refactoring

Fixed js exception and also refactoring userPreferences components to
functional component

* fix: 🐛 Code review. Localstorage fix minor bugs

* fix: 🐛 Ensure hotkey lower case always

* fix: translation switcher

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.53.6
 - @ohif/ui@0.62.1
 - @ohif/viewer@2.8.2

* Add new modal service

* Change serviceManager prop to servicesManager

* CR Update: fix casing and add required proptypes to providers

* CR Update: Improve ohifmodal proptypes

* CR Update: Fix typo in extensionmanager

* CR Update: add default props to service and check service in provider

* Refactor modal provider to better use its own state

* ci: don't build our master branch (#1177)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* Use modal instead of modal context

* Ci/promotable builds (#1179)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Change modal children order

* Ci/promotable builds (#1180)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* Ci/promotable builds (#1181)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* Ci/promotable builds (#1182)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Fix OHIFModal proptypes

* Ci/promotable builds (#1183)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* Ci/promotable builds (#1184)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* ci: workaround for sudo limitations

* ci: restore release workflow (#1185)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* ci: workaround for sudo limitations

* ci: restore release workflow

* Pass services to each module, improve tests

* Add servicesManager test and registerServices method

* Fix key warning of snackbar elements

* Remove netlify-cli; we';ll install this on CI server

* Update staging and prod netlify site IDs

* Clean up NPM_PUBLISH step

* Clean up DOCS_PUBLISH step

* Clean up Deploy workflow

* Custom executor to override cypress config

* Spacing

* Use an existing docker hub image

* Switch to npx instead of digging into npm bin location

* Remove e2e test before prod deploy

* Add workflow images

* docs: continous integrationn

* Add default props to modal

* chore(release): publish [skip ci]

 - @ohif/extension-cornerstone@1.5.1
 - @ohif/extension-vtk@0.53.7
 - @ohif/core@1.9.1
 - @ohif/ui@0.62.2
 - @ohif/viewer@2.8.3

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.53.8
 - @ohif/core@1.10.0
 - @ohif/ui@0.62.3
 - @ohif/viewer@2.8.4

* ci: Redirect site traffic to index.html if file is not resolved

* ci: fix typo

* fix: 🐛 Code review. Remove 'global state' for displaySize

* fix: 🐛 Code review. Ref back to useMedia and pass value down

Code review. Ref back to useMedia and pass value down (components)
instead of creating a specialized hook to tied any component on it

* fix: translation switcher

* fix

* cleanup

* add missed translation

* fix: 🐛 Code review

* fix: 🐛 Fix unit tests

* fix: 🐛 Fix unit test

* fix: 🐛 Code merge solve conflicts. Missing files changes

* Merge from master. Missing files changes

* feat: 🎸 Code review. Fix issues with DatePicker

Fixed at least the minimum issue with datePicker and update some content
on every translation changed

* fix: 🐛 Code review. Style fix on modal

* fixes in general preferences after merge

* fix

* export default i18n language

* translation fixes

* increase snackbar zindex

* initialize language

* WIP propagate props<>state fixes

* fix state propagation

* remove hotkey error when reset default

* fix datePicker

* i18n updates

* fix i18n strings

* fix reset default -- set right input value

* fix default language

* fix e2e for user preferences

* small fix datePicker props

* remove i18n from window -- debugging purposes only

* lint fixes
2019-11-28 00:48:27 -05:00
ohif-bot fd1a9e2abe chore(release): publish [skip ci]
- @ohif/viewer@2.11.1
2019-11-27 21:04:00 +00:00
Egor Lezhnin e34a057263 fix: of undefined name of project (#1231) 2019-11-27 16:02:12 -05:00
ohif-bot 27e02825a4 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.7.0
 - @ohif/extension-vtk@0.54.0
 - @ohif/core@1.13.0
 - @ohif/ui@0.65.0
 - @ohif/viewer@2.11.0
2019-11-25 22:16:35 +00:00
Igor Octaviano 8fd3af1e13 feat: Add new annotate tool using new dialog service (#1211)
* Testing dialog

* Refactor modal and add dialog component to simple dialog

* Use existent dialog

* Expect event from getText callback (annotation)

* Bump cornerstone-tools version

* Use simple dialog

* bump cornersotne version

* CR Update: Update dialgo method names and default values

* CR Update: Bump overlay and show only if theres at least one overlay prop set to true

* CR Update: Use percentage over view height in draggable area

* Fix broken test vtk checks WWWC tool

* Comment test (non-deterministic)
2019-11-25 17:13:42 -05:00
ohif-bot 1334ba8eb0 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.13
 - @ohif/ui@0.64.2
 - @ohif/viewer@2.10.2
2019-11-25 15:50:36 +00:00
Danny Brown a870b3cc60 fix: Issue branch from danny experimental changes pr 1128 (#1150)
* fix: study list translations

* Don't render until translations are ready

* Try to wait for translations to load

* Use null to render "nothing"

* Try toggling useSuspense off up a layer

* logging

* Remove useSuspense false flags

* DO NOT OPEN A PR WITH IT.
Experimental changes only.
Try to solve issues with useTranslation hooks
 #Please enter the commit message for your changes. Lines starting

* Remove unecessary changes

* feat: 🎸 useMedia hook to not use one more prop for upd state vl

* docs: Add license scan report and status (#1161)

Signed-off-by: fossabot <badges@fossa.io>

* fix: 🐛 Fix for JS breaking on header (#1164)

* feat: 🎸 Code review and refact

Revised code based on PRs(variable alias,...). Changed hook for
useMedia. Now, it provides two hooks: one to get displayMediaSize and
other to get Entity(component, objects...) based on displayMediaSize.
Implemented a similar solution for state manager to store
mediaQueryList(s) and displaySize for app.

* feat: 🎸 Fine tunning on mediaQuery value, fixed issue about it

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.53.6
 - @ohif/ui@0.62.1
 - @ohif/viewer@2.8.2

* Add new modal service

* Change serviceManager prop to servicesManager

* CR Update: fix casing and add required proptypes to providers

* CR Update: Improve ohifmodal proptypes

* CR Update: Fix typo in extensionmanager

* CR Update: add default props to service and check service in provider

* Refactor modal provider to better use its own state

* ci: don't build our master branch (#1177)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* Use modal instead of modal context

* Ci/promotable builds (#1179)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Change modal children order

* Ci/promotable builds (#1180)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* Ci/promotable builds (#1181)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* Ci/promotable builds (#1182)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Fix OHIFModal proptypes

* Ci/promotable builds (#1183)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* Ci/promotable builds (#1184)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* ci: workaround for sudo limitations

* ci: restore release workflow (#1185)

* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* ci: workaround for sudo limitations

* ci: restore release workflow

* Pass services to each module, improve tests

* Add servicesManager test and registerServices method

* Fix key warning of snackbar elements

* Remove netlify-cli; we';ll install this on CI server

* Update staging and prod netlify site IDs

* Clean up NPM_PUBLISH step

* Clean up DOCS_PUBLISH step

* Clean up Deploy workflow

* Custom executor to override cypress config

* Spacing

* Use an existing docker hub image

* Switch to npx instead of digging into npm bin location

* Remove e2e test before prod deploy

* Add workflow images

* docs: continous integrationn

* Add default props to modal

* chore(release): publish [skip ci]

 - @ohif/extension-cornerstone@1.5.1
 - @ohif/extension-vtk@0.53.7
 - @ohif/core@1.9.1
 - @ohif/ui@0.62.2
 - @ohif/viewer@2.8.3

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.53.8
 - @ohif/core@1.10.0
 - @ohif/ui@0.62.3
 - @ohif/viewer@2.8.4

* ci: Redirect site traffic to index.html if file is not resolved

* ci: fix typo

* fix: 🐛 Code review. Remove 'global state' for displaySize

* fix: 🐛 Code review. Ref back to useMedia and pass value down

Code review. Ref back to useMedia and pass value down (components)
instead of creating a specialized hook to tied any component on it

* Merge from master. Missing files changes

* feat: 🎸 Code review. Fix issues with DatePicker

Fixed at least the minimum issue with datePicker and update some content
on every translation changed
2019-11-25 10:45:37 -05:00
ohif-bot 52687a7113 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.12
 - @ohif/ui@0.64.1
 - @ohif/viewer@2.10.1
2019-11-20 19:17:36 +00:00
Mirna Silva 421de23ba3 test: Test Cases for User Preferences (#1195)
* Test Cases for User Preferences

* CI fix

* Fix for CI

* Fix based on PR review comments

* Fix for script-tag issue on CI

* Fix for new selectors on Preferences Modal and CI failure
2019-11-20 14:15:10 -05:00
ohif-bot adc8d9ea0f chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.6.0
 - @ohif/extension-vtk@0.53.11
 - @ohif/core@1.12.0
 - @ohif/ui@0.64.0
 - @ohif/viewer@2.10.0
2019-11-19 19:19:32 +00:00
Igor Octaviano f65639c2b0 feat: New dialog service (#1202)
* Add dialog provider boilerplate and improve provider imports

* Add react-draggable dependency

* Add dialog service boilerplate

* Refactor modal to use react-modal and remove dependency on modal themed styles

* Update cinedialog to use new dialog provider

* Add multiple dialogs and refactor provider (dialogs)

* Set app root (modal)

* Add modal zindex bigger than tooltip

* Block drag on buttons/svgs/paths/input and improve jsdoc

* Use guid util to generate dialog ids

* Explicit props in dialog provider

* Improve jsdocs

* Fix tests broken

* Fix boundaries calculation

* Remember last dialog position

* Update providers location

* Add scroll to modal

* Add toggable button toolbar and fix css modal
2019-11-19 14:17:33 -05:00
Danny Brown cdd75bda2a docs: copy-paste of Erik's notes on PRs and releases
At least until we can sneak some graphics, tables, and emojis in
2019-11-19 04:03:41 -05:00
Danny Brown d92fe44d3c chore: add triage flag label 2019-11-19 03:50:17 -05:00
Danny Brown d90c2ac024 chore: add triage flag label 2019-11-19 03:49:57 -05:00
Danny Brown 7ce5e1d2d3 chore: add triage flag 2019-11-19 03:49:36 -05:00
Danny Brown a3bf47f9bf chore: attempt known good syntax 2019-11-19 03:48:32 -05:00
Danny Brown 81f79a26ad chore: revert 2019-11-19 03:46:28 -05:00
Danny Brown 60b4b5d7cf chore: lets try double quotes 2019-11-19 03:45:52 -05:00
Danny Brown 69c54d4f49 chore: labels format 2019-11-19 03:44:26 -05:00
Danny Brown 0f8868a232 chore: add triage to default bug report label 2019-11-19 03:39:09 -05:00
Danny Brown 1bbfee4087 chore: use github label syntax for triage flag 2019-11-19 03:38:33 -05:00
ohif-bot 33c7b59611 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.10
 - @ohif/core@1.11.0
 - @ohif/ui@0.63.0
 - @ohif/viewer@2.9.0
2019-11-19 08:36:12 +00:00
ladeirarodolfo b17f753e62 feat: Issue 879 viewer route query param not filtering but promoting (#1141)
* 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: 🎸 Instead of filtering do promote. WIP

* feat: 🎸 Fix minor issue. Allow promote or filter

* feat: 🎸 Changing component to functional component

* Merge from master Part1/2
Conflicts solved:
	both added:      core/src/studies/services/wado/retrieveMetadataLoader.js
	both added:      core/src/studies/services/wado/retrieveMetadataLoaderAsync.js
	both added:      core/src/studies/services/wado/retrieveMetadataLoaderSync.js
	both modified:   viewer/src/connectedComponents/ViewerRetrieveStudyData.js
	both modified:   viewer/src/routes/ViewerRouting.js

* Merge process from master Part 1/2
Missing files from previous commit

* feat: 🎸 Add cancelable promises to cut async methods

* feat: 🎸 Missing changes from previous merge process

* feat: 🎸 Missing changes from previous merge process
2019-11-19 03:33:48 -05:00
Danny Brown c77a2ef5c9 docs: fix broken links 2019-11-19 03:24:49 -05:00
Danny Brown 348edd4717 docs: Issue curation 2019-11-19 03:23:30 -05:00
Danny Brown 407060658a docs: add "our process" to sidebar 2019-11-19 02:53:57 -05:00
Danny Brown fc00f9d505 docs: clarify purpose of page and index links 2019-11-19 02:52:24 -05:00
Danny Brown bc3d080672 docs: flush out issue triage 2019-11-19 02:46:04 -05:00
Danny Brown 09353354c3 docs: start "our process" docs 2019-11-19 02:08:46 -05:00
ohif-bot 0a12320083 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.9
 - @ohif/i18n@0.52.1
 - @ohif/ui@0.62.4
 - @ohif/viewer@2.8.5
2019-11-18 21:33:41 +00:00
Rodrigo Antinarelli 813ee5ed4d fix: minor date picker UX improvements
* improvement: Date Picker

* remove unused values

* fix: search results w/ valid date returning empty

* translations from locize

* styling select

* revert studyListDateFilterNumDays
2019-11-18 16:30:47 -05:00
Danny Brown 6085b5f733 docs: add ohif urls to docs
docs: add ohif urls to docs
2019-11-18 11:33:48 -05:00
dannyrb ffe12789dd docs: add ohif urls to docs 2019-11-18 11:32:13 -05:00
Danny Brown 0b68429dfb Merge pull request #1200 from dannyrb/ci/complete-promoted-deploy
ci: fix typo
2019-11-18 11:07:50 -05:00
dannyrb b747ea5144 ci: fix typo 2019-11-18 11:06:57 -05:00
Danny Brown caadf089df Merge pull request #1199 from dannyrb/ci/complete-promoted-deploy
ci: Redirect site traffic to index.html if file is not resolved
2019-11-18 11:00:11 -05:00
Danny Brown e0fc69c181 Merge branch 'master' into ci/complete-promoted-deploy 2019-11-18 11:00:03 -05:00
dannyrb 2b9765304f ci: Redirect site traffic to index.html if file is not resolved 2019-11-18 10:57:21 -05:00
ohif-bot 57730483e9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.8
 - @ohif/core@1.10.0
 - @ohif/ui@0.62.3
 - @ohif/viewer@2.8.4
2019-11-15 20:04:45 +00:00
Danny Brown 731b64e321 feat: new modal service
feat: new modal service
2019-11-15 15:03:20 -05:00
Danny Brown e97a09b0f4 Merge branch 'master' into feat/new-modal-service 2019-11-15 14:52:37 -05:00
Danny Brown f63d8a73d8 feat: Inject into Extension Modules / improve tests
[UI Services to Extensions] Inject into Extension Modules / improve tests
2019-11-15 14:47:40 -05:00
ohif-bot f10adf82f4 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.5.1
 - @ohif/extension-vtk@0.53.7
 - @ohif/core@1.9.1
 - @ohif/ui@0.62.2
 - @ohif/viewer@2.8.3
2019-11-15 18:44:17 +00:00
Danny Brown 6174109d5a feat: Add new modal service
[UI Services to Extensions]: Add new modal service
2019-11-15 13:42:20 -05:00
Danny Brown 244e822afc Merge branch 'master' into feat/new-modal-service 2019-11-15 13:31:16 -05:00
Igor cbdeeae597 Merge branch 'feat/new-modal-service' of github.com:OHIF/Viewers into feat/new-modal-service 2019-11-15 15:21:02 -03:00
Igor 7f6de72e8e Add default props to modal 2019-11-15 15:20:26 -03:00
Danny Brown 1f514e5f1b ci: remove e2e for deploy; add docs
ci: remove e2e for deploy; add docs
2019-11-15 12:03:30 -05:00
dannyrb f3fed511b9 docs: continous integrationn 2019-11-15 12:02:08 -05:00
dannyrb 31796d272c Merge branch 'ci/complete-promoted-deploy' of github.com:dannyrb/Viewers into ci/complete-promoted-deploy 2019-11-15 11:11:26 -05:00
dannyrb b41e6f93f3 Add workflow images 2019-11-15 11:11:14 -05:00
dannyrb 882a45d398 Remove e2e test before prod deploy 2019-11-15 11:08:52 -05:00
Danny Brown fb0cd9355f Merge pull request #1189 from dannyrb/ci/complete-promoted-deploy
Ci/complete promoted deploy
2019-11-15 10:33:01 -05:00
Danny Brown 2b12193020 Merge branch 'master' into ci/complete-promoted-deploy 2019-11-15 10:32:53 -05:00
dannyrb 9a626a2064 Switch to npx instead of digging into npm bin location 2019-11-15 10:31:16 -05:00
dannyrb 903777044a Use an existing docker hub image 2019-11-15 10:21:47 -05:00
Danny Brown 5e4725860d Merge pull request #1188 from dannyrb/ci/complete-promoted-deploy
Ci/complete promoted deploy
2019-11-15 10:08:43 -05:00
dannyrb 670470119f Spacing 2019-11-15 09:38:21 -05:00
dannyrb 0d4f90f3cd Custom executor to override cypress config 2019-11-15 09:34:57 -05:00
dannyrb 43dd87c2c1 Clean up Deploy workflow 2019-11-15 09:18:56 -05:00
dannyrb 98f8583fba Clean up DOCS_PUBLISH step 2019-11-15 09:11:47 -05:00
dannyrb 97a9a505ca Clean up NPM_PUBLISH step 2019-11-15 09:11:31 -05:00
dannyrb bb74f3a761 Update staging and prod netlify site IDs 2019-11-15 09:11:11 -05:00
dannyrb aa33397fe6 Remove netlify-cli; we';ll install this on CI server 2019-11-15 09:10:51 -05:00
Danny Brown 8ae731fe4c Merge branch 'master' into feat/new-modal-service 2019-11-15 08:43:40 -05:00
Igor add3416e11 Fix key warning of snackbar elements 2019-11-15 09:54:14 -03:00
Igor 136969bf18 Add servicesManager test and registerServices method 2019-11-15 09:27:42 -03:00
Igor e3e039ad47 Pass services to each module, improve tests 2019-11-14 19:29:40 -03:00
Danny Brown 0797eff0b7 ci: restore release workflow (#1185)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* ci: workaround for sudo limitations

* ci: restore release workflow
2019-11-14 16:32:14 -05:00
Danny Brown 55a5f04923 Ci/promotable builds (#1184)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* ci: workaround for sudo limitations
2019-11-14 16:20:31 -05:00
Danny Brown ed9e4d6751 Ci/promotable builds (#1183)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var
2019-11-14 16:08:25 -05:00
Igor b6119f8e29 Fix OHIFModal proptypes 2019-11-14 17:57:44 -03:00
Danny Brown 2e38f4b2e4 Ci/promotable builds (#1182)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command
2019-11-14 15:56:23 -05:00
Danny Brown e03abc45f5 Ci/promotable builds (#1181)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available
2019-11-14 15:48:09 -05:00
Danny Brown f03d64ce39 Ci/promotable builds (#1180)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command
2019-11-14 15:41:34 -05:00
Igor e895bfb1aa Change modal children order 2019-11-14 17:40:37 -03:00
Danny Brown 17358c3f4b Ci/promotable builds (#1179)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag
2019-11-14 15:27:34 -05:00
Igor ec7337e801 Use modal instead of modal context 2019-11-14 17:25:09 -03:00
Danny Brown 538ea2516d ci: don't build our master branch (#1177)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions
2019-11-14 15:01:55 -05:00
Igor fe687cd2f9 Refactor modal provider to better use its own state 2019-11-14 14:56:01 -03:00
Igor bdb9af5c4b CR Update: add default props to service and check service in provider 2019-11-14 12:43:25 -03:00
Igor db0f751a68 CR Update: Fix typo in extensionmanager 2019-11-14 11:50:37 -03:00
Igor 08fdb10f6d CR Update: Improve ohifmodal proptypes 2019-11-14 11:42:25 -03:00
Igor 95c18b4986 CR Update: fix casing and add required proptypes to providers 2019-11-14 11:16:35 -03:00
Igor e684558783 Change serviceManager prop to servicesManager 2019-11-14 10:32:24 -03:00
Igor 9104e7be50 Add new modal service 2019-11-14 10:19:33 -03:00
ohif-bot 81ad25b39f chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.6
 - @ohif/ui@0.62.1
 - @ohif/viewer@2.8.2
2019-11-14 03:53:10 +00:00
Igor Octaviano 54d2cde6da refactor: 💡 Improve hotkeyPreferences proptypes / add lowercase (#1165)
* refactor: 💡 Improve hotkeyPreferences proptypes / add lowercase

Improve hotkeyPreferences proptypes and lowercase event keys to guard
against different browsers casings

* Add isRequired to proptypes and empty hotkeys message
2019-11-13 22:42:49 -05:00
ohif-bot 9edc366ff7 chore(release): publish [skip ci]
- @ohif/viewer@2.8.1
2019-11-14 02:48:34 +00:00
Mirna Silva 79c10bb424 test: Visual testing for Cornerstone and VTK (#1169)
* Visual testing for Cornerstone and VTK

* Fix for CI

* Review fix: Removed cornerstone function and added snapshot description

* Added snapshot description

* Fix for CI
2019-11-13 21:36:19 -05:00
ohif-bot d8055e458f chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.5.0
 - @ohif/extension-vtk@0.53.5
 - @ohif/core@1.9.0
 - @ohif/ui@0.62.0
 - @ohif/viewer@2.8.0
2019-11-13 20:12:25 +00:00
Igor Octaviano 5c04e34c8f feat: expose UiNotifications service (#1172)
* playing around

* Change extension init and preRegistration signature

* Clear test code

* Update core index test

* Fix test

* Tricking the bug

* Renaming file back to trick some weird bug
2019-11-13 15:02:14 -05:00
Egor Lezhnin c59c5b3f14 feat: filter field for google api windows (#1170) 2019-11-13 09:38:49 -05:00
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
329 changed files with 14846 additions and 5733 deletions

No files matched your search

+1 -1
View File
@@ -62,7 +62,7 @@ echo "Done generating documentation output"
echo 'STARTING PUBLISH'
# WILL ALWAYS FAIL IF INITIATED FROM PR BRANCH
./node_modules/.bin/gh-pages \
npx gh-pages \
--silent \
--repo https://$GITHUB_TOKEN@github.com/OHIF/Viewers.git \
--message 'Autogenerated Message: [ci skip]' \
+228 -245
View File
@@ -14,52 +14,53 @@ version: 2.1
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@1.11.0
executors:
# Custom executor to override Cypress config
deploy-to-prod-executor:
docker:
- image: 'cypress/browsers:node10.16.0-chrome77'
environment:
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/
defaults: &defaults
docker:
- image: circleci/node:12.9.1
environment:
TERM: xterm # Enable colors in term
QUICK_BUILD: true
working_directory: ~/repo
jobs:
###
# Workflow: PR_CHECKS
###
UNIT_TESTS:
<<: *defaults
steps:
# Update yarn
- run: yarn -v
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: yarn run test:unit:ci
# PLATFORM/VIEWER
- run:
name: 'VIEWER: Combine report output'
@@ -73,7 +74,6 @@ jobs:
- codecov/upload:
file: '/home/circleci/repo/platform/viewer/coverage/reports'
flags: 'viewer'
# PLATFORM/CORE
- run:
name: 'CORE: Combine report output'
@@ -88,145 +88,9 @@ jobs:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
UNIT_TESTS_PERSIST_WORKSPACE:
<<: *defaults
steps:
# Update yarn
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: yarn run test:unit:ci
# PLATFORM/VIEWER
- run:
name: 'VIEWER: Combine report output'
command: |
viewerCov="/home/circleci/repo/platform/viewer/coverage"
touch "${viewerCov}/reports"
cat "${viewerCov}/clover.xml" >> "${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
cat "${viewerCov}/lcov.info" >>"${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/viewer/coverage/reports'
flags: 'viewer'
# PLATFORM/CORE
- run:
name: 'CORE: Combine report output'
command: |
coreCov="/home/circleci/repo/platform/core/coverage"
touch "${coreCov}/reports"
cat "${coreCov}/clover.xml" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
cat "${coreCov}/lcov.info" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
# Persist :+1:
- persist_to_workspace: &persist_to_workspace
root: ~/repo
paths: .
NPM_PUBLISH:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
- run:
name: Avoid hosts unknown for github
command:
mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
no\n" > ~/.ssh/config
- run: git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
- run: git config --global user.name "ohif-bot"
- run:
name: Authenticate with registry
command:
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run: npx lerna version
- run: npx lerna publish from-package
# Persist :+1:
- persist_to_workspace:
root: ~/repo
paths: .
DOCS_PUBLISH:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
- run:
name: Avoid hosts unknown for github
command:
mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
no\n" > ~/.ssh/config
- run: git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
- run: git config --global user.name "ohif-bot"
- run: yarn global add gitbook-cli
- run: yarn global add gh-pages
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
- run: ~/repo/.circleci/build-and-publish-docs.sh
DOCKER_MASTER_PUBLISH:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build and push Docker image
command: |
# This file will exist if a new version was published by
# our `semantic-release` command in the previous job
if [[ ! -e platform/viewer/success_version.txt ]]; then
exit 0
else
# Remove npm config
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat platform/viewer/success_version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our image, auth, and push
docker build --tag ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL --tag ohif/$IMAGE_NAME:latest .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL
docker push ohif/$IMAGE_NAME:latest
fi
###
# Workflow: PR_OPTIONAL_DOCKER_PUBLISH
###
DOCKER_PR_PUBLISH:
<<: *defaults
steps:
@@ -264,55 +128,190 @@ jobs:
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM
VIEWER_DEMO_BUILD:
###
# Workflow: DEPLOY
###
BUILD:
<<: *defaults
steps:
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# Build & Test
- run:
name: 'Build the OHIF Viewer'
command: yarn run build
no_output_timeout: 45m
# - run:
# name: 'Upload SourceMaps, Send Deploy Notification'
# command: |
# # export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
# # export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
# # export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
# curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
# Persist :+1:
- persist_to_workspace:
root: ~/repo
paths:
- platform/viewer/dist
- netlify.toml
- .netlify
DEPLOY_TO_DEV:
docker:
- image: circleci/node:12.9.1
environment:
TERM: xterm
NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77
working_directory: ~/repo
steps:
- attach_workspace:
at: ~/repo
# Build & Test
- run: cd .netlify && npm install
- run:
name: 'Build Demo Site'
command: yarn run build:demo
no_output_timeout: 45m
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
DEPLOY_TO_STAGING:
docker:
- image: circleci/node:12.9.1
environment:
TERM: xterm
NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969
working_directory: ~/repo
steps:
- attach_workspace:
at: ~/repo
- run: cd .netlify && npm install
- run:
name: 'Upload SourceMaps, Send Deploy Notification'
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
DEPLOY_TO_PRODUCTION:
docker:
- image: circleci/node:12.9.1
environment:
TERM: xterm
NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0
working_directory: ~/repo
steps:
- attach_workspace:
at: ~/repo
- run: cd .netlify && npm install
- run:
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
###
# Workflow: RELEASE
###
NPM_PUBLISH:
<<: *defaults
steps:
- run: yarn -v
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
# Use increasingly general patterns to restore cache
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache/yarn
key: yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Avoid hosts unknown for github
command: |
# export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
# export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
# export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
# Persist :+1:
rm -rf ~/.ssh
mkdir ~/.ssh/
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
git config --global user.name "ohif-bot"
- run:
name: Authenticate with NPM registry
command:
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run: npx lerna version
- run: npx lerna publish from-package
- persist_to_workspace:
root: ~/repo
paths: .
VIEWER_DEMO_PUBLISH:
working_directory: ~/repo
docker:
- image: google/cloud-sdk
DOCS_PUBLISH:
<<: *defaults
steps:
- checkout
- run:
name: Avoid hosts unknown for github
command: |
rm -rf ~/.ssh
mkdir ~/.ssh/
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
git config --global user.name "ohif-bot"
- run: yarn global add gitbook-cli gh-pages
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
- run: ~/repo/.circleci/build-and-publish-docs.sh
DOCKER_MASTER_PUBLISH:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Deploy latest version to viewer.ohif.org
name: Build and push Docker image
command: |
# https://circleci.com/docs/2.0/google-auth/#authorization
echo $GCLOUD_SERVICE_KEY | gcloud auth activate-service-account --key-file=-
gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}
# Remove all files in Storage Bucket; Push latest
gsutil -m rm gs://$GOOGLE_STORAGE_BUCKET/**
gsutil -m rsync -R platform/viewer/dist gs://$GOOGLE_STORAGE_BUCKET
# This file will exist if a new version was published by
# our command in the previous job. Created in npm postpublish hook
# in the `platform/viewer` project.
if [[ ! -e platform/viewer/success_version.txt ]]; then
exit 0
else
# Remove npm config
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat platform/viewer/success_version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our image, auth, and push
docker build --tag ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL --tag ohif/$IMAGE_NAME:latest .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL
docker push ohif/$IMAGE_NAME:latest
fi
workflows:
version: 2
# PULL REQUESTS
PR_CHECKS:
jobs:
- UNIT_TESTS:
@@ -379,82 +378,66 @@ workflows:
requires:
- AWAIT_APPROVAL
# MERGE TO MASTER
RELEASE:
###
# Our workflow for building, deploying, and promoting builds across our
# development, staging, and production environments.
###
DEPLOY:
jobs:
- UNIT_TESTS_PERSIST_WORKSPACE:
- BUILD:
filters:
branches:
only: master
# 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: 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:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_test_results:
path: cypress/results
- DEPLOY_TO_DEV:
requires:
- UNIT_TESTS_PERSIST_WORKSPACE
# E2E: script-tag
- cypress/run:
name: 'E2E: Script Tag'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest
yarn: true
record: false
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_test_results:
path: cypress/results
- BUILD
- PROMOTE_TO_STAGING:
type: approval
requires:
- UNIT_TESTS_PERSIST_WORKSPACE
# Update NPM
- DEPLOY_TO_DEV
- DEPLOY_TO_STAGING:
requires:
- PROMOTE_TO_STAGING
- PROMOTE_TO_PRODUCTION:
type: approval
requires:
- DEPLOY_TO_STAGING
- DEPLOY_TO_PRODUCTION:
requires:
- PROMOTE_TO_PRODUCTION
###
# Unit and E2E tests have already run for PR_CHECKS
# Re-running should not gain us any confidence here
###
RELEASE:
jobs:
- NPM_PUBLISH:
requires:
- 'E2E: Script Tag'
- 'E2E: PWA'
# Update docs.ohif.org
filters:
branches:
only: master
- DOCS_PUBLISH:
requires:
- 'E2E: Script Tag'
- 'E2E: PWA'
- VIEWER_DEMO_BUILD:
requires:
- 'E2E: Script Tag'
- 'E2E: PWA'
# Update viewer.ohif.org
- VIEWER_DEMO_PUBLISH:
requires:
- VIEWER_DEMO_BUILD
# Update hub.docker.org
filters:
branches:
only: master
- DOCKER_MASTER_PUBLISH:
requires:
- NPM_PUBLISH
#
#
# Potentially use to E2E test STAGING environment
# before publishing to production
# - cypress/run:
# name: 'E2E: PWA'
# executor: deploy-to-prod-executor
# browser: chrome
# working_directory: platform/viewer
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
# yarn: true
# record: false
# no-workspace: true
# store_artifacts: true
# post-steps:
# - store_artifacts:
# path: platform/viewer/cypress/screenshots
# - store_test_results:
# path: cypress/results
+1
View File
@@ -16,6 +16,7 @@
},
"globals": {
"cy": true,
"before": true,
"context": true,
"Cypress": true,
"assert": true
+1 -1
View File
@@ -2,7 +2,7 @@
name: "\U0001F41B Bug report"
about: Create a report to help us improve
title: ''
labels: 'Community: Report :bug:'
labels: 'Community: Report :bug:, Awaiting Reproduction, Triage :white_flag:'
assignees: ''
---
+1 -1
View File
@@ -2,7 +2,7 @@
name: "\U0001F680 Feature request"
about: Suggest an idea for this project
title: ''
labels: 'Community: Request :hand:'
labels: 'Community: Request :hand:, Triage :white_flag:'
assignees: ''
---
@@ -2,7 +2,7 @@
name: "\U0001F917 Support Question"
about: "I have a question \U0001F4AC"
title: ''
labels: 'Community: Question :question:'
labels: 'Community: Question :question:, Triage :white_flag:'
assignees: ''
---
@@ -1,23 +1,10 @@
### Request Checklist
### PR 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._
- [ ] Brief description of changes
- [ ] Links to any relevant issues
- [ ] Required status checks are passing
- [ ] User cases if changes impact the user's experience
- [ ] `@mention` a maintainer to request a review
<!--
Links
+1 -1
View File
@@ -15,7 +15,7 @@ exemptLabels:
- 'Task: Refactor 🛠'
- 'Task: Tests :microscope:'
- 'PR: Awaiting Review 👀'
- 'Triage 🏳️'
- 'Triage :white_flag:'
- 'Announcement 🎉'
# Label to use when marking an issue as stale
staleLabel: 'Stale :baguette_bread:'
+5
View File
@@ -0,0 +1,5 @@
# Specific to our non-deploy-preview deploys
# Confgure redirects using netlify.toml
# PWA Redirect
/* /index.html 200
+15
View File
@@ -0,0 +1,15 @@
{
"name": "root",
"private": true,
"engines": {
"node": ">=10",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"scripts": {
"deploy": "netlify deploy --prod --dir ./../platform/viewer/dist"
},
"devDependencies": {
"netlify-cli": "^2.21.0"
}
}
+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": {}
}
+4
View File
@@ -22,6 +22,7 @@
[![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]
@@ -324,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)
+20 -5
View File
@@ -1,5 +1,6 @@
# OHIF Viewers
- [Our Process](our-process.md)
- Essentials
- [Getting Started](essentials/getting-started.md)
- [Installation](essentials/installation.md)
@@ -20,11 +21,24 @@
- [Extensions](advanced/architecture.md#misc-extensions)
- [Diagram](advanced/architecture.md#diagram)
- [Common Questions](advanced/architecture.md#common-questions)
- [Extensions](advanced/extensions.md)
- [Overview](advanced/extensions.md#overview)
- [Modules](advanced/extensions.md#modules)
- [Registering](advanced/extensions.md#registering-extensions)
- [OHIF Maintained](advanced/extensions.md#ohif-maintained-extensions)
- [Extensions](extensions/index.md)
- [Registering](extensions/index.md#registering-an-extension)
- [Lifecycle Hooks](extensions/index.md#lifecycle-hooks)
- [preRegistration](extensions/lifecycle/pre-registration.md)
- [Modules](extensions/index.md#modules)
- [Commands](extensions/modules/commands.md)
- [Panel](extensions/modules/panel.md)
- [SOP Class Handler](extensions/modules/sop-class-handler.md)
- [Toolbar](extensions/modules/toolbar.md)
- [Viewport](extensions/modules/viewport.md)
- [Contexts](extension/index.md#contexts)
- [OHIF Maintained](extension/index.md#maintained-extensions)
- [Services](services/index.md)
- [Default](services/default/index.md)
- [UI](services/ui/index.md)
- [Dialog Service](services/ui/ui-dialog-service.md)
- [Modal Service](services/ui/ui-modal-service.md)
- [Notification Service](services/ui/ui-notification-service.md)
- [Custom Tools](advanced/custom-tools.md)
---
@@ -47,5 +61,6 @@
- [FAQ](frequently-asked-questions.md)
- [Contributing](contributing/index.md)
- [Continuous Integration](continous-integration.md)
- [Browser Support](browser-support.md)
- [Help](help.md)
+5 -5
View File
@@ -94,16 +94,16 @@ the viewer. Some common use cases include:
- Adding surgical templating tools and reports
- and many others
We expose common integration points via [extensions](./extensions.md) to make
this possible. The viewer and many of our own extensions also offer
We expose common integration points via [extensions](../extensions/index.md) to
make this possible. The viewer and many of our own extensions also offer
[configuration][configuration]. For a list of extensions maintained by OHIF,
[check out this helpful table](./extensions.html#ohif-maintained-extensions).
[check out this helpful table](../extensions/index.mdl#maintained-extensions).
If you find yourself thinking "I wish the Viewer could do X", and you can't
accomplish it with an extension today, create a GitHub issue! We're actively
looking for ways to improve our extensibility ^\_^
[Click here to read more about extensions!](./extensions.md)
[Click here to read more about extensions!](../extensions/index.md)
### Diagram
@@ -145,7 +145,7 @@ you would need to recreate.
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
[pwa]: https://developers.google.com/web/progressive-web-apps/
[configuration]: ../essentials/configuration.md
[extensions]: ./extensions.md
[extensions]: ../extensions/index.md
[core-github]: https://github.com/OHIF/viewers/platform/core
[ui-github]: https://github.com/OHIF/Viewers/platform/ui
<!-- prettier-ignore-end -->
+2 -1
View File
@@ -2,7 +2,8 @@
This is not yet exposed in an easy/convenient way. Most tools are currently
added by creating new Viewport, Toolbar, and SOPInstanceHandler extension
modules. You can read more about that approach in [extensions](./extensions.md).
modules. You can read more about that approach in
[extensions](../extensions/index.md).
In the near future, we intend to improve the extensibility of tools for existing
Viewports (like our Cornerstone.js and VTK.js viewports).
-263
View File
@@ -1,263 +0,0 @@
# Extensions
Extensions add new functionality to the viewer by registering one or more
modules. They go one step further than configuration in that they allow us to
inject custom React components, so long as they adhere to the module's
interface. This can be something as simple as adding a new button to the
toolbar, or as complex as a new viewport capable of rendering volumes in 3D.
- [Overview](#overview)
- [Modules](#modules)
- [Commands](#commands)
- [Hotkeys](#hotkeys)
- [Toolbar](#toolbar)
- [Panel](#panel)
- [Viewport](#viewport)
- [SOP Class Handler](#sopclasshandler)
## Overview
At a glance, an extension is a javascript object that has an `id` property, and
one or more "module" methods. You can find an abbreviated extension below, or
[view the source][example-ext-src] of our example extension.
```js
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'example-extension',
/**
* Registers one or more named commands scoped to a context. Commands are
* the primary means for...
*/
getCommandsModule() {
return {
defaultContext: 'VIEWER'
actions: { ... },
definitions: { ... }
}
},
/**
* Allows you to provide toolbar definitions that will be merged with any
* existing application toolbar configuration. Used to determine which
* buttons should be visible when, their order, what happens when they're
* clicked, etc.
*/
getToolbarModule() {
return {
definitions: [ ... ],
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE'
}
}
/**
* Not yet implemented
*/
getPanelModule: () => null,
/**
* Registers a ReactComponent that should be used to render data in a
* Viewport. The first registered viewport is our "default viewport". If
* more than one viewport is registered, we use `SopClassHandlers` to
* determine which viewport should be used.
*/
getViewportModule: () => reactViewportComponent,
/** Provides a whitelist of SOPClassUIDs the viewport is capable of rendering.
* Can modify default behavior for methods like `getDisplaySetFromSeries` */
getSopClassHandler: () => {
id: 'some-other-unique-id',
sopClassUids: [ ... ],
getDisplaySetFromSeries: (series, study, dicomWebClient, authorizationHeaders) => { ... }
},
}
```
### Modules
There are a few different module types. Each module type allows us to extend the
viewer in a different way, and provides a consistent API for us to do so. You
can find a full list of the different types of modules
[`in ohif-core`][module-types]. Information on each type of module, it's API,
and how we determine when/where it should be used is included below.
> NOTE: Modifying the extensions/modules registered to the OHIF Viewer currently
> requires us to import and pass extensions to the ExtensionManager in
> `src/App.js`, then rebuild the application. Long-term, we intend to make it
> possible to accomplish this without a build step.
#### Commands
The Commands Module allows us to register one or more commands scoped to
specific contexts. Commands can be run by [hotkeys][#], [toolbar buttons][#],
and any registered custom react component (like a [viewport][#] or [panel][#]).
Here is a simple example commands module:
```js
{
getCommandsModule() {
return {
actions: {
speak: ({ viewports, words }) => {
console.log(viewports, words);
},
},
definitions: {
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: 90 }
},
rotateViewportCCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: -90 },
context: 'ACTIVE_VIEWER::CORNERSTONE'
},
},
defaultContext: 'VIEWER'
}
}
}
```
#### Viewport
An extension can register a Viewport Module by providing a `getViewportModule()`
method that returns a React Component. The React component will receive the
following props:
```js
children: PropTypes.arrayOf(PropTypes.element)
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportData: PropTypes.object, // { studies, displaySet }
viewportIndex: PropTypes.number,
children: PropTypes.node,
customProps: PropTypes.object
```
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
component is used depends on:
- The Layout Configuration
- Registered SopClassHandlers
- The SopClassUID for visible/selected datasets
![Cornerstone Viewport](../assets/img/extensions-viewport.png)
<center><i>An example of three Viewports</i></center>
For a complete example implementation,
[check out the OHIFCornerstoneViewport](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/OHIFCornerstoneViewport.js).
#### Toolbar
An extension can register a Toolbar Module by providing a `getToolbarModule()`
method that returns a React Component. The component does not receive any props.
If you want to modify or react to state, you will need to connect to the redux
store. The given toolbar must determine its set of elements and the context of
them. The set of elements will be listed on toolbar `definitions`.
![Toolbar Extension](../assets/img/extensions-toolbar.gif)
<center><i>A toolbar extension example</i></center>
Toolbar components are rendered in the `ToolbarRow` component.
For a complete example implementation,
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
##### Toolbar Custom Component
Toolbar elements can define its own custom react component to be consumed when
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
you just need to expose your `CustomToolbarComponent` as the value of key
`CustomComponent`. In case the property `CustomComponent` is not present, a
default toolbar component will be used to render it. See bellow
```js
definitions: [
...
{
id: 'Custom',
label: 'Custom',
icon: 'custom-icon',
CustomComponent: CustomToolbarComponent,
}
...
]
```
`CustomComponent` components will receive the following props:
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
instance)
- toolbarClickCallback: callback method when clicking on toolbar
- button: its own definition object
- key: react key prop
- activeButtons: list of active elements
- isActive: if current
#### SopClassHandler
...
#### Panel
> The panel module is not yet in use.
#### Hotkeys
...
### Registering Extensions
Extensions are registered for the application at startup. The
`ExtensionManager`, exposed by `ohif-core`, registers a list of extensions with
our application's store. Each module provided by the extension becomes available
via `state.plugins.availablePlugins`, and consists of three parts: id, type
([PLUGIN_TYPE](https://github.com/OHIF/ohif-core/blob/43c08a29eff3fb646a0e83a03a236ddd84f4a6e8/src/plugins.js#L1-L6)),
and the return value of the module method.
In a future version, we will likely expose a way to provide the extensions you
would like included at startup.
_app.js_
```js
import { createStore, combineReducers } from 'redux';
import OHIF from '@ohif/core';
import OHIFCornerstoneExtension from 'ohif-cornerstone-extension';
const combined = combineReducers(OHIF.redux.reducers);
const store = createStore(combined);
const extensions = [new OHIFCornerstoneExtension()];
// Dispatches the `addPlugin` action to the store
// Adding extension modules to `state.plugins.availablePlugins`
ExtensionManager.registerExtensions(store, extensions);
```
## OHIF Maintained Extensions
A small number of powerful extensions for popular use cases are maintained by
OHIF. They're co-located in the
[`OHIF/Viewers`](https://github.com/OHIF/Viewers) repository, in the top level
[`extensions/`](https://github.com/OHIF/Viewers/tree/master/extensions)
directory.
{% include "./_maintained-extensions-table.md" %}
<!--
Links
-->
<!-- prettier-ignore-start -->
[example-ext-src]: https://github.com/OHIF/Viewers/tree/master/extensions/_example/src
[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js
<!-- prettier-ignore-end -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 260.4 82.9" enable-background="new 0 0 260.4 82.9" xml:space="preserve">
<g>
<path fill="#525DF9" d="M31.7,8.9c9.6,0,16.5,4.9,19,13.4l0.1,0.4h9.6l-0.1-0.6C57.6,8.5,46.7,0,31.8,0C13.7,0,0,14.1,0,32.9 s13.7,32.9,31.8,32.9c14.9,0,25.8-8.5,28.6-22.1l0.1-0.6h-9.6l-0.1,0.4c-2.5,8.5-9.5,13.4-19,13.4c-13.1,0-21.9-9.6-21.9-24 C9.8,18.6,18.6,8.9,31.7,8.9z"/>
<path fill="#525DF9" d="M110.1,58c-2.3,0-3.5-1.4-3.5-4.2V32.7c0-8.8-7.2-14.7-17.9-14.7c-7.5,0-16.7,3.9-18.3,14.9l-0.1,0.6h8.4 l0.1-0.4c1.4-5.7,5.8-6.9,9.3-6.9c6,0,9.5,2.7,9.5,7.4v2.6l-13.6,1.4c-7.6,0.8-15.7,5-15.7,14.6c0,8.1,6,13.5,14.8,13.5 c6.5,0,11.8-3.4,15-6.8c1.3,4.2,4.5,6.5,9.1,6.5c1.7,0,3.2-0.3,5.2-0.9l0.3-0.1v-7l-0.6,0.2C111.5,57.9,110.9,58,110.1,58z M97.7,43.5v7.3c-4.6,4.6-8.9,6.7-13.5,6.7c-2,0-6.8-0.6-6.8-5.7c0-3.8,2.8-6.3,7.4-6.9L97.7,43.5z"/>
<path fill="#525DF9" d="M146.5,18c-6.5,0-12,3.1-15.2,6.1v-5.2h-8.9v46h8.9V34.3c2.2-3.2,6.7-8.3,13-8.3c5.6,0,8.7,3.1,8.7,8.8 v30.1h8.9V33.3C161.9,23.7,156.1,18,146.5,18z"/>
<path fill="#525DF9" d="M195.8,18c-6.5,0-12,3.1-15.2,6.1v-5.2h-8.9v46h8.9V34.3c2.2-3.2,6.7-8.3,13-8.3c5.6,0,8.7,3.1,8.7,8.8 v30.1h8.9V33.3C211.2,23.7,205.4,18,195.8,18z"/>
<polygon fill="#525DF9" points="251.3,18.9 238.6,51.8 237.8,47.9 225.6,18.9 216.2,18.9 234.3,62.4 226,82.9 235,82.9 260.4,18.9 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 445.9 638" enable-background="new 0 0 445.9 638" xml:space="preserve">
<g>
<g>
<path fill="#5F5DF9" d="M224.5,638C101.1,638,0,537.8,0,414.7V223.3C0,100.2,101.1,0,224.5,0c109.6,0,202.8,78.3,220.9,186.1
c2.9,17.4-8.6,33.8-26,36.8c-17.4,2.9-33.7-8.8-36.7-26.2c-13-77-78.6-132.9-157-132.9c-88.2,0-159.3,71.6-159.3,159.5v191.4
c0,88,71.1,159.5,159.3,159.5c78.3,0,144.2-55.9,157.1-132.9c2.9-17.4,19.3-29.1,36.6-26.2c17.4,2.9,28.6,19.4,25.7,36.7
C427.2,559.7,334,638,224.5,638z"/>
</g>
<g opacity="0.5">
<path fill="#5F5DF9" d="M153.6,347.7c-17.6,0-30.7-14.3-30.7-31.9v-92.3c0-56.8,45.8-103,102.8-103c38.9,0,73.9,21.6,91.7,56.4
c8,15.7,1.7,34.9-14,42.9c-15.7,8-35,1.8-43-13.9c-6.8-13.4-21-21.7-35.8-21.7c-21.8,0-40.2,17.6-40.2,39.2v92.3
C184.3,333.4,171.2,347.7,153.6,347.7z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+109
View File
@@ -0,0 +1,109 @@
# Continous Integration (CI)
This repository uses `CircleCI` and `Netlify` for continous integration.
## Deploy Previews
[Netlify Deploy previews][deploy-previews] are generated for every pull request.
They allow pull request authors and reviewers to "Preview" the OHIF Viewer as if
the changes had been merged.
Deploy previews can be configured by modifying the `netlify.toml` file in the
root of the repository. Some additional scripts/assets for netlify are included
in the root `.netlify` directory.
## Workflows
[CircleCI Workflows][circleci-workflows] are a set of rules for defining a
collection of jobs and their run order. They are self-documenting and their
configuration can be found in our CircleCI configuration file:
`.circleci/config.yml`.
### Workflow: PR_CHECKS
The PR_CHECKS workflow (Pull Request Checks) runs our automated unit and
end-to-end tests for every code check-in. These tests must all pass before code
can be merged to our `master` branch.
<div style="text-align: center;">
<a href="/assets/img/WORKFLOW_PR_CHECKS.png">
<img src="/assets/img/WORKFLOW_PR_CHECKS.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Workflow diagram for PR_CHECKS</i></div>
</div>
### Workflow: PR_OPTIONAL_DOCKER_PUBLISH
The PR_OPTIONAL_DOCKER_PUBLISH workflow allows for "manual approval" to publish
the pull request as a tagged docker image. This is helpful when changes need to
be tested with the Google Adapter before merging to `master`.
<div style="text-align: center;">
<a href="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png">
<img src="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Workflow diagram for PR_WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH</i></div>
</div>
> NOTE: This workflow will fail unless it's for a branch on our `upstream`
> repository. If you need this functionality, but the branch is from a fork,
> merge the changes to a short-lived `feature/` branch on `upstream`
### Workflow: DEPLOY
The DEPLOY workflow deploys the OHIF Viewer when changes are merged to master.
It uses the Netlify CLI to deploy assets created as part of the repository's PWA
Build process (`yarn run build`). The workflow allows for "Manual Approval" to
promote the build to `STAGING` and `PRODUCTION` environments.
<div style="text-align: center;">
<a href="/assets/img/WORKFLOW_DEPLOY.png">
<img src="/assets/img/WORKFLOW_DEPLOY.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Workflow diagram for WORKFLOW_DEPLOY</i></div>
</div>
| Environment | Description | URL |
| ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- |
| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] / [OHIF][ohif-dev] |
| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-stage] / [OHIF][ohif-stage] |
| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] / [OHIF][ohif-prod] |
### Workflow: RELEASE
The RELEASE workflow publishes our `npm` packages, updated documentation, and
`docker` image when changes are merged to master. `Lerna` and "Semantic Commit
Syntax" are used to independently version and publish the many packages in our
monorepository. If a new version is cut/released, a Docker image is created.
Documentation is generated with `gitbook` and pushed to our `gh-pages` branch.
GitHub hosts the `gh-pages` branch with GitHub Pages.
- Platform Packages: https://github.com/ohif/viewers/#platform
- Extension Packages: https://github.com/ohif/viewers/#extensions
- Documentation: https://docs.ohif.org/
<div style="text-align: center;">
<a href="/assets/img/WORKFLOW_RELEASE.png">
<img src="/assets/img/WORKFLOW_RELEASE.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Workflow diagram for WORKFLOW_RELEASE</i></div>
</div>
### HOTFIX
_Not yet implemented_
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[deploy-previews]: https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/
[circleci-workflows]: https://circleci.com/docs/2.0/workflows/
[netlify-dev]: https://ohif-dev.netlify.com
[netlify-stage]: https://ohif-stage.netlify.com
[netlify-prod]: https://ohif-prod.netlify.com
[ohif-dev]: https://viewer-dev.ohif.org
[ohif-stage]: https://viewer-stage.ohif.org
[ohif-prod]: https://viewer-prod.ohif.org
<!-- prettier-ignore-end -->
+2 -2
View File
@@ -7,8 +7,8 @@ like to discuss the changes you intend to make to clarify where or how they
should be implemented, please don't hesitate to create a new issue. At a
minimum, you may want to read the following documentation:
- [Essentials: Getting Started](./essentials/getting-started.md)
- [Advanced: Architecture](./advanced/architecture.md)
- [Essentials: Getting Started](/essentials/getting-started.md)
- [Advanced: Architecture](/advanced/architecture.md)
Pull requests that are:
+1 -2
View File
@@ -35,8 +35,7 @@ benefits, but comes at the cost of time and complexity. Some benefits include:
_Today:_
- Leverage [extensions](/advanced/extensions.md) to drop-in powerful new
features
- Leverage [extensions](/extensions/index.md) to drop-in powerful new features
- Add routes and customize the viewer's workflow
- Finer control over styling and whitelabeling
@@ -98,7 +98,7 @@ extension enabled here][whole-slide-ext-demo] ([source code][ext-code-sandbox])
and [without it here][whole-slide-base-demo] ([source code][code-sandbox]).
You can read more about extensions and how to create your own in our
[extensions guide](/advanced/extensions.md).
[extensions guide](/extensions/index.md).
#### FAQ
@@ -0,0 +1,62 @@
<table>
<thead>
<tr>
<th>Extension</th>
<th>Description</th>
<th>Modules</th>
</tr>
</thead>
<tbody>
<!-- CORNERSTONE.js -->
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-cornerstone">
Cornerstone
</a>
</td>
<td>
A viewport powered by <code>cornerstone.js</code>. Adds support for 2D DICOM rendering and manipulation, as well as support for the tools features in <a href="https://tools.cornerstonejs.org/examples/"><code>cornerstone-tools</code></a>. Also adds "CINE Dialog" to the Toolbar.
</td>
<td>Viewport, Toolbar</td>
</tr>
<!-- VTK.js -->
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-vtk">
VTK.js
</a>
</td>
<td>
A viewport powered by <code>vtk.js</code>. Adds support for volume renderings and advanced features like MPR. Also adds "3D Rotate" to the Toolbar.
</td>
<td>Viewport, Toolbar</td>
</tr>
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-html">DICOM HTML</a>
</td>
<td>
Renders text and HTML content for <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12">specific SopClassUIDs</a>.
</td>
<td>Viewport, SopClassHandler</td>
</tr>
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a>
</td>
<td>
Renders PDFs for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6">specific SopClassUID</a>.
</td>
<td>Viewport, SopClassHandler</td>
</tr>
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-microscopy">DICOM Microscopy</a>
</td>
<td>
Renders Microscopy images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7">specific SopClassUID</a>.
</td>
<td>Viewport, SopClassHandler</td>
</tr>
</tbody>
</table>
+151
View File
@@ -0,0 +1,151 @@
# Extensions
- [Overview](#overview)
- [Concepts](#concepts)
- [Extension Skeleton](#extension-skeleton)
- [Registering an Extension](#registering-an-extension)
- [Lifecylce Hooks](#lifecycle-hooks)
- [Modules](#modules)
- [Contexts](#contexts)
- [Consuming Extensions](#consuming-extensions)
- [Maintained Extensions](#maintained-extensions)
## Overview
We use extensions to help us isolate and package groups of related features.
Extensions provide functionality, ui components, and new behaviors.
<div style="text-align: center;">
<a href="/assets/img/extensions-diagram.png">
<img src="/assets/img/extensions-diagram.png" alt="Extensions Diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Diagram showing how extensions are configured and accessed.</i></div>
</div>
The `@ohif/viewer`'s application level configuration gives us the ability to add
and configure extensions. When the application starts, extensions are registered
with the `ExtensionManager`. Different portions of the `@ohif/viewer` project
will use registered extensions to influence application behavior.
Extensions allow us to:
- Wrap and integrate functionality of 3rd party dependencies in a reusable way
- Change how application data is mapped and transformed
- Display a consistent/cohesive UI
- Inject custom components to override built-in components
Practical examples of extensions include:
- A set of segmentation tools that build on top of the `cornerstone` viewport
- Showing ML/AI report summaries for the selected study/series/image
- Support for parsing DICOM structured reports and displaying them in a user
friendly way
- [See our maintained extensions for more examples of what's possible](#maintained-extensions)
## Concepts
### Extension Skeleton
An extension is a plain JavaScript object has an `id` property, and one or more
"getModuleFunctions" and/or lifecycle hooks. You can read more about
[lifecycle hooks](#lifecycle-hooks) and [modules](#modules) further down.
```js
// prettier-ignore
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'example-extension',
// Lifecyle
preRegistration() { /* */ },
// Modules
getCommandsModule() { /* */ },
getToolbarModule() { /* */ },
getPanelModule() { /* */ },
getSopClassHandler() { /* */ },
getViewportModule() { /* */ },
}
```
### Registering an Extension
There are two different ways to register and configure extensions. You can
leverage one or both strategies. Which one(s) you choose depend on your
application's requirements. Each [module](#modules) defined by the extension
becomes available to the core application via the `ExtensionManager`.
```js
// prettier-ignore
const config = {
extensions: [
MyFirstExtension,
[
MySecondExtension,
{ /* MySecondExtensions Configuration */ },
],
];
}
```
#### Runtime Extensions
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
that file includes an `Extensions` key that supports an array of extensions to
register.
#### Bundled Extensions
The `@ohif/viewer` works best when built as a "Progressive Web Application"
(PWA). If you know the extensions your application will need, you can specify
them at "build time" to leverage some advantaged afforded to us by modern
tooling:
- Code Splitting
- Tree Shaking
- Dependency deduplication
You can update the list of bundled extensions by:
1. Having your `@ohif/viewer` project depend on the extension
2. Importing and adding it to the list of extensions in the
`<repo-root>/platform/src/index.js` entrypoint.
### Lifecycle Hooks
...
### Modules
There are a few different module types. Each module type allows us to extend the
viewer in a different way, and provides a consistent API for us to do so. You
can find a full list of the different types of modules
[`in ohif-core`][module-types]. Information on each type of module, it's API,
and how we determine when/where it should be used is included below.
### Contexts
...
## Consuming Extensions
...
## Maintained Extensions
A small number of powerful extensions for popular use cases are maintained by
OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in
the top level [`extensions/`][ext-source] directory.
{% include "./_maintained-extensions-table.md" %}
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[viewers-repo]: https://github.com/OHIF/Viewers
[ext-source]: https://github.com/OHIF/Viewers/tree/master/extensions
[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js
<!-- prettier-ignore-end -->
@@ -0,0 +1,21 @@
# Lifecylce Hook: preRegistration
If an extension defines the `preRegistration` lifecycle hook, it is called
before any modules are registered to the `ExtensionManager`.
```js
export default {
id: 'MyExampleExtension',
preRegistration({ servicesManager, commandsManager, configuration }) {
console.log('Wiring up important stuff.');
window.importantStuff = () => {
console.log(configuration);
};
console.log('Important stuff has been wired.');
window.importantStuff();
},
};
```
@@ -0,0 +1,34 @@
# Module: Commands
The Commands Module allows us to register one or more commands scoped to
specific contexts. Commands can be run by [hotkeys][#], [toolbar buttons][#],
and any registered custom react component (like a [viewport][#] or [panel][#]).
Here is a simple example commands module:
```js
{
getCommandsModule() {
return {
actions: {
speak: ({ viewports, words }) => {
console.log(viewports, words);
},
},
definitions: {
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: 90 }
},
rotateViewportCCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: -90 },
context: 'ACTIVE_VIEWER::CORNERSTONE'
},
},
defaultContext: 'VIEWER'
}
}
}
```
+3
View File
@@ -0,0 +1,3 @@
# Module: Panel
...
@@ -0,0 +1,3 @@
# Module: SOP Class Handler
...
+48
View File
@@ -0,0 +1,48 @@
# Module: Toolbar
An extension can register a Toolbar Module by providing a `getToolbarModule()`
method that returns a React Component. The component does not receive any props.
If you want to modify or react to state, you will need to connect to the redux
store. The given toolbar must determine its set of elements and the context of
them. The set of elements will be listed on toolbar `definitions`.
![Toolbar Extension](../assets/img/extensions-toolbar.gif)
<center><i>A toolbar extension example</i></center>
Toolbar components are rendered in the `ToolbarRow` component.
For a complete example implementation,
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
## Toolbar Custom Component
Toolbar elements can define its own custom react component to be consumed when
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
you just need to expose your `CustomToolbarComponent` as the value of key
`CustomComponent`. In case the property `CustomComponent` is not present, a
default toolbar component will be used to render it. See bellow
```js
definitions: [
...
{
id: 'Custom',
label: 'Custom',
icon: 'custom-icon',
CustomComponent: CustomToolbarComponent,
}
...
]
```
`CustomComponent` components will receive the following props:
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
instance)
- toolbarClickCallback: callback method when clicking on toolbar
- button: its own definition object
- key: react key prop
- activeButtons: list of active elements
- isActive: if current
@@ -0,0 +1,29 @@
# Module: Viewport
An extension can register a Viewport Module by providing a `getViewportModule()`
method that returns a React Component. The React component will receive the
following props:
```js
children: PropTypes.arrayOf(PropTypes.element)
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportData: PropTypes.object, // { studies, displaySet }
viewportIndex: PropTypes.number,
children: PropTypes.node,
customProps: PropTypes.object
```
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
component is used depends on:
- The Layout Configuration
- Registered SopClassHandlers
- The SopClassUID for visible/selected datasets
![Cornerstone Viewport](../../assets/img/extensions-viewport.png)
<center><i>An example of three Viewports</i></center>
For a complete example implementation,
[check out the OHIFCornerstoneViewport](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/OHIFCornerstoneViewport.js).
+3
View File
@@ -0,0 +1,3 @@
# Hotkeys
...
+160
View File
@@ -0,0 +1,160 @@
# Our Process
Our process is a living, breathing thing. We strive to have regular
[retrospectives][retrospective] that help us shape and adapt our process to our
team's current needs. This document attempts to capture the broad strokes of
that process in an effort to:
- Strengthen community member involvement and understanding
- Welcome feedback and helpful suggestions
## Overview
- [Issue Triage](#issue-triage)
- [Issue Curation ("backlog grooming")](#issue-curation-backlog-grooming)
- [Contributions (Pull Requests)](#contributions-pull-requests)
- [Releases](#releases)
_Include issue lifecycle diagram_
## Issue Triage
[GitHub issues][gh-issues] are the best way to provide feedback, ask questions,
and suggest changes to the OHIF Viewer's core team. Community issues generally
fall into one of three categories, and are marked with a `triage` label when
created.
| Issue Template Name | Description |
| ---------------------- | ---------------------------------------------------------------------------------------- |
| Community: Report 🐛 | Describe a new issue; Provide steps to reproduce; Expected versus actual result? |
| Community: Request ✋ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? |
| Community: Question ❓ | Seek clarification or assistance relevant to the repository. |
_table 1. issue template names and descriptions_
Issues that require `triage` are akin to support tickets. As this is often our
first contact with would-be adopters and contributors, it's important that we
strive for timely responses and satisfactory resolutions. We attempt to
accomplish this by:
1. Responding to issues requiring `triage` at least once a week
2. Create new "official issues" from "community issues"
3. Provide clear guidance and next steps (when applicable)
4. Regularly clean up old (stale) issues
> :pencil: Less obviously, patterns in the issues being reported can highlight
> areas that need improvement. For example, users often have difficulty
> navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce
> our ticket volume for this issue?
### Backlogged Issues
Community issues serve as vehicles of discussion that lead us to "backlogged
issues". Backlogged issues are the distilled and actionable information
extracted from community issues. They contain the scope and requirements
necessary for hand-off to a core-team (or community) contributor ^\_^
| Category | Description | Labels |
| -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified 🐛][label-bug] |
| Stories | A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story 🙌][label-story] |
| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling 🤖][label-tooling], [Task: Docs 📖][label-docs], [Task: Refactor 🛠][label-refactor], [Task: Tests 🔬][label-tests] |
_table 2. backlogged issue types ([full list of labels][gh-labels])_
## Issue Curation (["backlog grooming"][groom-backlog])
If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on
our backlog. If an issue is on our backlog, it means we are, at the very least,
committed to reviewing any community drafted Pull Requests to complete the
issue. If you're interested in seeing an issue completed but don't know where to
start, please don't hesitate to leave a comment!
While we don't yet have a long-term or quarterly road map, we do regularly add
items to our ["Active Development" GitHub Project Board][gh-board]. Items on
this project board are either in active development by Core Team members, or
queued up for development as in-progress items are completed.
> :pencil: Want to contribute but not sure where to start? Check out [Up for
> grabs][label-grabs] issues and our [Contributing
> documentation][contributing-docs]
## Contributions (Pull Requests)
Incoming Pull Requests (PRs) are triaged using the following labels. Code review
is performed on all PRs where the bug fix or added functionality is deemed
appropriate:
| Labels | Description |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Classification** | |
| [PR: Bug Fix][label-bug] | Filed to address a Bug. |
| [PR: Draft][draft] | Filed to gather early feedback from the core team, but which is not intended for merging in the short term. |
| **Review Workflow** | |
| [PR: Awaiting Response 💬][awaiting-response] | The core team is waiting for additional information from the author. |
| [PR: Awaiting Review 👀][awaiting-review] | The core team has not yet performed a code review. |
| [PR: Awaiting Revisions 🖊][awaiting-revisions] | Following code review, this label is applied until the author has made sufficient changes. |
| **QA** | |
| [PR: Awaiting User Cases 💃][awaiting-stories] | The PR code changes need common language descriptions of impact to end users before the review can start |
| [PR: No UX Impact 🙃][no-ux-impact] | The PR code changes do not impact the user's experience |
We rely on GitHub Checks and integrations with third party services to evaluate
changes in code quality and test coverage. Tests must pass and User cases must
be present (when applicable) before a PR can be merged to master, and code
quality and test coverage must not changed by a significant margin. For some
repositories, visual screenshot-based tests are also included, and video
recordings of end-to-end tests are stored for later review.
[You can read more about our continous integration efforts here](/continous-integration.md)
## Releases
Releases are made automatically based on the type of commits which have been
merged (major.minor.patch). Releases are automatically pushed to NPM. Release
notes are automatically generated. Users can subscribe to GitHub and NPM
releases.
We host development, staging, and production environments for the Progressive
Web Application version of the OHIF Viewer. [Development][ohif-dev] always
reflects the latest changes on our master branch. [Staging][ohif-stage] is used
to regression test a release before a bi-weekly deploy to our [Production
environment][ohif-prod].
Important announcements are made on GitHub, tagged as Announcement, and pinned
so that they remain at the top of the Issue page.
The Core team occasionally performs full manual testing to begin the process of
releasing a Stable version. Once testing is complete, the known issues are
addressed and a Stable version is released.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[groom-backlog]: https://www.agilealliance.org/glossary/backlog-grooming
[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective
[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose
[gh-labels]: https://github.com/OHIF/Viewers/labels
<!-- Issue Labels -->
[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A
[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A
[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A
[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A
[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A
[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A
<!-- PR Labels -->
[draft]: https://github.com/OHIF/Viewers/labels/PR%3A%20Draft
[awaiting-response]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Response%20%3Aspeech_balloon%3A
[awaiting-review]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Review%20%3Aeyes%3A
[awaiting-stories]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20UX%20Stories%20%3Adancer%3A
[awaiting-revisions]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Revisions%20%3Apen%3A
[no-ux-impact]: https://github.com/OHIF/Viewers/labels/PR%3A%20No%20UX%20Impact%20%3Aupside_down_face%3A
<!-- -->
[ohif-dev]: https://viewer-dev.ohif.org
[ohif-stage]: https://viewer-stage.ohif.org
[ohif-prod]: https://viewer.ohif.org
[gh-board]: https://github.com/OHIF/Viewers/projects/4
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
[contributing-docs]: https://docs.ohif.org/contributing/
<!-- prettier-ignore-end -->
+65
View File
@@ -0,0 +1,65 @@
# Services (default)
- [Overview](#overview)
- [Example](#example)
## Overview
Services are a work in progress. As we are still in the progress of creating a
non-ui maintained service, this usage may change.
<div style="text-align: center;">
<a href="/assets/img/services.png">
<img src="/assets/img/services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
</div>
## Example
The simplest service return a new object that has a `name` property, and
methods/properties that give the service its functionality. The "Factory
Function" that creates the service is provided with the implementation (this is
slightly different for UI Services).
```js
const _speak = () => {
console.warn('Speak is not implemented');
};
/**
* Factory function to create `HelloWorldService`
*
* @param {object} implementation
* @param {function} implementation.speak - Speak's implementation
* @returns HelloWorldService
*/
export default function createHelloWorldService({ speak }) {
return {
name: 'HelloWorldService',
speak: speak || _speak,
};
}
```
A service, once created, can be registered with the `ServicesManager` to make it
accessible to extensions. Similarly, the application code can access named
services from the `ServicesManager`.
```js
// In the application
const speak = () => {
window.alert('HELLO WORLD');
};
const HelloWorldService = createHelloWorldService({ speak });
const servicesManager = new ServicesManager();
servicesManager.registerService(HelloWorldService);
// In an extension
const { HelloWorldService } = servicesManager.services;
if (HelloWorldService) {
HelloWorldService.speak();
}
```
+69
View File
@@ -0,0 +1,69 @@
# Services Overview
- [Overview](#overview)
- [Kinds of Services](#kinds-of-services)
- [Services (default)](#services-default)
- [UI Services](#ui-services)
- [Related Patterns](#related-patterns)
## Overview
Services are a beefier version of [commands][commands]. They provide a set of
operations, often tied to some shared state, and are made available to
extensions via the `ServicesManager`. Services are particularly well suited to
address [cross-cutting concerns][cross-cutting-concerns].
Each service should be:
- self-contained
- able to fail and/or be removed without breaking the application
- completely interchangeable with another module implementing the same interface
## Kinds of Services
Depending on the kind of service, we follow slightly different conventions. For
example, a UI service often receives its implementation from a React Context
Provider. You can read more about the different kinds of services and what makes
them different below:
### Services (default)
Services are a work in progress. As we are still in the progress of creating a
non-ui maintained service, this usage may change.
[You can read more about default services: here](./default/index.md)
### UI Services
A typical web application will have components and state for common UI like
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
these components from an extension.
We maintain the following UI Services:
- [UIDialogService](./ui/ui-dialog-service.md)
- [UIModalService](./ui/ui-modal-service.md)
- [UINotificationService](./ui/ui-notification-service.md)
You can read more about a specific service by selecting it in the above list,
and more about [UI services in general: here](./ui/index.md)
## Related Patterns
Services are "concern-specific" code modules that can be consumed across layers.
We try to minimize the coupling they introduce by authoring services that are
able to fail or be removed. Related patterns that may reduce coupling include:
- Pub/Sub
- Commands
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[commands]: #/
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
<!-- prettier-ignore-end -->
+106
View File
@@ -0,0 +1,106 @@
# UI Services
- [Overview](#overview)
- [Example](#example)
- [Tips & Tricks](#/)
- [Maintained Services](#/)
## Overview
A typical web application will have components and state for common UI like
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
these components from an extension.
<div style="text-align: center;">
<a href="/assets/img/ui-services.png">
<img src="/assets/img/ui-services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
</div>
In `@ohif/core`, we have a collection of service factories. We select one we
would like our application to support, create an instance of it, and pass that
instance to our `ServicesManager` AND to a React component (in this example,
`ModalContext`'s provider).
The `ModalContext`'s provider:
- Exposes context values
- Exposes methods that leverage `useCallback` hooks
- Sets the service's implementation in a `useEffect` hook
The `ServicesManager` is:
- Passed to the `ExtensionManager`
- The `ExtensionManager` makes the `ServicesManager` available to:
- All of it's lifecycle hooks (`preRegistration`)
- Each "getModuleFunction" (`getToolbarModule`, `getPanelModule`, etc.)
## Example
That's all fine and good, but it's still a little too abstract. What does this
translate to in practice?
```js
// In the application
const UINotificationService = createUINotificationService();
const servicesManager = new ServicesManager();
servicesManager.registerService(UINotificationService);
// UI Service Provider
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [service, hide, show]);
// In an extension
const { UINotificationService } = servicesManager.services;
if (UINotificationService) {
UINotificationService.show('Hello from the other side 👋');
}
```
<div style="text-align: center;">
<a href="/assets/img/notification-example.gif">
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
</div>
## Tips & Tricks
It's important to remember that all we're doing is making it possible to control
bits of the application's UI from an extension. Here are a few non-obvious
takeaways worth mentioning:
- Your application code should continue to use React context
(consumers/providers) as it normally would
- You can substitute our "out of the box" UI implementations with your own
- You can create and register your own UI services
- You can choose not to register a service or provide a service implementation
- In extensions, you can provide fallback/alternative behavior if an expected
service is not registered
- No `UIModalService`? Use the `UINotificationService` to notify users.
- While we don't have an examples of this, you can technically register a
service in an extension and expose it to the core application
> Note: These are recommended patterns, not hard and fast rules. Following them
> will help reduce confusion and interoperability with the larger OHIF
> community, but they're not silver bullets. Please speak up, create an issue,
> if you would like to discuss new services or improvements to this pattern.
## Maintained Services
Our `@ohif/viewer` project is an example of how to glue together the different
parts and pieces of the OHIF Platform to create a polished and powerful product.
To accomplish that, we maintain several UI Services that you can use in your own
project, or provide alternative implementations for:
| Name | Docs |
| --------------------- | ------------------------------------ |
| UIDialogService | [Here](./ui-dialog-service.md) |
| UIModalService | [Here](./ui-modal-service.md) |
| UINotificationService | [Here](./ui-notification-service.md) |
@@ -0,0 +1,49 @@
# UI Dialog Service
Dialogs have similar characteristics to that of Modals, but often with a
streamlined focus. They can be helpful when:
- We need to grab the user's attention
- We need user input
- We need to show additional information
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
<div style="text-align: center;">
<a href="/assets/img/dialog-example.gif">
<img src="/assets/img/dialog-example.gif" alt="UI Dialog Service Example" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>GIF showing successful call of UIDialogService from an extension.</i></div>
</div>
## Interface
For a more detailed look on the options and return values each of these methods
is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| -------------- | ------------------------------------------------------ |
| `create()` | Creates a new Dialog that is displayed until dismissed |
| `dismiss()` | Dismisses the specified dialog |
| `dismissAll()` | Dismisses all dialogs |
## Implementations
| Implementation | Consumer |
| ------------------------------------ | -------------------------- |
| [Dialog Provider][dialog-provider]\* | Baked into Dialog Provider |
`*` - Denotes maintained by OHIF
> 3rd Party implementers may be added to this table via pull requests.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIDialogService/index.js
[dialog-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/DialogProvider.js
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
<!-- prettier-ignore-end -->
@@ -0,0 +1,50 @@
# UI Modal Service
Modals have similar characteristics to that of Dialogs, but are often larger,
and only allow for a single instance to be viewable at once. They also tend to
be centered, and not draggable. They're commonly used when:
- We need to grab the user's attention
- We need user input
- We need to show additional information
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
<div style="text-align: center;">
<a href="/assets/img/modal-example.gif">
<img src="/assets/img/modal-example.gif" alt="UI Modal Service Example" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>GIF showing successful call of UIModalService from an extension.</i></div>
</div>
## Interface
For a more detailed look on the options and return values each of these methods
is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| ---------- | ------------------------------------- |
| `hide()` | Hides the open modal |
| `show()` | Shows the provided content in a modal |
## Implementations
| Implementation | Consumer |
| ---------------------------------- | ----------------------------- |
| [Modal Provider][modal-provider]\* | [OHIFModal][modal-consumer]\* |
`*` - Denotes maintained by OHIF
> 3rd Party implementers may be added to this table via pull requests.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js
[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js
[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
<!-- prettier-ignore-end -->
@@ -0,0 +1,51 @@
# UI Notification Service
Notifications can be annoying and disruptive. They can also deliver timely
helpful information, or expedite the user's workflow. Here is some high level
guidance on when and how to use them:
- Notifications should be non-interfering (timely, relevant, important)
- We should only show small/brief notifications
- Notifications should be contextual to current behavior/actions
- Notifications can serve warnings (acting as a confirmation)
If you're curious about the DOs and DON'Ts of notifications, check out this
article: ["How To Design Notifications For Better UX"][ux-article]
<div style="text-align: center;">
<a href="/assets/img/notification-example.gif">
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
</div>
## Interface
For a more detailed look on the options and return values each of these methods
is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| ---------- | --------------------------------------- |
| `hide()` | Hides the specified notification |
| `show()` | Creates and displays a new notification |
## Implementations
| Implementation | Consumer |
| ---------------------------------------- | ----------------------------------------- |
| [Snackbar Provider][snackbar-provider]\* | [SnackbarContainer][snackbar-container]\* |
`*` - Denotes maintained by OHIF
> 3rd Party implementers may be added to this table via pull requests.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UINotificationService/index.js
[snackbar-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/SnackbarProvider.js
[snackbar-container]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/components/snackbar/SnackbarContainer.js
[ux-article]: https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d
<!-- prettier-ignore-end -->
+4 -1
View File
@@ -11,7 +11,10 @@ export default {
* LIFECYCLE HOOKS
*/
preRegistration(extensionConfiguration) {},
preRegistration({
servicesManager,
configuration: extensionConfiguration,
}) {},
/**
* MODULE GETTERS
+79
View File
@@ -3,6 +3,85 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.0...@ohif/extension-cornerstone@1.7.1) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.6.0...@ohif/extension-cornerstone@1.7.0) (2019-11-25)
### Features
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.1...@ohif/extension-cornerstone@1.6.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.0...@ohif/extension-cornerstone@1.5.1) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.1...@ohif/extension-cornerstone@1.5.0) (2019-11-13)
### Features
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
## [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)
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "1.3.1",
"version": "1.7.2",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -32,7 +32,7 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.0.9",
"cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3",
@@ -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"
}
}
@@ -2,6 +2,7 @@ import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF from '@ohif/core';
import { connect } from 'react-redux';
import throttle from 'lodash.throttle';
import { setEnabledElement } from './state';
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
const {
@@ -80,11 +81,11 @@ const mapDispatchToProps = (dispatch, ownProps) => {
*/
onElementEnabled: event => {
const enabledElement = event.detail.element;
setEnabledElement(viewportIndex, enabledElement);
dispatch(
setViewportSpecificData(viewportIndex, {
// TODO: Hack to make sure our plugin info is available from the outset
plugin: 'cornerstone',
dom: enabledElement,
})
);
},
@@ -0,0 +1,148 @@
import React from 'react';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import PropTypes from 'prop-types';
import { ViewportDownloadForm } from '@ohif/ui';
import { utils } from '@ohif/core';
import { getEnabledElement } from './state';
const MINIMUM_SIZE = 100;
const DEFAULT_SIZE = 512;
const MAX_TEXTURE_SIZE = 10000;
const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => {
const activeEnabledElement = getEnabledElement(activeViewportIndex);
const enableViewport = viewportElement => {
if (viewportElement) {
cornerstone.enable(viewportElement);
}
};
const disableViewport = viewportElement => {
if (viewportElement) {
cornerstone.disable(viewportElement);
}
};
const updateViewportPreview = (viewportElement, downloadCanvas, fileType) =>
new Promise(resolve => {
cornerstone.fitToWindow(viewportElement);
viewportElement.addEventListener(
'cornerstoneimagerendered',
function updateViewport(event) {
const enabledElement = cornerstone.getEnabledElement(event.target)
.element;
const type = 'image/' + fileType;
const dataUrl = downloadCanvas.toDataURL(type, 1);
let newWidth = enabledElement.offsetHeight;
let newHeight = enabledElement.offsetWidth;
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
newHeight *= multiplier;
newWidth *= multiplier;
}
resolve({ dataUrl, width: newWidth, height: newHeight });
viewportElement.removeEventListener(
'cornerstoneimagerendered',
updateViewport
);
}
);
});
const loadImage = (activeViewport, viewportElement, width, height) =>
new Promise(resolve => {
if (activeViewport && viewportElement) {
const enabledElement = cornerstone.getEnabledElement(activeViewport);
const viewport = Object.assign({}, enabledElement.viewport);
delete viewport.scale;
viewport.translation = {
x: 0,
y: 0,
};
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
cornerstone.displayImage(viewportElement, image);
cornerstone.setViewport(viewportElement, viewport);
cornerstone.resize(viewportElement, true);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
resolve({ image, width: newWidth, height: newHeight });
});
}
});
const toggleAnnotations = (toggle, viewportElement) => {
cornerstoneTools.store.state.tools.forEach(({ name }) => {
if (toggle) {
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
} else {
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
}
});
};
const downloadBlob = (
filename,
fileType,
viewportElement,
downloadCanvas
) => {
const file = `${filename}.${fileType}`;
const mimetype = `image/${fileType}`;
/* Handles JPEG images for IE11 */
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
const image = downloadCanvas.toDataURL(mimetype, 1);
const blob = utils.b64toBlob(
image.replace('data:image/jpeg;base64,', ''),
mimetype
);
return window.navigator.msSaveBlob(blob, file);
}
viewportElement.querySelector('canvas').toBlob(blob => {
const URLObj = window.URL || window.webkitURL;
const a = document.createElement('a');
a.href = URLObj.createObjectURL(blob);
a.download = file;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
};
return (
<ViewportDownloadForm
onClose={onClose}
minimumSize={MINIMUM_SIZE}
maximumSize={MAX_TEXTURE_SIZE}
defaultSize={DEFAULT_SIZE}
canvasClass={'cornerstone-canvas'}
activeViewport={activeEnabledElement}
enableViewport={enableViewport}
disableViewport={disableViewport}
updateViewportPreview={updateViewportPreview}
loadImage={loadImage}
toggleAnnotations={toggleAnnotations}
downloadBlob={downloadBlob}
/>
);
};
CornerstoneViewportDownloadForm.propTypes = {
onClose: PropTypes.func,
activeViewportIndex: PropTypes.number.isRequired,
};
export default CornerstoneViewportDownloadForm;
+237 -238
View File
@@ -1,261 +1,260 @@
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll');
const actions = {
rotateViewport: ({ viewports, rotation }) => {
const enabledElement = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
const commandsModule = ({ servicesManager }) => {
const actions = {
rotateViewport: ({ viewports, rotation }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.rotation += rotation;
cornerstone.setViewport(enabledElement, viewport);
}
},
flipViewportHorizontal: ({ viewports }) => {
const enabledElement = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.hflip = !viewport.hflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
flipViewportVertical: ({ viewports }) => {
const enabledElement = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.vflip = !viewport.vflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
scaleViewport: ({ viewports, direction }) => {
const enabledElement = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
const step = direction * 0.15;
if (enabledElement) {
if (step) {
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.scale += step;
viewport.rotation += rotation;
cornerstone.setViewport(enabledElement, viewport);
} else {
cornerstone.fitToWindow(enabledElement);
}
}
},
resetViewport: ({ viewports }) => {
const enabledElement = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
},
flipViewportHorizontal: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
cornerstone.reset(enabledElement);
}
},
invertViewport: ({ viewports }) => {
const enabledElement = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.hflip = !viewport.hflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
flipViewportVertical: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.invert = !viewport.invert;
cornerstone.setViewport(enabledElement, viewport);
}
},
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
// better mouseButtonMask sets.
setToolActive: ({ toolName }) => {
if (!toolName) {
console.warn('No toolname provided to setToolActive command');
}
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
},
updateViewportDisplaySet: ({ direction }) => {
// TODO
console.warn('updateDisplaySet: ', direction);
},
clearAnnotations: ({ viewports }) => {
const element = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
if (!element) {
return;
}
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.vflip = !viewport.vflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
scaleViewport: ({ direction, viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
const step = direction * 0.15;
const enabledElement = cornerstone.getEnabledElement(element);
if (!enabledElement || !enabledElement.image) {
return;
}
const {
toolState,
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
if (
!toolState ||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
) {
return;
}
const imageIdToolState = toolState[enabledElement.image.imageId];
const measurementsToRemove = [];
Object.keys(imageIdToolState).forEach(toolType => {
const { data } = imageIdToolState[toolType];
data.forEach(measurementData => {
const { _id, lesionNamingNumber, measurementNumber } = measurementData;
if (!_id) {
return;
if (enabledElement) {
if (step) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.scale += step;
cornerstone.setViewport(enabledElement, viewport);
} else {
cornerstone.fitToWindow(enabledElement);
}
}
},
resetViewport: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
measurementsToRemove.push({
toolType,
_id,
lesionNamingNumber,
measurementNumber,
if (enabledElement) {
cornerstone.reset(enabledElement);
}
},
invertViewport: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.invert = !viewport.invert;
cornerstone.setViewport(enabledElement, viewport);
}
},
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
// better mouseButtonMask sets.
setToolActive: ({ toolName }) => {
if (!toolName) {
console.warn('No toolname provided to setToolActive command');
}
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
},
updateViewportDisplaySet: ({ direction }) => {
// TODO
console.warn('updateDisplaySet: ', direction);
},
clearAnnotations: ({ viewports }) => {
const element = getEnabledElement(viewports.activeViewportIndex);
if (!element) {
return;
}
const enabledElement = cornerstone.getEnabledElement(element);
if (!enabledElement || !enabledElement.image) {
return;
}
const {
toolState,
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
if (
!toolState ||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
) {
return;
}
const imageIdToolState = toolState[enabledElement.image.imageId];
const measurementsToRemove = [];
Object.keys(imageIdToolState).forEach(toolType => {
const { data } = imageIdToolState[toolType];
data.forEach(measurementData => {
const {
_id,
lesionNamingNumber,
measurementNumber,
} = measurementData;
if (!_id) {
return;
}
measurementsToRemove.push({
toolType,
_id,
lesionNamingNumber,
measurementNumber,
});
});
});
});
measurementsToRemove.forEach(measurementData => {
OHIF.measurements.MeasurementHandlers.onRemoved({
detail: {
toolType: measurementData.toolType,
measurementData,
measurementsToRemove.forEach(measurementData => {
OHIF.measurements.MeasurementHandlers.onRemoved({
detail: {
toolType: measurementData.toolType,
measurementData,
},
});
});
},
nextImage: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
scroll(enabledElement, 1);
},
previousImage: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
scroll(enabledElement, -1);
},
getActiveViewportEnabledElement: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
return enabledElement;
},
showDownloadViewportModal: ({ title, viewports }) => {
const activeViewportIndex = viewports.activeViewportIndex;
const { UIModalService } = servicesManager.services;
UIModalService.show({
content: CornerstoneViewportDownloadForm,
title,
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
},
});
});
},
nextImage: ({ viewports }) => {
const enabledElement = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
},
};
scroll(enabledElement, 1);
},
previousImage: ({ viewports }) => {
const enabledElement = _getActiveViewportEnabledElement(
viewports.viewportSpecificData,
viewports.activeViewportIndex
);
const definitions = {
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
storeContexts: ['viewports'],
options: {},
},
getActiveViewportEnabledElement: {
commandFn: actions.getActiveViewportEnabledElement,
storeContexts: ['viewports'],
options: {},
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: 90 },
},
rotateViewportCCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: -90 },
},
invertViewport: {
commandFn: actions.invertViewport,
storeContexts: ['viewports'],
options: {},
},
flipViewportVertical: {
commandFn: actions.flipViewportVertical,
storeContexts: ['viewports'],
options: {},
},
flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal,
storeContexts: ['viewports'],
options: {},
},
scaleUpViewport: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: 1 },
},
scaleDownViewport: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: -1 },
},
fitViewportToWindow: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: 0 },
},
resetViewport: {
commandFn: actions.resetViewport,
storeContexts: ['viewports'],
options: {},
},
clearAnnotations: {
commandFn: actions.clearAnnotations,
storeContexts: ['viewports'],
options: {},
},
nextImage: {
commandFn: actions.nextImage,
storeContexts: ['viewports'],
options: {},
},
previousImage: {
commandFn: actions.previousImage,
storeContexts: ['viewports'],
options: {},
},
// TODO: First/Last image
// Next/Previous series/DisplaySet
nextViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: 1 },
},
previousViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: -1 },
},
// TOOLS
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
options: {},
},
};
scroll(enabledElement, -1);
},
return {
actions,
definitions,
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
};
};
const definitions = {
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: 90 },
},
rotateViewportCCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: -90 },
},
invertViewport: {
commandFn: actions.invertViewport,
storeContexts: ['viewports'],
options: {},
},
flipViewportVertical: {
commandFn: actions.flipViewportVertical,
storeContexts: ['viewports'],
options: {},
},
flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal,
storeContexts: ['viewports'],
options: {},
},
scaleUpViewport: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: 1 },
},
scaleDownViewport: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: -1 },
},
fitViewportToWindow: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: 0 },
},
resetViewport: {
commandFn: actions.resetViewport,
storeContexts: ['viewports'],
options: {},
},
clearAnnotations: {
commandFn: actions.clearAnnotations,
storeContexts: ['viewports'],
options: {},
},
nextImage: {
commandFn: actions.nextImage,
storeContexts: ['viewports'],
options: {},
},
previousImage: {
commandFn: actions.previousImage,
storeContexts: ['viewports'],
options: {},
},
// TODO: First/Last image
// Next/Previous series/DisplaySet
nextViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: 1 },
},
previousViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: -1 },
},
// TOOLS
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
options: {},
},
};
/**
* Grabs `dom` reference for the enabledElement of
* the active viewport
*/
function _getActiveViewportEnabledElement(viewports, activeIndex) {
const activeViewport = viewports[activeIndex] || {};
return activeViewport.dom;
}
export default {
actions,
definitions,
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
};
export default commandsModule;
+7 -4
View File
@@ -2,6 +2,7 @@ import init from './init.js';
import asyncComponent from './asyncComponent.js';
import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const OHIFCornerstoneViewport = asyncComponent(() =>
import(
@@ -24,8 +25,8 @@ export default {
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration(configuration = {}) {
init(configuration);
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
getViewportModule() {
return OHIFCornerstoneViewport;
@@ -33,7 +34,9 @@ export default {
getToolbarModule() {
return toolbarModule;
},
getCommandsModule() {
return commandsModule;
getCommandsModule({ servicesManager }) {
return commandsModule({ servicesManager });
},
};
export { CornerstoneViewportDownloadForm };
+54 -3
View File
@@ -2,13 +2,56 @@ 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';
import { SimpleDialog } from '@ohif/ui';
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);
/**
*
* @param {object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init(configuration = {}) {
export default function init({ servicesManager, configuration = {} }) {
const { UIDialogService } = servicesManager.services;
const callInputDialog = (data, event, callback) => {
let dialogId = UIDialogService.create({
content: SimpleDialog.InputDialog,
defaultPosition: {
x: (event && event.currentPoints.canvas.x) || 0,
y: (event && event.currentPoints.canvas.y) || 0,
},
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
measurementData: data ? { description: data.text } : {},
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
},
},
});
};
const { csToolsConfig } = configuration;
const { StackManager } = OHIF.utils;
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
@@ -42,7 +85,6 @@ export default function init(configuration = {}) {
ZoomTouchPinchTool,
// Annotations
EraserTool,
ArrowAnnotateTool,
BidirectionalTool,
LengthTool,
AngleTool,
@@ -65,7 +107,6 @@ export default function init(configuration = {}) {
ZoomTouchPinchTool,
// Annotations
EraserTool,
ArrowAnnotateTool,
BidirectionalTool,
LengthTool,
AngleTool,
@@ -78,6 +119,16 @@ export default function init(configuration = {}) {
];
tools.forEach(tool => csTools.addTool(tool));
csTools.addTool(csTools.ArrowAnnotateTool, {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
},
});
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
+20
View File
@@ -0,0 +1,20 @@
const state = {
enabledElements: {},
};
/**
* Sets the enabled element `dom` reference for an active viewport.
* @param {HTMLElement} dom Active viewport element.
* @return void
*/
const setEnabledElement = (viewportIndex, element) =>
(state.enabledElements[viewportIndex] = element);
/**
* Grabs the enabled element `dom` reference of an active viewport.
*
* @return {HTMLElement} Active viewport element.
*/
const getEnabledElement = viewportIndex => state.enabledElements[viewportIndex];
export { setEnabledElement, getEnabledElement };
+23 -1
View File
@@ -23,6 +23,17 @@ const TOOLBAR_BUTTON_TYPES = {
BUILT_IN: 'builtIn',
};
const TOOLBAR_BUTTON_BEHAVIORS = {
CINE: 'CINE',
DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT',
};
/* TODO: Export enums through a extension manager. */
const enums = {
TOOLBAR_BUTTON_TYPES,
TOOLBAR_BUTTON_BEHAVIORS,
};
const definitions = [
{
id: 'StackScroll',
@@ -102,7 +113,7 @@ const definitions = [
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'CINE',
behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
},
},
{
@@ -213,6 +224,17 @@ const definitions = [
commandName: 'setToolActive',
commandOptions: { toolName: 'Eraser' },
},
{
id: 'Download',
label: 'Download',
icon: 'create-screen-capture',
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT,
togglable: true,
},
},
],
},
];
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [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.2",
"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(viewportIndex);
}
};
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>}
</>
+280
View File
@@ -3,6 +3,286 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.54.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.2...@ohif/extension-vtk@0.54.3) (2019-12-06)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.54.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.1...@ohif/extension-vtk@0.54.2) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.54.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.0...@ohif/extension-vtk@0.54.1) (2019-11-28)
**Note:** Version bump only for package @ohif/extension-vtk
# [0.54.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.13...@ohif/extension-vtk@0.54.0) (2019-11-25)
### Features
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
## [0.53.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.12...@ohif/extension-vtk@0.53.13) (2019-11-25)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.11...@ohif/extension-vtk@0.53.12) (2019-11-20)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.10...@ohif/extension-vtk@0.53.11) (2019-11-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.9...@ohif/extension-vtk@0.53.10) (2019-11-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.8...@ohif/extension-vtk@0.53.9) (2019-11-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.7...@ohif/extension-vtk@0.53.8) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.6...@ohif/extension-vtk@0.53.7) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.5...@ohif/extension-vtk@0.53.6) (2019-11-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.4...@ohif/extension-vtk@0.53.5) (2019-11-13)
**Note:** Version bump only for package @ohif/extension-vtk
## [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)
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.52.17",
"version": "0.54.3",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -49,12 +49,12 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"lodash.throttle": "^4.1.1",
"react-vtkjs-viewport": "^0.3.6"
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^1.5.0",
"@ohif/ui": "^0.56.0",
"cornerstone-tools": "^4.0.9",
"@ohif/core": "^1.13.3",
"@ohif/ui": "^0.65.1",
"cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3",
+56 -14
View File
@@ -65,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,
};
@@ -175,6 +183,19 @@ class OHIFVTKViewport extends Component {
}
};
/**
*
*
* @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];
@@ -183,18 +204,11 @@ class OHIFVTKViewport extends Component {
const { vtkImageData, imageMetaData0 } = imageDataObject;
const { windowWidth, windowCenter, modality } = imageMetaData0;
let lower;
let upper;
if (modality === 'PT') {
// For PET just set the range to 0-5 SUV
lower = 0;
upper = 5;
} else {
lower = windowCenter - windowWidth / 2.0;
upper = windowCenter + windowWidth / 2.0;
}
const { lower, upper } = _getRangeFromWindowLevels(
windowWidth,
windowCenter,
modality
);
const volumeActor = vtkVolume.newInstance();
const volumeMapper = vtkVolumeMapper.newInstance();
@@ -379,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;
+322 -318
View File
@@ -1,7 +1,7 @@
import throttle from 'lodash.throttle';
import {
vtkInteractorStyleMPRCrosshairs,
vtkInteractorStyleMPRWindowLevel,
vtkInteractorStyleMPRSlice,
vtkInteractorStyleMPRRotate,
vtkSVGCrosshairsWidget,
} from 'react-vtkjs-viewport';
@@ -9,354 +9,358 @@ import {
import setMPRLayout from './utils/setMPRLayout.js';
import setViewportToVTK from './utils/setViewportToVTK.js';
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 = {};
const commandsModule = ({ commandsManager }) => {
// TODO: Put this somewhere else
let apis = {};
async function _getActiveViewportVTKApi(viewports) {
const {
numRows,
numColumns,
layout,
viewportSpecificData,
activeViewportIndex,
} = viewports;
async function _getActiveViewportVTKApi(viewports) {
const {
numRows,
numColumns,
layout,
viewportSpecificData,
activeViewportIndex,
} = viewports;
const currentData = layout.viewports[activeViewportIndex];
if (currentData && currentData.plugin === 'vtk') {
// TODO: I was storing/pulling this from Redux but ran into weird issues
if (apis[activeViewportIndex]) {
return apis[activeViewportIndex];
const currentData = layout.viewports[activeViewportIndex];
if (currentData && currentData.plugin === 'vtk') {
// TODO: I was storing/pulling this from Redux but ran into weird issues
if (apis[activeViewportIndex]) {
return apis[activeViewportIndex];
}
}
const displaySet = viewportSpecificData[activeViewportIndex];
let api;
if (!api) {
try {
api = await setViewportToVTK(
displaySet,
activeViewportIndex,
numRows,
numColumns,
layout,
viewportSpecificData
);
} catch (error) {
throw new Error(error);
}
}
return api;
}
const displaySet = viewportSpecificData[activeViewportIndex];
function _setView(api, sliceNormal, viewUp) {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
istyle.setSliceNormal(...sliceNormal);
istyle.setViewUp(...viewUp);
let api;
if (!api) {
try {
api = await setViewportToVTK(
displaySet,
activeViewportIndex,
numRows,
numColumns,
layout,
viewportSpecificData
renderWindow.render();
}
function getVOIFromCornerstoneViewport() {
const dom = commandsManager.runCommand('getActiveViewportEnabledElement');
const cornerstoneElement = cornerstone.getEnabledElement(dom);
if (cornerstoneElement) {
const imageId = cornerstoneElement.image.imageId;
const { modality } = cornerstone.metaData.get(
'generalSeriesModule',
imageId
);
} catch (error) {
throw new Error(error);
if (modality !== 'PT') {
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
return {
windowWidth,
windowCenter,
};
}
}
}
return api;
}
function setVOI(voi) {
const { windowWidth, windowCenter } = voi;
const lower = windowCenter - windowWidth / 2.0;
const upper = windowCenter + windowWidth / 2.0;
function _setView(api, sliceNormal, viewUp) {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
istyle.setSliceNormal(...sliceNormal);
istyle.setViewUp(...viewUp);
const rgbTransferFunction = apis[0].volumes[0]
.getProperty()
.getRGBTransferFunction(0);
renderWindow.render();
}
rgbTransferFunction.setRange(lower, upper);
const actions = {
axial: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
apis[viewports.activeViewportIndex] = api;
_setView(api, [0, 0, 1], [0, -1, 0]);
},
sagittal: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
apis[viewports.activeViewportIndex] = api;
_setView(api, [1, 0, 0], [0, 0, 1]);
},
coronal: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
apis[viewports.activeViewportIndex] = api;
_setView(api, [0, 1, 0], [0, 0, 1]);
},
enableRotateTool: () => {
apis.forEach(api => {
const istyle = vtkInteractorStyleMPRRotate.newInstance();
api.setInteractorStyle({ istyle });
api.updateVOI(windowWidth, windowCenter);
});
},
enableCrosshairsTool: () => {
apis.forEach((api, apiIndex) => {
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
}
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
});
},
enableLevelTool: () => {
function updateVOI(apis, windowWidth, windowCenter) {
const actions = {
axial: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
apis[viewports.activeViewportIndex] = api;
_setView(api, [0, 0, 1], [0, -1, 0]);
},
sagittal: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
apis[viewports.activeViewportIndex] = api;
_setView(api, [1, 0, 0], [0, 0, 1]);
},
coronal: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
apis[viewports.activeViewportIndex] = api;
_setView(api, [0, 1, 0], [0, 0, 1]);
},
enableRotateTool: () => {
apis.forEach(api => {
api.updateVOI(windowWidth, windowCenter);
const istyle = vtkInteractorStyleMPRRotate.newInstance();
api.setInteractorStyle({ istyle });
});
}
},
enableCrosshairsTool: () => {
apis.forEach((api, apiIndex) => {
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
const callbacks = {
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
renderWindow.render();
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
throttledUpdateVOIs(apis, windowWidth, windowCenter);
},
};
apis.forEach(api => {
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
api.setInteractorStyle({ istyle, callbacks });
});
},
setSlabThickness: ({ slabThickness }) => {
apis.forEach(api => {
api.setSlabThickness(slabThickness);
});
},
changeSlabThickness: ({ change }) => {
apis.forEach(api => {
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
api.setSlabThickness(slabThickness);
});
},
setBlendModeToComposite: () => {
apis.forEach(api => {
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(slabThickness);
}
renderWindow.render();
});
},
setBlendModeToMaximumIntensity: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToMaximumIntensity) {
mapper.setBlendModeToMaximumIntensity();
}
renderWindow.render();
});
},
setBlendMode: ({ blendMode }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
api.volumes[0].getMapper().setBlendMode(blendMode);
renderWindow.render();
});
},
mpr2d: async ({ viewports }) => {
// TODO push a lot of this backdoor logic lower down to the library level.
const displaySet =
viewports.viewportSpecificData[viewports.activeViewportIndex];
// 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 {
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
} catch (error) {
throw new Error(error);
}
if (cornerstoneVOI) {
setVOI(cornerstoneVOI);
}
// Add widgets and set default interactorStyle of each viewport.
apis.forEach((api, apiIndex) => {
api.addSVGWidget(
vtkSVGCrosshairsWidget.newInstance(),
'crosshairsWidget'
);
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
});
},
};
window.vtkActions = actions;
const definitions = {
axial: {
commandFn: actions.axial,
storeContexts: ['viewports'],
options: {},
},
coronal: {
commandFn: actions.coronal,
storeContexts: ['viewports'],
options: {},
},
sagittal: {
commandFn: actions.sagittal,
storeContexts: ['viewports'],
options: {},
},
enableRotateTool: {
commandFn: actions.enableRotateTool,
storeContexts: [],
options: {},
},
enableCrosshairsTool: {
commandFn: actions.enableCrosshairsTool,
storeContexts: [],
options: {},
},
enableLevelTool: {
commandFn: actions.enableLevelTool,
storeContexts: [],
options: {},
},
setBlendModeToComposite: {
commandFn: actions.setBlendModeToComposite,
storeContexts: [],
options: { blendMode: BlendMode.COMPOSITE_BLEND },
},
setBlendModeToMaximumIntensity: {
commandFn: actions.setBlendModeToMaximumIntensity,
storeContexts: [],
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
},
setBlendModeToMinimumIntensity: {
commandFn: actions.setBlendMode,
storeContexts: [],
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
},
setBlendModeToAverageIntensity: {
commandFn: actions.setBlendMode,
storeContexts: [],
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
},
setSlabThickness: {
// TODO: How do we pass in a function argument?
commandFn: actions.setSlabThickness,
storeContexts: [],
options: {},
},
increaseSlabThickness: {
commandFn: actions.changeSlabThickness,
storeContexts: [],
options: {
change: 3,
},
},
decreaseSlabThickness: {
commandFn: actions.changeSlabThickness,
storeContexts: [],
options: {
change: -3,
},
},
mpr2d: {
commandFn: actions.mpr2d,
storeContexts: ['viewports'],
options: {},
context: 'VIEWER',
},
};
enableLevelTool: () => {
function updateVOI(apis, windowWidth, windowCenter) {
apis.forEach(api => {
api.updateVOI(windowWidth, windowCenter);
});
}
export default {
definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK',
};
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
function getVOIFromCornerstoneViewport(displaySet) {
const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom);
const callbacks = {
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
if (cornerstoneElement) {
const imageId = cornerstoneElement.image.imageId;
renderWindow.render();
});
const { modality } = cornerstone.metaData.get(
'generalSeriesModule',
imageId
);
if (modality !== 'PT') {
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
return {
windowWidth,
windowCenter,
throttledUpdateVOIs(apis, windowWidth, windowCenter);
},
};
}
}
}
function setVOI(voi) {
const { windowWidth, windowCenter } = voi;
const lower = windowCenter - windowWidth / 2.0;
const upper = windowCenter + windowWidth / 2.0;
apis.forEach(api => {
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
const rgbTransferFunction = apis[0].volumes[0]
.getProperty()
.getRGBTransferFunction(0);
api.setInteractorStyle({ istyle, callbacks });
});
},
setSlabThickness: ({ slabThickness }) => {
apis.forEach(api => {
api.setSlabThickness(slabThickness);
});
},
changeSlabThickness: ({ change }) => {
apis.forEach(api => {
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
rgbTransferFunction.setRange(lower, upper);
api.setSlabThickness(slabThickness);
});
},
setBlendModeToComposite: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
apis.forEach(api => {
api.updateVOI(windowWidth, windowCenter);
});
}
const slabThickness = api.getSlabThickness();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToComposite) {
mapper.setBlendModeToComposite();
}
if (istyle.setSlabThickness) {
istyle.setSlabThickness(slabThickness);
}
renderWindow.render();
});
},
setBlendModeToMaximumIntensity: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToMaximumIntensity) {
mapper.setBlendModeToMaximumIntensity();
}
renderWindow.render();
});
},
setBlendMode: ({ blendMode }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
api.volumes[0].getMapper().setBlendMode(blendMode);
renderWindow.render();
});
},
mpr2d: async ({ viewports }) => {
// TODO push a lot of this backdoor logic lower down to the library level.
const displaySet =
viewports.viewportSpecificData[viewports.activeViewportIndex];
// Get current VOI if cornerstone viewport.
const cornerstoneVOI = getVOIFromCornerstoneViewport();
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 {
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
} catch (error) {
throw new Error(error);
}
if (cornerstoneVOI) {
setVOI(cornerstoneVOI);
}
// Add widgets and set default interactorStyle of each viewport.
apis.forEach((api, apiIndex) => {
api.addSVGWidget(
vtkSVGCrosshairsWidget.newInstance(),
'crosshairsWidget'
);
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
});
},
};
window.vtkActions = actions;
const definitions = {
axial: {
commandFn: actions.axial,
storeContexts: ['viewports'],
options: {},
},
coronal: {
commandFn: actions.coronal,
storeContexts: ['viewports'],
options: {},
},
sagittal: {
commandFn: actions.sagittal,
storeContexts: ['viewports'],
options: {},
},
enableRotateTool: {
commandFn: actions.enableRotateTool,
storeContexts: [],
options: {},
},
enableCrosshairsTool: {
commandFn: actions.enableCrosshairsTool,
storeContexts: [],
options: {},
},
enableLevelTool: {
commandFn: actions.enableLevelTool,
storeContexts: [],
options: {},
},
setBlendModeToComposite: {
commandFn: actions.setBlendModeToComposite,
storeContexts: [],
options: { blendMode: BlendMode.COMPOSITE_BLEND },
},
setBlendModeToMaximumIntensity: {
commandFn: actions.setBlendModeToMaximumIntensity,
storeContexts: [],
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
},
setBlendModeToMinimumIntensity: {
commandFn: actions.setBlendMode,
storeContexts: [],
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
},
setBlendModeToAverageIntensity: {
commandFn: actions.setBlendMode,
storeContexts: [],
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
},
setSlabThickness: {
// TODO: How do we pass in a function argument?
commandFn: actions.setSlabThickness,
storeContexts: [],
options: {},
},
increaseSlabThickness: {
commandFn: actions.changeSlabThickness,
storeContexts: [],
options: {
change: 3,
},
},
decreaseSlabThickness: {
commandFn: actions.changeSlabThickness,
storeContexts: [],
options: {
change: -3,
},
},
mpr2d: {
commandFn: actions.mpr2d,
storeContexts: ['viewports'],
options: {},
context: 'VIEWER',
},
};
return {
definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK',
};
};
export default commandsModule;
+2 -2
View File
@@ -20,8 +20,8 @@ const vtkExtension = {
getToolbarModule() {
return toolbarModule;
},
getCommandsModule() {
return commandsModule;
getCommandsModule({ commandsManager }) {
return commandsModule({ commandsManager });
},
};
@@ -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>
);
+3 -14
View File
@@ -8,15 +8,6 @@
# managed by CircleCI and deployed to our Google Hosting
#
# Settings in the [build] context are global and are applied to all contexts
# unless otherwise overridden by more specific contexts.
[build]
# Directory to change to before starting a build.
# This is where we will look for package.json/.nvmrc/etc.
base = ""
publish = "project/build-output/"
# NODE_VERSION in root `.nvmrc` takes priority
# YARN_FLAGS: https://www.netlify.com/docs/build-gotchas/#yarn
[build.environment]
@@ -28,17 +19,15 @@
# Production context: all deploys from the Production branch set in your site's
# deploy contexts will inherit these settings.
# [context.production]
# publish = "project/output/"
# command = "make publish"
# environment = { ACCESS_TOKEN = "super secret", NODE_VERSION = "8.0.1" }
[context.production]
ignore = "exit 0" # Never build production; We'll let our CI do that
# Deploy Preview context: all deploys generated from a pull/merge request will
# inherit these settings.
[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"
[[headers]]
# Define which paths this specific [[headers]] block will cover.
+2 -1
View File
@@ -72,6 +72,7 @@
"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",
@@ -102,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": {
+178
View File
@@ -3,6 +3,184 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.2...@ohif/core@1.13.3) (2019-12-06)
**Note:** Version bump only for package @ohif/core
## [1.13.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.1...@ohif/core@1.13.2) (2019-12-02)
**Note:** Version bump only for package @ohif/core
## [1.13.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.0...@ohif/core@1.13.1) (2019-11-28)
### Bug Fixes
* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
# [1.13.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.12.0...@ohif/core@1.13.0) (2019-11-25)
### Features
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.11.0...@ohif/core@1.12.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.10.0...@ohif/core@1.11.0) (2019-11-19)
### Features
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.1...@ohif/core@1.10.0) (2019-11-15)
### Features
* Inject into Extension Modules / improve tests ([f63d8a7](https://github.com/OHIF/Viewers/commit/f63d8a73d867ad9dfd8ee0cad74edce180eb34f0))
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.0...@ohif/core@1.9.1) (2019-11-15)
**Note:** Version bump only for package @ohif/core
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.8.0...@ohif/core@1.9.0) (2019-11-13)
### Features
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
# [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)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.5.0",
"version": "1.13.3",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -32,7 +32,7 @@
},
"peerDependencies": {
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.0.9",
"cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3"
},
+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;
}
+58 -9
View File
@@ -43,19 +43,68 @@ export class HotkeysManager {
}
/**
* Registers a list of hotkeydefinitions. Optionally, sets the
* default hotkey bindings for all provided definitions. These
* Registers a list of hotkeydefinitions.
*
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
*/
setHotkeys(hotkeyDefinitions) {
const definitions = Array.isArray(hotkeyDefinitions)
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
definitions.forEach(definition => this.registerHotkeys(definition));
}
/**
* Set default hotkey bindings. These
* values are used in `this.restoreDefaultBindings`.
*
* @param {HotkeyDefinition[]} hotkeyDefinitions
* @param {Boolean} [isDefaultDefinitions]
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
*/
setHotkeys(hotkeyDefinitions, isDefaultDefinitions = false) {
hotkeyDefinitions.forEach(definition => this.registerHotkeys(definition));
setDefaultHotKeys(hotkeyDefinitions) {
const definitions = Array.isArray(hotkeyDefinitions)
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
if (isDefaultDefinitions) {
this.hotkeyDefaults = hotkeyDefinitions;
}
this.hotkeyDefaults = definitions;
}
/**
* It parses given object containing hotkeyDefinition to array like.
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
*
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
* @returns {HotkeyDefinition[]}
*/
_parseToArrayLike(hotkeyDefinitionsObj) {
const copy = { ...hotkeyDefinitionsObj };
return Object.entries(copy).map(entryValue =>
this._parseToHotKeyObj(entryValue[0], entryValue[1])
);
}
/**
* Return HotkeyDefinition object like based on given property name and property value
* @param {string} propertyName property name of hotkey definition object
* @param {object} propertyValue property value of hotkey definition object
*
* @example
*
* const hotKeyObj = {hotKeyDefA: {keys:[],....}}
*
* const parsed = _parseToHotKeyObj(Object.keys(hotKeyDefA)[0], hotKeyObj[hotKeyDefA]);
* {
* commandName: hotKeyDefA,
* keys: [],
* ....
* }
*
*/
_parseToHotKeyObj(propertyName, propertyValue) {
return {
commandName: propertyName,
...propertyValue,
};
}
/**
@@ -94,18 +94,20 @@ describe('HotkeysManager', () => {
expect(firstCallArgs).toEqual(hotkeyDefinitions[0]);
expect(secondCallArgs).toEqual(hotkeyDefinitions[1]);
});
it('does not set this.hotkeyDefaults by default', () => {
it('does not set this.hotkeyDefaults when calling setHotKeys', () => {
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
hotkeysManager.setHotkeys(hotkeyDefinitions);
expect(hotkeysManager.hotkeyDefaults).toEqual([]);
});
it('sets this.hotkeyDefaults when isDefaultDefinitions is true', () => {
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
const isDefaultDefinitions = true;
});
hotkeysManager.setHotkeys(hotkeyDefinitions, isDefaultDefinitions);
describe('setDefaultHotKeys()', () => {
it('it sets default hotkeys', () => {
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
hotkeysManager.setDefaultHotKeys(hotkeyDefinitions);
expect(hotkeysManager.hotkeyDefaults).toEqual(hotkeyDefinitions);
});
@@ -147,6 +147,7 @@ export class StudyMetadata extends Metadata {
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
isClip: true,
@@ -2,12 +2,13 @@ import MODULE_TYPES from './MODULE_TYPES.js';
import log from './../log.js';
export default class ExtensionManager {
constructor({ commandsManager }) {
constructor({ commandsManager, servicesManager }) {
this.modules = {};
this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES);
//
this._commandsManager = commandsManager;
this._servicesManager = servicesManager;
this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = [];
@@ -66,7 +67,11 @@ export default class ExtensionManager {
// preRegistrationHook
if (extension.preRegistration) {
extension.preRegistration(configuration);
extension.preRegistration({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
configuration,
});
}
// Register Modules
@@ -106,7 +111,10 @@ export default class ExtensionManager {
}
try {
const extensionModule = getModuleFn();
const extensionModule = getModuleFn({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
});
if (!extensionModule) {
log.warn(
@@ -40,6 +40,31 @@ describe('ExtensionManager.js', () => {
});
describe('registerExtension()', () => {
it('calls preRegistration() for extension', () => {
// SUT
const fakeExtension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(fakeExtension);
// Assert
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
});
it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => {
const configuration = { config: 'Some configuration' };
extensionManager._servicesManager = { services: { TestService: {} } };
// SUT
const fakeExtension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(fakeExtension, configuration);
// Assert
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
commandsManager: extensionManager._commandsManager,
configuration,
});
});
it('logs a warning if the extension is null or undefined', () => {
const undefinedExtension = undefined;
const nullExtension = null;
@@ -110,6 +135,26 @@ describe('ExtensionManager.js', () => {
);
});
it('successfully passes a servicesManager and commandsManager instances to each module', () => {
extensionManager._servicesManager = { services: { TestService: {} } };
const extension = {
id: 'hello-world',
getViewportModule: jest.fn(),
getSopClassHandlerModule: jest.fn(),
getPanelModule: jest.fn(),
getToolbarModule: jest.fn(),
getCommandsModule: jest.fn(),
};
extensionManager.registerExtension(extension);
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
commandsManager: extensionManager._commandsManager,
});
});
it('successfully registers a module for each module type', () => {
const extension = {
id: 'hello-world',
+17
View File
@@ -1,6 +1,7 @@
import './lib';
import { ExtensionManager, MODULE_TYPES } from './extensions';
import { ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes/';
import DICOMWeb from './DICOMWeb';
@@ -18,12 +19,19 @@ import ui from './ui';
import user from './user.js';
import utils from './utils/';
import {
createUINotificationService,
createUIModalService,
createUIDialogService,
} from './services';
const OHIF = {
MODULE_TYPES,
//
CommandsManager,
ExtensionManager,
HotkeysManager,
ServicesManager,
//
utils,
studies,
@@ -41,6 +49,10 @@ const OHIF = {
viewer: {},
measurements,
hangingProtocols,
//
createUINotificationService,
createUIModalService,
createUIDialogService,
};
export {
@@ -49,6 +61,7 @@ export {
CommandsManager,
ExtensionManager,
HotkeysManager,
ServicesManager,
//
utils,
studies,
@@ -65,6 +78,10 @@ export {
DICOMWeb,
measurements,
hangingProtocols,
//
createUINotificationService,
createUIModalService,
createUIDialogService,
};
export { OHIF };
+5
View File
@@ -8,6 +8,11 @@ describe('Top level exports', () => {
'CommandsManager',
'ExtensionManager',
'HotkeysManager',
'ServicesManager',
//
'createUINotificationService',
'createUIModalService',
'createUIDialogService',
//
'utils',
'studies',
+24 -11
View File
@@ -12,13 +12,25 @@ import {
SET_VIEWPORT_LAYOUT_AND_DATA,
} from './constants/ActionTypes.js';
/**
* The definition of a viewport layout.
*
* @typedef {Object} ViewportLayout
* @property {number} numRows -
* @property {number} numColumns -
* @property {array} viewports -
*/
/**
* VIEWPORT
*/
export const setViewportSpecificData = (viewportIndex, data) => ({
export const setViewportSpecificData = (
viewportIndex,
viewportSpecificData
) => ({
type: SET_VIEWPORT,
viewportIndex,
data,
viewportSpecificData,
});
export const setViewportActive = viewportIndex => ({
@@ -27,10 +39,7 @@ export const setViewportActive = viewportIndex => ({
});
/**
* @param {object} layout
* @param {number} layout.numRows
* @param {number} layout.numColumns
* @param {array} layout.viewports
* @param {ViewportLayout} layout
*/
export const setLayout = ({ numRows, numColumns, viewports }) => ({
type: SET_VIEWPORT_LAYOUT,
@@ -40,10 +49,9 @@ export const setLayout = ({ numRows, numColumns, viewports }) => ({
});
/**
* @param {object} layout
* @param {number} layout.numRows
* @param {number} layout.numColumns
* @param {array} layout.viewports
* @param {array} viewports
*/
export const setViewportLayoutAndData = (
{ numRows, numColumns, viewports },
@@ -61,9 +69,9 @@ export const clearViewportSpecificData = viewportIndex => ({
viewportIndex,
});
export const setActiveViewportSpecificData = data => ({
export const setActiveViewportSpecificData = viewportSpecificData => ({
type: SET_ACTIVE_SPECIFIC_DATA,
data,
viewportSpecificData,
});
/**
@@ -113,13 +121,18 @@ export const setServers = servers => ({
});
const actions = {
// VIEWPORT
/**
* VIEWPORT
*/
setViewportActive,
setViewportSpecificData,
setViewportLayoutAndData,
setLayout,
clearViewportSpecificData,
setActiveViewportSpecificData,
/**
* NOT-VIEWPORT
*/
setStudyLoadingProgress,
clearStudyLoadingProgress,
setUserPreferences,
+5 -5
View File
@@ -28,7 +28,7 @@ describe('actions', () => {
describe('viewport action creators', () => {
it('should create an action to set the viewport specific data', () => {
const data = {
const viewportSpecificData = {
displaySetInstanceUid: 'ef859a23-4631-93ab-d26b-7940a822c699',
seriesDate: '20151026',
seriesTime: '082611.370000',
@@ -65,12 +65,12 @@ describe('actions', () => {
const expectedAction = {
type: types.SET_ACTIVE_SPECIFIC_DATA,
data,
viewportSpecificData,
};
expect(actions.setActiveViewportSpecificData(data)).toEqual(
expectedAction
);
expect(
actions.setActiveViewportSpecificData(viewportSpecificData)
).toEqual(expectedAction);
});
it('should create an action to clear clearViewportSpecificData', () => {
+13 -13
View File
@@ -1,25 +1,25 @@
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: '' },
},
generalPreferences: {
// language: 'en-US'
},
};
const preferences = (state, action) => {
switch (action.type) {
case 'SET_USER_PREFERENCES': {
const newState = action.state ? action.state : cloneDeep(defaultState);
const newState = action.state || cloneDeep(defaultState);
return Object.assign({}, state, newState);
}
+103 -51
View File
@@ -1,3 +1,6 @@
import cloneDeep from 'lodash.clonedeep';
import merge from 'lodash.merge';
import {
CLEAR_VIEWPORT,
SET_ACTIVE_SPECIFIC_DATA,
@@ -8,10 +11,7 @@ import {
SET_VIEWPORT_LAYOUT_AND_DATA,
} from './../constants/ActionTypes.js';
import cloneDeep from 'lodash.clonedeep';
import merge from 'lodash.merge';
const defaultState = {
const DEFAULT_STATE = {
numRows: 1,
numColumns: 1,
activeViewportIndex: 0,
@@ -26,85 +26,137 @@ const defaultState = {
};
/**
* @param {Object} [state=defaultState]
* @param {Object} action
* @param {string} [action.type]
* @param {number} [action.viewportIndex]
* @param {Object} [action.layout]
* @param {Object} [action.viewportSpecificData]
* The definition of a viewport action.
*
* @typedef {Object} ViewportAction
* @property {string} type -
* @property {Object} data -
* @property {Object} layout -
* @property {number} viewportIndex -
* @property {Object} viewportSpecificData -
*/
const viewports = (state = defaultState, action) => {
let currentData;
let viewportSpecificData;
/**
* @param {Object} [state=DEFAULT_STATE] The current viewport state.
* @param {ViewportAction} action A viewport action.
*/
const viewports = (state = DEFAULT_STATE, action) => {
let useActiveViewport = false;
switch (action.type) {
case SET_VIEWPORT_ACTIVE:
return Object.assign({}, state, {
activeViewportIndex: action.viewportIndex,
});
/**
* Sets the active viewport index.
*
* @return {Object} New state.
*/
case SET_VIEWPORT_ACTIVE: {
return { ...state, activeViewportIndex: action.viewportIndex };
}
/**
* Sets viewport layout.
*
* @return {Object} New state.
*/
case SET_VIEWPORT_LAYOUT: {
const { numRows, numColumns, viewports } = action;
const layout = {
viewports: [...viewports],
return {
...state,
numRows: action.numRows,
numColumns: action.numColumns,
layout: { viewports: [...action.viewports] },
};
return Object.assign({}, state, { numRows, numColumns, layout });
}
/**
* Sets viewport layout and data.
*
* @return {Object} New state.
*/
case SET_VIEWPORT_LAYOUT_AND_DATA: {
const { numRows, numColumns, viewports, viewportSpecificData } = action;
const layout = {
viewports: [...viewports],
return {
...state,
numRows: action.numRows,
numColumns: action.numColumns,
layout: { viewports: [...action.viewports] },
viewportSpecificData: cloneDeep(action.viewportSpecificData),
};
return Object.assign({}, state, {
numRows,
numColumns,
layout,
viewportSpecificData: cloneDeep(viewportSpecificData),
});
}
/**
* Sets viewport specific data of active viewport.
*
* @return {Object} New state.
*/
case SET_VIEWPORT: {
const layout = cloneDeep(state.layout);
const hasPlugin = action.data && action.data.plugin;
viewportSpecificData = cloneDeep(state.viewportSpecificData);
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
viewportSpecificData[action.viewportIndex] = merge(
{},
viewportSpecificData[action.viewportIndex],
action.data
action.viewportSpecificData
);
if (hasPlugin) {
layout.viewports[action.viewportIndex].plugin = action.data.plugin;
if (action.viewportSpecificData && action.viewportSpecificData.plugin) {
layout.viewports[action.viewportIndex].plugin =
action.viewportSpecificData.plugin;
}
return Object.assign({}, state, { layout, viewportSpecificData });
return { ...state, layout, viewportSpecificData };
}
/**
* Sets viewport specific data of active/any viewport.
*
* @return {Object} New state.
*/
case SET_ACTIVE_SPECIFIC_DATA:
useActiveViewport = true;
// Allow fall-through
// eslint-disable-next-line
case SET_SPECIFIC_DATA: {
const layout = cloneDeep(state.layout);
const viewportIndex = useActiveViewport
? state.activeViewportIndex
: action.viewportIndex;
currentData = cloneDeep(state.viewportSpecificData[viewportIndex]) || {};
viewportSpecificData = cloneDeep(state.viewportSpecificData);
viewportSpecificData[viewportIndex] = merge({}, currentData, action.data);
return Object.assign({}, state, { viewportSpecificData });
}
case CLEAR_VIEWPORT:
viewportSpecificData = cloneDeep(state.viewportSpecificData);
if (action.viewportIndex) {
viewportSpecificData[action.viewportIndex] = {};
return Object.assign({}, state, { viewportSpecificData });
} else {
return defaultState;
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
viewportSpecificData[viewportIndex] = {
...action.viewportSpecificData,
};
if (action.viewportSpecificData && action.viewportSpecificData.plugin) {
layout.viewports[viewportIndex].plugin =
action.viewportSpecificData.plugin;
}
default:
return { ...state, layout, viewportSpecificData };
}
/**
* Clears viewport specific data of any viewport.
*
* @return {Object} New state.
*/
case CLEAR_VIEWPORT: {
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
if (action.viewportIndex) {
viewportSpecificData[action.viewportIndex] = {};
return { ...state, viewportSpecificData };
} else {
return DEFAULT_STATE;
}
}
/**
* Returns the current application state.
*
* @return {Object} The current state.
*/
default: {
return state;
}
}
};
@@ -60,7 +60,7 @@ describe('viewports reducer', () => {
const setViewportAction = {
type: types.SET_VIEWPORT,
viewportIndex: viewportToSet,
data: {
viewportSpecificData: {
hello: 'this is that data for the viewport',
world: 'that will be set for the viewportIndex',
},
@@ -69,7 +69,7 @@ describe('viewports reducer', () => {
const updatedState = reducer(undefined, setViewportAction);
const updatedViewport = updatedState.viewportSpecificData[viewportToSet];
expect(updatedViewport).toEqual(setViewportAction.data);
expect(updatedViewport).toEqual(setViewportAction.viewportSpecificData);
});
it('should handle CLEAR_VIEWPORT', () => {
@@ -0,0 +1,49 @@
import log from './../log.js';
export default class ServicesManager {
constructor() {
this.services = {};
this.registeredServiceNames = [];
}
/**
*
* @param {Object} service
*/
registerService(service) {
if (!service) {
log.warn(
'Attempting to register a null/undefined service. Exiting early.'
);
return;
}
let serviceName = service.name;
if (!serviceName) {
log.warn(`Service name not set. Exiting early.`);
return;
}
if (this.registeredServiceNames.includes(serviceName)) {
log.warn(
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
);
return;
}
this.services[service.name] = service;
// Track service registration
this.registeredServiceNames.push(serviceName);
}
/**
* An array of services.
*
* @param {Object[]} services - Array of services
*/
registerServices(services) {
services.forEach(service => this.registerService(service));
}
}
@@ -0,0 +1,70 @@
import ServicesManager from './ServicesManager.js';
import log from '../log.js';
jest.mock('./../log.js');
describe('ServicesManager.js', () => {
let servicesManager;
beforeEach(() => {
servicesManager = new ServicesManager();
log.warn.mockClear();
jest.clearAllMocks();
});
describe('registerServices()', () => {
it('calls registerService() for each service', () => {
servicesManager.registerService = jest.fn();
const fakeServices = [
{ name: 'UINotificationTestService', hide: jest.fn() },
{ name: 'UIModalTestService', hide: jest.fn() },
];
servicesManager.registerServices(fakeServices);
expect(servicesManager.registerService.mock.calls.length).toBe(2);
});
});
describe('registerService()', () => {
it('logs a warning if the service is null or undefined', () => {
const undefinedService = undefined;
const nullService = null;
servicesManager.registerService(undefinedService);
servicesManager.registerService(nullService);
expect(log.warn.mock.calls.length).toBe(2);
});
it('logs a warning if the service does not have a name', () => {
const serviceWithEmptyName = { name: '', hide: jest.fn() };
const serviceWithoutName = { hide: jest.fn() };
servicesManager.registerService(serviceWithEmptyName);
servicesManager.registerService(serviceWithoutName);
expect(log.warn.mock.calls.length).toBe(2);
});
it('tracks which services have been registered', () => {
const service = {
name: 'UINotificationService',
};
servicesManager.registerService(service);
expect(servicesManager.registeredServiceNames).toContain(service.name);
});
it('logs a warning if the service has an name that has already been registered', () => {
const service = { name: 'UINotificationService' };
servicesManager.registerService(service);
servicesManager.registerService(service);
expect(log.warn.mock.calls.length).toBe(1);
});
});
});
@@ -0,0 +1,119 @@
/**
* A UI Element Position
*
* @typedef {Object} ElementPosition
* @property {number} top -
* @property {number} left -
* @property {number} right -
* @property {number} bottom -
*/
/**
* UI Dialog
*
* @typedef {Object} DialogProps
* @property {string} id The dialog id.
* @property {ReactElement|HTMLElement} content The dialog content.
* @property {Object} contentProps The dialog content props.
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
* @property {boolean} [showOverlay=false] Controls dialog overlay.
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
* @property {Function} onStop Called when dragging stops.
* @property {Function} onDrag Called while dragging.
*/
const uiDialogServicePublicAPI = {
name: 'UIDialogService',
dismiss,
dismissAll,
create,
setServiceImplementation,
};
const uiDialogServiceImplementation = {
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
_create: () => console.warn('create() NOT IMPLEMENTED'),
};
function createUIDialogService() {
return uiDialogServicePublicAPI;
}
/**
* Show a new UI dialog;
*
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position }
*/
function create({
id,
content,
contentProps,
onStart,
onDrag,
onStop,
isDraggable = true,
showOverlay = false,
defaultPosition,
position,
}) {
return uiDialogServiceImplementation._create({
id,
content,
contentProps,
onStart,
onDrag,
onStop,
isDraggable,
showOverlay,
defaultPosition,
position,
});
}
/**
* Destroys all dialogs, if any
*
* @returns void
*/
function dismissAll() {
return uiDialogServiceImplementation._dismissAll();
}
/**
* Destroy the dialog, if currently created
*
* @returns void
*/
function dismiss({ id }) {
return uiDialogServiceImplementation._dismiss({ id });
}
/**
*
*
* @param {*} {
* dismiss: dismissImplementation,
* dismissAll: dismissAllImplementation,
* create: createImplementation,
* }
*/
function setServiceImplementation({
dismiss: dismissImplementation,
dismissAll: dismissAllImplementation,
create: createImplementation,
}) {
if (dismissImplementation) {
uiDialogServiceImplementation._dismiss = dismissImplementation;
}
if (dismissAllImplementation) {
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
}
if (createImplementation) {
uiDialogServiceImplementation._create = createImplementation;
}
}
export default createUIDialogService;
@@ -0,0 +1,84 @@
/**
* UI Modal
*
* @typedef {Object} ModalProps
* @property {ReactElement|HTMLElement} [content=null] Modal content.
* @property {Object} [contentProps=null] Modal content props.
* @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key.
* @property {boolean} [isOpen=true] Make the Modal visible or hidden.
* @property {boolean} [closeButton=true] Should the modal body render the close button.
* @property {string} [title=null] Should the modal render the title independently of the body content.
* @property {string} [customClassName=null] The custom class to style the modal.
*/
const uiModalServicePublicAPI = {
name: 'UIModalService',
hide,
show,
setServiceImplementation,
};
const uiModalServiceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUIModalService() {
return uiModalServicePublicAPI;
}
/**
* Show a new UI modal;
*
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
*/
function show({
content = null,
contentProps = null,
shouldCloseOnEsc = false,
isOpen = true,
closeButton = true,
title = null,
customClassName = null,
}) {
return uiModalServiceImplementation._show({
content,
contentProps,
shouldCloseOnEsc,
isOpen,
closeButton,
title,
customClassName,
});
}
/**
* Hides/dismisses the modal, if currently shown
*
* @returns void
*/
function hide() {
return uiModalServiceImplementation._hide();
}
/**
*
*
* @param {*} {
* hide: hideImplementation,
* show: showImplementation,
* }
*/
function setServiceImplementation({
hide: hideImplementation,
show: showImplementation,
}) {
if (hideImplementation) {
uiModalServiceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiModalServiceImplementation._show = showImplementation;
}
}
export default createUIModalService;
@@ -0,0 +1,84 @@
/**
* A UI Notification
*
* @typedef {Object} Notification
* @property {string} title -
* @property {string} message -
* @property {number} [duration=5000] - in ms
* @property {string} [position="bottomRight"] -"topLeft" | "topCenter | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
* @property {string} [type="info"] - "info" | "error" | "warning" | "success"
* @property {boolean} [autoClose=true]
*/
const uiNotificationServicePublicAPI = {
name: 'UINotificationService',
hide,
show,
setServiceImplementation,
};
const uiNotificationServiceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUINotificationService() {
return uiNotificationServicePublicAPI;
}
/**
* Create and show a new UI notification; returns the
* ID of the created notification.
*
* @param {Notification} notification { title, message, duration, position, type, autoClose}
* @returns {number} id
*/
function show({
title,
message,
duration = 5000,
position = 'bottomRight',
type = 'info',
autoClose = true,
}) {
return uiNotificationServiceImplementation._show({
title,
message,
duration,
position,
type,
autoClose,
});
}
/**
* Hides/dismisses the notification, if currently shown
*
* @param {number} id - id of the notification to hide/dismiss
* @returns undefined
*/
function hide(id) {
return uiNotificationServiceImplementation._hide({ id });
}
/**
*
*
* @param {*} {
* hide: hideImplementation,
* show: showImplementation,
* }
*/
function setServiceImplementation({
hide: hideImplementation,
show: showImplementation,
}) {
if (hideImplementation) {
uiNotificationServiceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiNotificationServiceImplementation._show = showImplementation;
}
}
export default createUINotificationService;
+11
View File
@@ -0,0 +1,11 @@
import ServicesManager from './ServicesManager.js';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
import createUIDialogService from './UIDialogService';
export {
createUINotificationService,
createUIModalService,
createUIDialogService,
ServicesManager,
};
@@ -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;
}
}
Loaded 100 of 329 files, more files were not shown because too many files have changed in this diff. Show more