Compare commits

..
Author SHA1 Message Date
ohif-bot b449f8eb0e chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.2
 - @ohif/extension-vtk@1.2.10
 - @ohif/core@2.3.5
 - @ohif/viewer@3.4.6
2020-01-28 12:41:49 +00:00
Erik Ziegler cf5160fb13 Add DICOM Group 6xxx Overlay Plane rendering (#1402)
* Add DICOM Group 6xxx Overlay Plane rendering

* Bump to cornerstone-tools 4.12.0 to support DICOM 6xxx Overlays
2020-01-28 13:40:03 +01:00
ohif-bot 4c1c91c006 chore(release): publish [skip ci]
- @ohif/viewer@3.4.5
2020-01-27 16:10:06 +00:00
Mirna Silva 9ed39c149b chore: Removing dependency ffmpeg-installer (#1397)
Fixes FOSSA warning/failure
2020-01-27 11:06:51 -05:00
ohif-bot b3f0fe029d chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.9
 - @ohif/core@2.3.4
 - @ohif/viewer@3.4.4
2020-01-27 09:14:56 +00:00
Erik Ziegler a1b78afdb9 Add palette color cache object back to core (fix #1350) (#1395) 2020-01-27 10:12:07 +01:00
Mirna Silva 3d3cfb1b41 Merge pull request #1393 from mirnasilva/fix_NewPatientSaveMeasurement
Fix on CI - Choosing another Patient Study
2020-01-24 16:17:10 -03:00
Mirna Silva b812fe03d2 Fix on CI - Changing to another Patient Study 2020-01-24 15:56:25 -03:00
ohif-bot 84f087bca4 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.8
 - @ohif/core@2.3.3
 - @ohif/viewer@3.4.3
2020-01-24 10:33:21 +00:00
Igor Octaviano a7e612eb88 refactor: 💡 Clean up JSON launch with DICOMWeb server (#1388)
* refactor: 💡 Clean up JSON launch with DICOMWeb server

Clean up JSON launch with DICOMWeb server

✅ Closes: #1387

* Remove test file

* parse seriesinstanceuids in query

* Add default studies

* Remove test file

* Cleanup retrievedata changes

* Set active server
2020-01-24 10:31:22 +00:00
Mirna Silva 9e3afda2f1 Merge pull request #1384 from mirnasilva/percy-ci-fix
Visual tests: Fixed ci config for visual tests (percy)
2020-01-20 11:13:24 -03:00
Mirna Silva 4d7a91a584 Merge branch 'master' into percy-ci-fix 2020-01-17 20:48:51 -03:00
Mirna Silva 1dc320edcd Fixed ci config for visual tests 2020-01-17 20:44:45 -03:00
ohif-bot 1d02f5485f chore(release): publish [skip ci]
- @ohif/viewer@3.4.2
2020-01-17 22:45:01 +00:00
Mirna Silva 4c9a977920 Merge pull request #1382 from OHIF/mirna/test/visual-tests
Percy fix for CI failure
2020-01-17 19:43:19 -03:00
Mirna Silva b5db09c25e Merge pull request #1383 from mirnasilva/percy-ci-fix
Percy ci fix
2020-01-17 19:14:16 -03:00
Mirna Silva 8dc4a5b82a Merge branch 'percy-ci-fix' of https://github.com/mirnasilva/Viewers into percy-ci-fix 2020-01-17 16:36:08 -03:00
Mirna Silva 44add9ce2f Fix for save measurements failure 2020-01-17 16:35:26 -03:00
Mirna Silva c19eceeede Merge pull request #1381 from mirnasilva/percy-ci-fix
Percy tests - fix for CI failure
2020-01-17 14:46:43 -03:00
Mirna Silva 72bc5a5ae5 Merge branch 'master' into percy-ci-fix 2020-01-17 14:45:13 -03:00
Mirna Silva dfdf46dbdd fix for CI failure 2020-01-17 14:31:53 -03:00
ohif-bot 571dff2c09 chore(release): publish [skip ci]
- @ohif/viewer@3.4.1
2020-01-15 14:16:43 +00:00
Gustavo André Lelis b9a66d4424 fix: 🐛 Metadata is being mistakenly purged (#1360)
Closes: #1326
2020-01-15 09:15:10 -05:00
ohif-bot 7ab2e3e60d chore(release): publish [skip ci]
- @ohif/viewer@3.4.0
2020-01-14 15:26:35 +00:00
mukhlin a5d6bc6a51 feat: Custom Healthcare API endpoint (#1367)
* Add ability to configure Google Cloud Healthcare API endpoint.

* Fixed bug in script.

* Fixed PR remark.

* Set Healthcare API endpoint in proper place.
2020-01-14 10:22:53 -05:00
ohif-bot df612ca4a0 chore(release): publish [skip ci]
- @ohif/viewer@3.3.8
2020-01-10 18:00:27 +00:00
Mirna Silva a91ce57c43 Merge pull request #1363 from OHIF/task/improveVisualTest
Task/improve visual tests. Related to PR #1327
2020-01-10 14:57:33 -03:00
Mirna Silva 342884f83e Merge pull request #1327 from mirnasilva/1320_ImproveVisualRegression
E2E test: Percy visual regression improvements
2020-01-10 14:30:01 -03:00
Mirna Silva 0fb39bad84 Changes on config file 2020-01-10 14:18:20 -03:00
Mirna Silva c74c80bb6f Fix for VTK test 2020-01-09 16:46:29 -03:00
Mirna Silva 910d5c65d5 Changing RELEASE config to run only percy tests 2020-01-09 16:17:22 -03:00
Mirna Silva d0fb4843ab Changing screenshot paths back to original 2020-01-09 15:29:39 -03:00
Mirna Silva f488b857fe Merge branch '1320_ImproveVisualRegression' of https://github.com/mirnasilva/Viewers into 1320_ImproveVisualRegression 2020-01-09 14:50:40 -03:00
Mirna Silva 256e8b1116 Merge branch 'master' into 1320_ImproveVisualRegression 2020-01-09 14:49:42 -03:00
Mirna Silva 168dd87747 Merge branch 'master' of https://github.com/mirnasilva/Viewers into 1320_ImproveVisualRegression 2020-01-08 15:21:43 -03:00
ohif-bot 13c41e6d86 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.7
 - @ohif/ui@1.1.8
 - @ohif/viewer@3.3.7
2020-01-08 13:48:10 +00:00
barhantasandDanny Brown 7ab0bbb325 fix: measurements panel css and delete button visibility (#1352)
Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-01-08 08:43:30 -05:00
ohif-bot 2213187b14 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.6
 - @ohif/viewer@3.3.6
2020-01-07 22:44:05 +00:00
Maher Jendoubi 4c4d8a61b6 chore: Fix a typo (#1353) 2020-01-07 17:41:16 -05:00
Mirna Silva 67f58f5f09 Added dependency ffmpeg-installer 2020-01-07 19:09:04 -03:00
Mirna Silva dfc35885bb Changed record to false 2020-01-07 18:28:16 -03:00
Mirna Silva e9c449d1db Changed path for cypress/videos 2020-01-07 18:07:20 -03:00
Mirna Silva 6dac50c7c6 Improvements on VTK visual tests 2 2020-01-07 17:23:35 -03:00
Mirna Silva 9be54f6164 Improvements on VTK visual tests 2020-01-07 16:52:28 -03:00
Mirna Silva 341cc5a4eb Merge branch 'master' into 1320_ImproveVisualRegression 2020-01-06 19:07:20 -03:00
Mirna Silva 7b80171203 Removing unnecessary screenshots 2020-01-06 18:54:09 -03:00
ohif-bot 2e5a385e53 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.5
 - @ohif/core@2.3.2
 - @ohif/viewer@3.3.5
2020-01-06 21:29:37 +00:00
Danny Brown a3caeb381e chore: remove unused file; duplicated by studyInstanceHelpers (#1349)
* chore: remove unused file; duplicated by studyInstanceHelpers

* Change expected study list results num
2020-01-06 16:26:57 -05:00
Mirna Silva ab0af86379 Merge branch '1320_ImproveVisualRegression' of https://github.com/mirnasilva/Viewers into 1320_ImproveVisualRegression 2020-01-06 17:09:24 -03:00
Mirna Silva 86426cb024 Changed command CI line for diff test paths 2020-01-06 17:08:00 -03:00
Mirna Silva 348840ef26 Merge branch 'master' of https://github.com/mirnasilva/Viewers into 1320_ImproveVisualRegression 2020-01-06 16:20:11 -03:00
Mirna Silva 52ef934961 Merge branch 'master' into 1320_ImproveVisualRegression 2020-01-06 14:48:30 -03:00
ohif-bot be995f93b6 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.4
 - @ohif/core@2.3.1
 - @ohif/viewer@3.3.4
2019-12-30 20:01:54 +00:00
Danny Brown 5036615e00 chore: throw our error if we encounter one (no swallow) (#1337) 2019-12-30 14:53:39 -05:00
Mirna Silva 4ee2b135a7 Added more tests and updated CI config.yml 2019-12-24 01:17:02 -03:00
Mirna Silva 388fe04e09 E2E test: Percy visua regression improvements 2019-12-23 23:10:55 -03:00
Gustavo André Lelis 2e215aed4c test: 💍 Updating tests after bug fix (#1324)
Closes: #1167
2019-12-20 19:14:07 -05:00
ohif-bot ffd27d6db4 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.1
 - @ohif/extension-vtk@1.2.3
 - @ohif/viewer@3.3.3
2019-12-20 21:01:26 +00:00
Gustavo André LelisandDanny Brown 6540e36818 fix: 🐛 1241: Make Plugin switch part of ToolbarModule (#1322)
* fix: 🐛 Make Plugin switch part of ToolbarModule

Closes: #1241

* Move setCornerstoneViewport command into cornerstone extension

* Exposing extensionManager into extension's get modules functions

* Remove Plugin switch

* Updating set cornerstonelayout to work on VIEWER context

* Creating a customComponent for 2D MPR button

* Use extensionManager to show or not buttons
Adding isHidden option into toolbarModules

* Make buttons hidden depending on extension registration

* Fix unit test after changes on extensionManager

* Remove workaround for extensionManager

* Use studyMeatadataManager and remove TODO's

* Remove storeContext not needed

* Moving Exit 2D MPR into cornerstone extension with VTK context

* Remove PluginSwitch files

* Fix E2E tests

* Adding cypress screenshots into gitignore

* Remove extensionManager from getModuleFn functions and moving CustomComponent for VTK button to VTK extension

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 15:59:36 -05:00
ohif-bot a436436def chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.2
 - @ohif/ui@1.1.7
 - @ohif/viewer@3.3.2
2019-12-20 19:10:48 +00:00
Mirna Silva d6d92afbda test: Refactoring toolbar data-cy selectors (#1323) 2019-12-20 14:07:00 -05:00
ohif-bot e1142910e4 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.1
 - @ohif/ui@1.1.6
 - @ohif/viewer@3.3.1
2019-12-20 18:57:38 +00:00
Mirna Silva 067e6c769b test: Save Measurements Functionality (#1300)
* E2E Tests: Save Measurements Functionality

* Moving test file to PWA only. Script-tag doesn't have the required Study

* Removing data-cy selector from cornerstone toolbar
2019-12-20 13:54:59 -05:00
ohif-bot d375d02a4b chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.0
 - @ohif/extension-vtk@1.2.0
 - @ohif/core@2.3.0
 - @ohif/viewer@3.3.0
2019-12-20 17:18:56 +00:00
Igor OctavianoandDanny Brown 63594d36b0 feat: 🎸 Configuration so viewer tools can nix handles (#1304)
* feat: 🎸 Configuration so viewer tools can nix handles

Adds a key to cornerstone configuration that allows to toggle on/off
handle rendering

Closes: #1223

* Update extension docs

* Update doc

* docs: include hideHandles configuration in our configuration docs

* chore: default to hiding handles for annotations

* chore: switch to drawHandlesOnHover to mimic legacy viewer functionality

* Flip logic for drawHandlesOnHover; reverse of hideHandles

* Don't hide the handles of ellipse or rectangle roi

* invert check

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 12:15:40 -05:00
ohif-bot 6c160f517f chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.7
 - @ohif/ui@1.1.5
 - @ohif/viewer@3.2.2
2019-12-19 15:15:49 +00:00
Gustavo André Lelis 23305cec9c fix: 🐛 Fix drag-n-drop of local files into OHIF (#1319)
* fix: 🐛 Fix drag-n-drop of local files into OHIF

Closes: #1307

* CR Update: Centralize dialogs
2019-12-19 10:14:05 -05:00
ohif-bot 36a741f0bc chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.6
 - @ohif/core@2.2.1
 - @ohif/viewer@3.2.1
2019-12-18 18:29:28 +00:00
Igor Octaviano 695ad732e1 refactor: 💡 Move DICOMSR code to @ohif/core project (#1310)
* Move DICOMSR to @ohif/core

* refactor: 💡 Move DICOMSR code to @ohif/core project

We've just merged support for DICOM Structured Reports. Today, support
for this feature lives in the @ohif/viewer project in it's lib folder.
Ideally, it should be moved to @ohif/core and given a clear public API
and unit tests.

Closes: #1282

* Update imports

* CR Update: Update imports
2019-12-18 13:25:53 -05:00
ohif-bot 73a3f07e2b chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.5
 - @ohif/core@2.2.0
 - @ohif/viewer@3.2.0
2019-12-16 19:11:59 +00:00
Igor Octaviano 4ea239a953 feat: 🎸 Expose extension config to modules (#1279)
* feat: 🎸 Expose extension config to modules

Currently, only the preRegistration hook receives the extension's
configuration as a parameter. Providing getModuleFn's with the
extension's configuration, and all lifecycle/modules with the
application's configuration as rootConfig should open the doors to more
configurable extensions.

Closes: #1268

* CR Update: Pass extension and service config through extension manager preinit/getmodule

* CR Update: Remove appConfig from servicesManager

* CR Update: Remove appconfig variable
2019-12-16 14:07:53 -05:00
ohif-bot 625d5d70a7 chore(release): publish [skip ci]
- @ohif/viewer@3.1.12
2019-12-16 18:30:12 +00:00
Igor Octaviano 5c3d8b37b6 fix: 🐛 Dismiss all dialogs if leaving viewer route #1242 (#1301)
* fix: 🐛 #1242

Dismiss all dialogs if route change

Closes: #1242

* CR Update: Dismiss dialogs in videwer
2019-12-16 13:26:31 -05:00
ohif-bot 8bbbf3a889 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.1.1
 - @ohif/viewer@3.1.11
2019-12-16 16:49:50 +00:00
arthur-miller d5bf72851a fix: 🐛 add WwwcRegionTool to cornerstone tools initialization (#1302) 2019-12-16 11:47:02 -05:00
ohif-bot 2914c83032 chore(release): publish [skip ci]
- @ohif/extension-dicom-pdf@1.0.1
 - @ohif/viewer@3.1.10
2019-12-16 16:13:00 +00:00
Igor Octaviano 31e542dfd9 fix: 🐛 Update ohif pdf extension to optionally use pdfjs (#1162)
* fix: 🐛 Update ohif pdf extension to optionally use pdfjs

Update ohif pdf extension of optionally use pdfjs

Closes: #1049

* Update branch and fix scrolling

* Dynamically import component

* Fix wrong file change

* Fix import

* Add multi opened pdf configuration

* Refactor connected component

* Specify worker explicitly

* Specify worker explicitly

* CR Update: Remove dead code

* CR Update: Refactor state

* CR Update: use refs

* CR Update: use refs
2019-12-16 11:10:10 -05:00
ohif-bot 25599a2e2a chore(release): publish [skip ci]
- @ohif/i18n@0.52.4
 - @ohif/viewer@3.1.9
2019-12-16 15:13:14 +00:00
Danny Brown 1cf7015511 docs: stub environment variables docs 2019-12-16 09:47:12 -05:00
Igor Octaviano 756f70cc54 refactor: 💡 Use env variables to set locize (#1299)
Instead of relying in dynamic configuration, use dedicated env variables
to set locize api key / configuration

Closes: #1271
2019-12-16 09:41:41 -05:00
ohif-bot ded2e61913 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.4
 - @ohif/core@2.1.1
 - @ohif/ui@1.1.4
 - @ohif/viewer@3.1.8
2019-12-16 14:28:33 +00:00
Igor Octaviano b58aa4575a refactor: Remove LabelingFlow/ContextMenu services and reimplement on top of the Dialog service #1264 (#1292)
* Remove ContextMenu and Labelling services and provider to use DialogService directly

* Refactor init to display labelling

* Cleanup labelling flow in measurements init

* Cleanup viewer imports

* Add classname to toolcontextmenu

* Add error message
2019-12-16 09:19:40 -05:00
Danny Brown f78abe2afa docs: stub viewer configuration 2019-12-16 09:15:10 -05:00
Danny Brown f48157117f docs: hide empty pages 2019-12-16 09:00:24 -05:00
Danny Brown 6f8bd0dff8 docs: fix scope of diagram path 2019-12-16 08:57:43 -05:00
ohif-bot 943af89df2 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.3
 - @ohif/ui@1.1.3
 - @ohif/viewer@3.1.7
2019-12-13 15:14:42 +00:00
Danny Brown cd2da349e5 fix: allow empty values for dimensions (#1295)
* fix: allow empty values for dimensions

* fix: consistently use `defaultSize` for intial values
2019-12-13 10:09:18 -05:00
ohif-bot 14e86dcddd chore(release): publish [skip ci]
- @ohif/viewer@3.1.6
2019-12-13 04:45:56 +00:00
Danny Brown 2c8b93656d ci: fix webgl e2e test failures (only on CI) (#1296)
* ci: update executor and browser flag for circleci orbs

* ci: browser flag when running percy/ci tests

* ci: limit ourselves to the executors defined by the cypress orb for now
2019-12-12 23:43:22 -05:00
ohif-bot 6932468206 chore(release): publish [skip ci]
- @ohif/viewer@3.1.5
2019-12-13 02:19:44 +00:00
Danny Brown 7bc9efb0f5 ci: store cypress videos as artifacts; make sure we upload them to dashboard
* fix: Set VTK viewport as active by interaction

* chore: upgrade cypress to latest

* Revert "fix: Set VTK viewport as active by interaction"

This reverts commit b6e174c2def9d8f688a54d32053679788b9aaf76.

* ci: add record flag for master cypress runs

* ci: simplify cypress orb tasks now that cypress@3.8 uses electron w/ chromium 78

* ci: remove video false flag

* ci: drop browser flag

* ci: store video artifacts
2019-12-12 21:17:35 -05:00
ohif-bot 5d47cc43c1 chore(release): publish [skip ci]
- @ohif/viewer@3.1.4
2019-12-13 01:26:39 +00:00
Danny Brown e46a11a01e ci: upgrade cypress version (#1293)
* fix: Set VTK viewport as active by interaction

* chore: upgrade cypress to latest

* Revert "fix: Set VTK viewport as active by interaction"

This reverts commit b6e174c2def9d8f688a54d32053679788b9aaf76.

* ci: add record flag for master cypress runs
2019-12-12 20:21:27 -05:00
ohif-bot 27a950d956 chore(release): publish [skip ci]
- @ohif/viewer@3.1.3
2019-12-12 18:11:14 +00:00
Danny Brown 03d4c10adc chore: remove unused file 2019-12-12 13:08:30 -05:00
Mirna Silva 7a0c369006 test: PT/CT json, studies, series, frames, metadatas (#1145)
* Tests for routes: PT/CT json, studies, series, frames, metadatas

* Fix based on reviews and JSON file for PT/CT Study added

* chore: revert to remote JSON file

* chore: delete PTCTStudy.json file

We can circle back to this approach when/if we have issues. No need to hold-up this PR. My fault on this one.
2019-12-12 01:05:45 -05:00
ohif-bot 8c946a4742 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.2
 - @ohif/i18n@0.52.3
 - @ohif/ui@1.1.2
 - @ohif/viewer@3.1.2
2019-12-12 05:54:34 +00:00
Rodrigo Antinarelli 30b9e44220 fix: translations (#1234) 2019-12-12 00:46:58 -05:00
ohif-bot 380abe2705 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.1
 - @ohif/ui@1.1.1
 - @ohif/viewer@3.1.1
2019-12-11 16:48:26 +00:00
Mirna Silva f4570c96c2 tests: Download Image File Modal (#1219)
* E2E Tests: Download Image File Modal

* CI fix

* Fixes for PR comments

* Small change just to check CI behavior

* Fix for CI failure: comparing classes instead of image src

* Fixed Download button test and blocked Download Dialog event

* Deleting file ConnectedViewportDownloadForm.js

This file was not supposed to be created by this PR. It was already deleted by another user on Master branch.

* Removing wrong import line

* Fixes based on PR comments

* Fixes based on PR comments

* test: remove assignment to window
2019-12-11 11:46:10 -05:00
ohif-bot 1fe951a71e chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.1.0
 - @ohif/extension-dicom-html@1.1.0
 - @ohif/extension-vtk@1.1.0
 - @ohif/core@2.1.0
 - @ohif/ui@1.1.0
 - @ohif/viewer@3.1.0
2019-12-11 16:18:54 +00:00
Gustavo André Lelis ebe1af8d4f feat: 🎸 DICOM SR STOW on MeasurementAPI (#954)
* feat: 🎸 DICOM SR STOW on MeasurementAPI

Closes: #758

* feat: 🎸 Adding the dataExchance from DICOM SR

* feat: 🎸 Fix some issues and get the retrieve measurements working

* feat: 🎸 Preventing measurement tool helpers to broke because the data is not Number (it comes as string from dcmjs)

* feat: 🎸 Adding a button to start testing SATE functionality

* feat: 🎸Small fixes into the code

* feat: 🎸 MAke sure we get latest SR in the study, even on lazyLoad mode

* Revert "feat: 🎸 Preventing measurement tool helpers to broke because the data is not Number (it comes as string from dcmjs)"

This reverts commit 4b3a1efb5810487d2319826fd59c03d779882b5e.

* feat: 🎸 Make sure after sync we update the viewports and also that annotation tools are set to passive, so it can be drawn in the viewports

* feat: 🎸 Remove console logs and start using OHIF.log

* feat: 🎸 Refactor measurement table to use Snackbar service of messages and make sure we only show save button if server type is dicomWeb

* feat: 🎸 Refactor to remove mocked wadoRoot and use server

* Fix to expose error once we can't save the SR

* Update DCMJS into latest dev version

* Update dcmjs with latest fixes

* Fix PR callouts

* Logging error on MeasurementReport creation

* Preventing from breaking on non supported tools and also warning that some toolType is not sopported

* Small error message update

* Expose better the errors and when we have unsopported tools into the measurements

* Renaming variable

* Expose dcmjs tool support check

* Change the way we warn the unsupportedTools

* Adding style to the button

* Small change on button style

* Fix git conflicts

* Fix after merge with latest master

* Fix after merge with master, btn was not clickable

* Fixing callouts

* Adding util functions into files and refactor with JSDocs

* Refactoring code, adding some basic JSDocs

* Bump dcmjs with latest version and fixes

* JSDocs and small refactors

* yarn lock file after dcmjs update

* Remove unused parameters

* Registering a plain copy of server into measurementAPI

* Small refactor

* Start changing from toolType to toolName

* Rename a function to get instanceMetadata

* Adding error if server is not DICOMWeb on retrieve

* Move snackbar outside of UI component and use UINotificationService

* Refactor and JSDocs

* Small fixes on JSDocs (style)

* Remove unused import

* Improve JSDocs on MeasurementApi

* Remove refactor leftover

* Improve JSDocs and propTypes

* JSDocs and small refactors
2019-12-11 11:14:01 -05:00
141 changed files with 3830 additions and 3030 deletions

No files matched your search

+16 -28
View File
@@ -13,14 +13,7 @@ version: 2.1
## ##
orbs: orbs:
codecov: codecov/codecov@1.0.5 codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@1.11.0 cypress: cypress-io/cypress@1.13.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 defaults: &defaults
docker: docker:
@@ -335,12 +328,15 @@ workflows:
working_directory: platform/viewer working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
wait-on: 'http://localhost:3000' wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}' cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace no-workspace: true # Don't persist workspace
post-steps: post-steps:
- store_artifacts: - store_artifacts:
path: platform/viewer/cypress/screenshots path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires: requires:
- UNIT_TESTS - UNIT_TESTS
# E2E: script-tag # E2E: script-tag
@@ -357,13 +353,15 @@ workflows:
working_directory: platform/viewer working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*' spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
wait-on: 'http://localhost:3000' wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}' cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace no-workspace: true # Don't persist workspace
post-steps: post-steps:
- store_artifacts: - store_artifacts:
path: platform/viewer/cypress/screenshots path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires: requires:
- UNIT_TESTS - UNIT_TESTS
@@ -375,6 +373,7 @@ workflows:
- cypress/run: - cypress/run:
name: 'Generate Percy Snapshots' name: 'Generate Percy Snapshots'
executor: cypress/browsers-chrome76 executor: cypress/browsers-chrome76
browser: chrome
pre-steps: pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest add wait-on' # Use yarn latest
@@ -389,6 +388,8 @@ workflows:
post-steps: post-steps:
- store_artifacts: - store_artifacts:
path: platform/viewer/cypress/screenshots path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires: requires:
- AWAIT_APPROVAL - AWAIT_APPROVAL
@@ -445,9 +446,11 @@ workflows:
branches: branches:
only: master only: master
# Update base branch snapshots # Update base branch snapshots
# and record a Cypress dashboard test run
- cypress/run: - cypress/run:
name: 'Generate Percy Snapshots' name: 'Generate Percy Snapshots'
executor: cypress/browsers-chrome76 executor: cypress/browsers-chrome76
browser: chrome
pre-steps: pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest add wait-on' # Use yarn latest
@@ -462,28 +465,13 @@ workflows:
post-steps: post-steps:
- store_artifacts: - store_artifacts:
path: platform/viewer/cypress/screenshots path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
- store_test_results:
path: platform/viewer/cypress/results
filters: filters:
branches: branches:
only: master only: master
- DOCKER_MASTER_PUBLISH: - DOCKER_MASTER_PUBLISH:
requires: requires:
- NPM_PUBLISH - 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
+23 -9
View File
@@ -1,17 +1,31 @@
#!/bin/bash #!/bin/bash
# If CLIENT_ID is specified, use the google.js configuration with the modified ID if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
if [ ! -z "$CLIENT_ID" ]
then then
echo "Google Cloud Healthcare $CLIENT_ID has been provided: " # If CLIENT_ID is specified, use the google.js configuration with the modified ID
echo $CLIENT_ID if [ -n "$CLIENT_ID" ]
echo "Updating config..." then
echo "Google Cloud Healthcare \$CLIENT_ID has been provided: "
echo "$CLIENT_ID"
echo "Updating config..."
# - Use SED to replace the CLIENT_ID that is currently in google.js # - Use SED to replace the CLIENT_ID that is currently in google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
fi
# - Copy google.js to overwrite app-config.js # If HEALTHCARE_API_ENDPOINT is specified, use the google.js configuration with the modified endpoint
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js if [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
echo "Google Cloud Healthcare \$HEALTHCARE_API_ENDPOINT has been provided: "
echo "$HEALTHCARE_API_ENDPOINT"
echo "Updating config..."
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
sed -i -e "s+https://healthcare.googleapis.com/v1beta1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
fi
# - Copy google.js to overwrite app-config.js
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
fi fi
echo "Starting Nginx to serve the OHIF Viewer..." echo "Starting Nginx to serve the OHIF Viewer..."
+1
View File
@@ -30,6 +30,7 @@ docker/dcm4che/dcm4che-arc
# Cypress test results # Cypress test results
videos/ videos/
screenshots/
# Locize settings # Locize settings
.locize .locize
+5
View File
@@ -72,12 +72,17 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG), 'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''), 'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'), 'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''), 'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM), 'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
}), }),
], ],
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881 // Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
-2
View File
@@ -1,7 +1,6 @@
# OHIF Viewers # OHIF Viewers
- [Our Process](our-process.md) - [Our Process](our-process.md)
- [Quick Start](quick-start.md)
- Development - Development
- [Getting Started](development/getting-started.md) - [Getting Started](development/getting-started.md)
- [Contributing](development/contributing.md) - [Contributing](development/contributing.md)
@@ -31,7 +30,6 @@
- [OHIF Maintained](extensions/index.md#maintained-extensions) - [OHIF Maintained](extensions/index.md#maintained-extensions)
- [Services](services/index.md) - [Services](services/index.md)
- [Default](services/default/index.md) - [Default](services/default/index.md)
- [Measurements](services/default/measurements.md)
- [UI](services/ui/index.md) - [UI](services/ui/index.md)
- [Dialog Service](services/ui/ui-dialog-service.md) - [Dialog Service](services/ui/ui-dialog-service.md)
- [Modal Service](services/ui/ui-modal-service.md) - [Modal Service](services/ui/ui-modal-service.md)
+5 -3
View File
@@ -193,7 +193,8 @@ today, create a GitHub issue!
The `ExtensionManager` is a class made available to us via the `@ohif/core` The `ExtensionManager` is a class made available to us via the `@ohif/core`
project (platform/core). Our application instantiates a single instance of it, project (platform/core). Our application instantiates a single instance of it,
and provides a `ServicesManager` and `CommandsManager`. and provides a `ServicesManager` and `CommandsManager` along with the application's
configuration through the appConfig key (optional).
```js ```js
const commandsManager = new CommandsManager(); const commandsManager = new CommandsManager();
@@ -201,6 +202,7 @@ const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({ const extensionManager = new ExtensionManager({
commandsManager, commandsManager,
servicesManager, servicesManager,
appConfig
}); });
``` ```
@@ -210,8 +212,8 @@ The `ExtensionManager` only has a few public members:
- `registerExtensions` - Registers an array of extensions - `registerExtensions` - Registers an array of extensions
- `modules` - An object containing registered extensions by `MODULE_TYPE` - `modules` - An object containing registered extensions by `MODULE_TYPE`
During registration, lifecycle hooks and modules have access to During registration, lifecycle hooks and modules have access to the extension's config,
`ExtensionManager`'s `ServicesManager` and `CommandsManager`. the application's config and `ExtensionManager`'s `ServicesManager` and `CommandsManager` instances.
Our `@ohif/viewer` uses the `modules` member to access registered extensions at Our `@ohif/viewer` uses the `modules` member to access registered extensions at
appropriate places in our application. appropriate places in our application.
+1 -1
View File
@@ -13,7 +13,7 @@ like user preferences and previous query paramaters. Because of this, the Viewer
has been built to be highly configurable to work with almost any web accessible has been built to be highly configurable to work with almost any web accessible
data source. data source.
![scope-of-project diagram](./assets/img/scope-of-project.png) ![scope-of-project diagram](./../assets/img/scope-of-project.png)
To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files. To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files.
These can be delivered to your end users however you would like: These can be delivered to your end users however you would like:
+65 -1
View File
@@ -1,3 +1,67 @@
# Viewer: Configuration # Viewer: Configuration
... We maintain a number of common viewer application configurations at
[`<root>/platform/viewer/public/configs`][config-dir]. How these values are
passed to the viewer depend on how it's deployed, but the two most common paths
are:
- `index.html` looks for `https://your-website.com/app-config.js` OR
- `index.html` passes the values to `OHIF.installViewer()`
```js
window.config = {
routerBasename: '/',
whiteLabelling: {},
extensions: [],
showStudyList: true,
filterQueryParam: false,
servers: {
dicomWeb: [
{
name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
},
],
},
// Supported Keys: https://craig.is/killing/mice
hotkeys: [
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportVertical',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportHorizontal',
label: 'Flip Vertically',
keys: ['v'],
},
],
/* Configuration passed to the bundled cornerstone extension
*
* The cornerstone extension is currently tightly coupled to the platform.
* Until we're able to decouple it, this key will serve as a workaround to
* pass it configuration.
*/
cornerstoneExtensionConfig: {
/* Whether to show/hide annotation "handles" */
hideHandles: true,
},
};
```
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[config-dir]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
<!-- prettier-ignore-end -->
@@ -0,0 +1,23 @@
# Environment Variables
There are a number of environment variables we use at build time to influence the output application's behavior.
```bash
# Application
NODE_ENV=< production | development >
DEBUG=< true | false >
APP_CONFIG=< relative path to application configuration file >
PUBLIC_URL=<>
VERSION_NUMBER=<Set by CircleCI>
BUILD_NUM=<Set by CircleCI>
# i18n
USE_LOCIZE=<false>
LOCIZE_PROJECTID=<ProjectID to pull translations for>
LOCIZE_API_KEY=<To enable Locize live editing of translations>
```
## Setting Environment Variables
- `npx cross-env`
- `.env` files
- env variables on build machine, or for terminal session
+1
View File
@@ -14,6 +14,7 @@ export default {
preRegistration({ preRegistration({
servicesManager = {}, servicesManager = {},
commandsManager = {}, commandsManager = {},
appConfig = {},
configuration = {}, configuration = {},
}) {}, }) {},
+52
View File
@@ -3,6 +3,58 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.1...@ohif/extension-cornerstone@2.2.2) (2020-01-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.0...@ohif/extension-cornerstone@2.2.1) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.1...@ohif/extension-cornerstone@2.2.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.0...@ohif/extension-cornerstone@2.1.1) (2019-12-16)
### Bug Fixes
* 🐛 add WwwcRegionTool to cornerstone tools initialization ([#1302](https://github.com/OHIF/Viewers/issues/1302)) ([d5bf728](https://github.com/OHIF/Viewers/commit/d5bf72851a32dff9fd3fc09332ea5250bc7e6114))
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11) ## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-cornerstone **Note:** Version bump only for package @ohif/extension-cornerstone
+14
View File
@@ -85,6 +85,20 @@ Tools can be configured through extension configuration using the tools key:
... ...
``` ```
## Annotate Tools Configuration
*We currently support one property for annotation tools.*
### Hide handles
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
```js
...
cornerstoneExtensionConfig: {
hideHandles: true,
},
...
## Resources ## Resources
### Repositories ### Repositories
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone", "name": "@ohif/extension-cornerstone",
"version": "2.0.2", "version": "2.2.2",
"description": "OHIF extension for Cornerstone", "description": "OHIF extension for Cornerstone",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -32,9 +32,9 @@
"@ohif/ui": "^0.50.0", "@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.12.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
@@ -2,6 +2,7 @@ import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools'; import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core'; import OHIF from '@ohif/core';
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
import { getEnabledElement } from './state'; import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm'; import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll'); const scroll = cornerstoneTools.import('util/scroll');
@@ -242,6 +243,9 @@ const commandsModule = ({ servicesManager }) => {
cornerstoneTools.removeToolState(element, toolType, tool); cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element); cornerstone.updateImage(element);
}, },
setCornerstoneLayout: () => {
setCornerstoneLayout();
}
}; };
const definitions = { const definitions = {
@@ -348,6 +352,12 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [], storeContexts: [],
options: {}, options: {},
}, },
setCornerstoneLayout: {
commandFn: actions.setCornerstoneLayout,
storeContexts: [],
options: {},
context: 'VIEWER',
},
}; };
return { return {
+80 -28
View File
@@ -1,10 +1,10 @@
import OHIF from '@ohif/core'; import OHIF from '@ohif/core';
import { SimpleDialog } from '@ohif/ui';
import cornerstone from 'cornerstone-core'; import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools'; import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
import queryString from 'query-string';
import { SimpleDialog } from '@ohif/ui';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
import queryString from 'query-string';
import initCornerstoneTools from './initCornerstoneTools.js';
function fallbackMetaDataProvider(type, imageId) { function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) { if (!imageId.includes('wado?requestType=WADO')) {
@@ -76,30 +76,36 @@ export default function init({ servicesManager, configuration }) {
initCornerstoneTools(defaultCsToolsConfig); initCornerstoneTools(defaultCsToolsConfig);
// ~~ Toooools 🙌 const toolsGroupedByType = {
const tools = [ touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
csTools.PanTool, annotations: [
csTools.ZoomTool, csTools.ArrowAnnotateTool,
csTools.WwwcTool, csTools.EraserTool,
csTools.MagnifyTool, csTools.BidirectionalTool,
csTools.StackScrollTool, csTools.LengthTool,
csTools.StackScrollMouseWheelTool, csTools.AngleTool,
// Touch csTools.FreehandRoiTool,
csTools.PanMultiTouchTool, csTools.EllipticalRoiTool,
csTools.ZoomTouchPinchTool, csTools.DragProbeTool,
// Annotations csTools.RectangleRoiTool,
csTools.ArrowAnnotateTool, ],
csTools.EraserTool, segmentation: [csTools.BrushTool],
csTools.BidirectionalTool, other: [
csTools.LengthTool, csTools.PanTool,
csTools.AngleTool, csTools.ZoomTool,
csTools.FreehandRoiTool, csTools.WwwcTool,
csTools.EllipticalRoiTool, csTools.WwwcRegionTool,
csTools.DragProbeTool, csTools.MagnifyTool,
csTools.RectangleRoiTool, csTools.StackScrollTool,
// Segmentation csTools.StackScrollMouseWheelTool,
csTools.BrushTool, csTools.OverlayTool,
]; ],
};
let tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
/* Add extension tools configuration here. */ /* Add extension tools configuration here. */
const internalToolsConfig = { const internalToolsConfig = {
@@ -113,20 +119,66 @@ export default function init({ servicesManager, configuration }) {
}, },
}; };
/* Abstract tools configuration using extension configuration. */
const parseToolProps = (props, tool) => {
const { annotations } = toolsGroupedByType;
// An alternative approach would be to remove the `drawHandlesOnHover` config
// from the supported configuration properties in `cornerstone-tools`
const toolsWithHideableHandles = annotations.filter(
tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name)
);
let parsedProps = { ...props };
/**
* drawHandles - Never/Always show handles
* drawHandlesOnHover - Only show handles on handle hover (pointNearHandle)
*
* Does not apply to tools where handles aren't placed in predictable
* locations.
*/
if (
configuration.hideHandles !== false &&
toolsWithHideableHandles.includes(tool)
) {
if (props.configuration) {
parsedProps.configuration.drawHandlesOnHover = true;
} else {
parsedProps.configuration = { drawHandlesOnHover: true };
}
}
return parsedProps;
};
/* Add tools with its custom props through extension configuration. */ /* Add tools with its custom props through extension configuration. */
tools.forEach(tool => { tools.forEach(tool => {
const toolName = tool.name.replace('Tool', ''); const toolName = tool.name.replace('Tool', '');
const externalToolsConfig = configuration.tools || {}; const externalToolsConfig = configuration.tools || {};
const externalToolProps = externalToolsConfig[toolName] || {}; const externalToolProps = externalToolsConfig[toolName] || {};
const internalToolProps = internalToolsConfig[toolName] || {}; const internalToolProps = internalToolsConfig[toolName] || {};
const props = merge(internalToolProps, externalToolProps); const props = merge(
internalToolProps,
parseToolProps(externalToolProps, tool)
);
csTools.addTool(tool, props); csTools.addTool(tool, props);
}); });
// TODO -> We need a better way to do this with maybe global tool state setting all tools passive.
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
tools.forEach(tool => {
if (tool.prototype instanceof BaseAnnotationTool) {
// BaseAnnotationTool would likely come from csTools lib exports
const toolName = new tool().name;
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
csTools.setToolActive('Pan', { mouseButtonMask: 4 }); csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 }); csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 }); csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required
csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
csTools.setToolActive('ZoomTouchPinch', {}); csTools.setToolActive('ZoomTouchPinch', {});
csTools.setToolEnabled('Overlay', {});
} }
@@ -237,6 +237,15 @@ const definitions = [
}, },
], ],
}, },
{
id: 'Exit2DMPR',
label: 'Exit 2D MPR',
icon: 'times',
//
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'setCornerstoneLayout',
context: 'ACTIVE_VIEWPORT::VTK',
}
]; ];
export default { export default {
@@ -0,0 +1,22 @@
import { redux } from '@ohif/core';
const { setLayout } = redux.actions;
/**
* Update the current layout with a simple Cornerstone one
*
* @return void
*/
const setCornerstoneLayout = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
const action = setLayout(layout);
window.store.dispatch(action);
}
export default setCornerstoneLayout;
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [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) ## [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 **Note:** Version bump only for package @ohif/extension-dicom-html
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-dicom-html", "name": "@ohif/extension-dicom-html",
"version": "1.0.2", "version": "1.1.0",
"description": "OHIF extension for rendering structured reports to HTML", "description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -29,7 +29,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6" "react-dom": "^16.8.6"
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.0...@ohif/extension-dicom-pdf@1.0.1) (2019-12-16)
### Bug Fixes
* 🐛 Update ohif pdf extension to optionally use pdfjs ([#1162](https://github.com/OHIF/Viewers/issues/1162)) ([31e542d](https://github.com/OHIF/Viewers/commit/31e542dfd973c3e982ede6d02d79bfe65175e792)), closes [#1049](https://github.com/OHIF/Viewers/issues/1049)
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.4...@ohif/extension-dicom-pdf@1.0.0) (2019-09-27) # [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.4...@ohif/extension-dicom-pdf@1.0.0) (2019-09-27)
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-dicom-pdf", "name": "@ohif/extension-dicom-pdf",
"version": "1.0.0", "version": "1.0.1",
"description": "OHIF extension for Dicom PDF", "description": "OHIF extension for Dicom PDF",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -37,7 +37,8 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"lodash.isequal": "^4.5.0" "lodash.isequal": "^4.5.0",
"pdfjs-dist": "^2.2.228"
}, },
"gitHead": "a5baa9228c0eda0df880136bde4420d78e6f8706" "gitHead": "a5baa9228c0eda0df880136bde4420d78e6f8706"
} }
@@ -0,0 +1,27 @@
import OHIF from '@ohif/core';
import { connect } from 'react-redux';
import OHIFDicomPDFViewport from './OHIFDicomPDFViewport';
const { setViewportActive } = OHIF.redux.actions;
const mapStateToProps = (state, ownProps) => {
const { activeViewportIndex } = state.viewports;
return { activeViewportIndex };
};
const mapDispatchToProps = (dispatch, ownProps) => {
const { viewportIndex } = ownProps;
return {
setViewportActive: () => {
dispatch(setViewportActive(viewportIndex));
},
};
};
const ConnectedOHIFDicomPDFViewer = connect(
mapStateToProps,
mapDispatchToProps
)(OHIFDicomPDFViewport);
export default ConnectedOHIFDicomPDFViewer;
@@ -0,0 +1,62 @@
.DicomPDFViewport {
--header-height: 50px;
}
.DicomPDFViewport #toolbar {
display: flex;
align-items: center;
color: #fff;
padding: 0.5em;
border-bottom: var(--ui-border-thickness) solid var(--ui-border-color);
height: var(--header-height);
}
.DicomPDFViewport #canvas {
height: calc(100% - var(--header-height));
overflow-y: scroll;
}
.DicomPDFViewport #text-layer {
height: 100%;
margin-top: var(--header-height);
}
.DicomPDFViewport #pdf-canvas-container {
position: relative;
}
.DicomPDFViewport canvas, #text-layer {
display: block;
margin: 0 auto;
overflow: hidden;
}
.DicomPDFViewport #toolbar button {
color: currentColor;
background-color: transparent;
font: inherit;
border: var(--ui-border-thickness) solid var(--ui-border-color);
border-radius: 3px;
padding: 0.25em 0.5em;
margin-right: 0.5em;
cursor: pointer;
}
.DicomPDFViewport #text-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1;
}
.DicomPDFViewport #text-layer>span {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
+197 -23
View File
@@ -1,6 +1,14 @@
import React, { Component } from 'react'; import React, { Component, createRef } from 'react';
import dicomParser from 'dicom-parser'; import dicomParser from 'dicom-parser';
import PDFJS from 'pdfjs-dist';
import PropTypes from 'prop-types';
import TypedArrayProp from './TypedArrayProp'; import TypedArrayProp from './TypedArrayProp';
import './DicomPDFViewport.css';
import pdfjsBuild from 'pdfjs-dist/build/pdf';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
pdfjsBuild.GlobalWorkerOptions.workerSrc = pdfjsWorker;
// TODO: Should probably use dcmjs for this // TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = { const SOP_CLASS_UIDS = {
@@ -8,16 +16,88 @@ const SOP_CLASS_UIDS = {
}; };
class DicomPDFViewport extends Component { class DicomPDFViewport extends Component {
state = { constructor(props) {
fileURL: null, super(props);
error: null,
}; this.state = {
fileURL: null,
error: null,
currentPageIndex: 1,
pdf: null,
scale: 1,
};
this.canvas = createRef();
this.textLayer = createRef();
}
static propTypes = { static propTypes = {
byteArray: TypedArrayProp.uint8, byteArray: TypedArrayProp.uint8,
useNative: PropTypes.bool,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
setViewportActive: PropTypes.func,
viewportIndex: PropTypes.number,
}; };
renderPDF = (dataSet, byteArray) => { static defaultProps = {
useNative: false,
};
async componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
const fileURL = this.getPDFFileUrl(dataSet, this.props.byteArray);
this.setState(state => ({ ...state, fileURL }));
if (!this.props.useNative) {
const pdf = await PDFJS.getDocument(fileURL).promise;
this.setState(state => ({ ...state, pdf }), () => this.updatePDFCanvas());
}
}
updatePDFCanvas = async () => {
const { pdf, scale, currentPageIndex } = this.state;
const context = this.canvas.getContext('2d');
const page = await pdf.getPage(currentPageIndex);
let viewport = page.getViewport({ scale });
this.canvas.height = viewport.height;
this.canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
};
await page.render(renderContext);
const textContent = await page.getTextContent();
this.textLayer.innerHTML = '';
this.textLayer.style.height = viewport.height + 'px';
this.textLayer.style.width = viewport.width + 'px';
PDFJS.renderTextLayer({
textContent,
container: this.textLayer,
viewport,
textDivs: [],
});
};
componentDidUpdate(prevProps, prevState) {
const { currentPageIndex, scale } = this.state;
const newValidScale = prevState.scale !== scale && scale > 0;
const newValidPageNumber =
prevState.currentPageIndex !== currentPageIndex && currentPageIndex > 0;
if (newValidScale || newValidPageNumber) {
this.updatePDFCanvas();
}
}
getPDFFileUrl = (dataSet, byteArray) => {
let pdfByteArray = byteArray; let pdfByteArray = byteArray;
if (dataSet) { if (dataSet) {
@@ -36,50 +116,144 @@ class DicomPDFViewport extends Component {
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' }); const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(PDF); const fileURL = URL.createObjectURL(PDF);
this.setState({ return fileURL;
fileURL, };
});
onPageChange = async event => {
const { currentPageIndex, pdf } = this.state;
let newPageIndex = currentPageIndex;
const action = event.target.getAttribute('data-pager');
if (action === 'prev') {
if (currentPageIndex === 1) {
return;
}
newPageIndex -= 1;
if (currentPageIndex < 0) {
newPageIndex = 0;
}
}
if (action === 'next') {
if (currentPageIndex === pdf.numPages - 1) {
return;
}
newPageIndex += 1;
if (currentPageIndex > pdf.numPages - 1) {
newPageIndex = pdf.numPages - 1;
}
}
this.setState(state => ({ ...state, currentPageIndex: newPageIndex }));
};
onZoomChange = () => {
let newZoomValue = this.state.scale;
const action = event.target.getAttribute('data-pager');
if (action === '+') {
newZoomValue += 0.25;
}
if (action === '-') {
newZoomValue -= 0.25;
}
this.setState(state => ({ ...state, scale: newZoomValue }));
}; };
parseByteArray = byteArray => { parseByteArray = byteArray => {
const options = { const options = { untilTag: '' };
untilTag: '',
};
let dataSet; let dataSet;
try { try {
dataSet = dicomParser.parseDicom(byteArray, options); dataSet = dicomParser.parseDicom(byteArray, options);
} catch (error) { } catch (error) {
this.setState({ this.setState(state => ({ ...state, error }));
error,
});
} }
return dataSet; return dataSet;
}; };
componentDidMount() { setViewportActiveHandler = () => {
const dataSet = this.parseByteArray(this.props.byteArray); const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
this.renderPDF(dataSet, this.props.byteArray); if (viewportIndex !== activeViewportIndex) {
} setViewportActive(viewportIndex);
}
};
downloadPDFCanvas = () => {
const { fileURL } = this.state;
const a = document.createElement('a');
a.href = fileURL;
a.download = fileURL.substr(fileURL.lastIndexOf('/') + 1);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
render() { render() {
const { fileURL, pdf, error } = this.state;
return ( return (
<div <div
className={'DicomPDFViewport'} className={'DicomPDFViewport'}
onClick={this.setViewportActiveHandler}
onScroll={this.setViewportActiveHandler}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
> >
{this.state.fileURL && ( {!this.props.useNative ? (
<>
<div id="toolbar">
<div id="pager">
{pdf && pdf.numPages > 1 && (
<>
<button data-pager="prev" onClick={this.onPageChange}>
{`<`}
</button>
<button data-pager="next" onClick={this.onPageChange}>
{`>`}
</button>
</>
)}
<button data-pager="-" onClick={this.onZoomChange}>
{`-`}
</button>
<button data-pager="+" onClick={this.onZoomChange}>
{`+`}
</button>
<button onClick={this.downloadPDFCanvas}>Download</button>
</div>
</div>
<div id="canvas">
<div id="pdf-canvas-container">
<canvas
id="pdf-canvas"
ref={canvas => (this.canvas = canvas)}
/>
<div
id="text-layer"
ref={textLayer => (this.textLayer = textLayer)}
></div>
</div>
</div>
</>
) : (
<object <object
data={this.state.fileURL} aria-label="PDF Viewer"
data={fileURL}
type="application/pdf" type="application/pdf"
width="100%" width="100%"
height="100%" height="100%"
/> />
)} )}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>} {error && <h2>{JSON.stringify(error)}</h2>}
</div> </div>
); );
} }
@@ -11,6 +11,9 @@ class OHIFDicomPDFViewport extends Component {
studies: PropTypes.object, studies: PropTypes.object,
displaySet: PropTypes.object, displaySet: PropTypes.object,
viewportIndex: PropTypes.number, viewportIndex: PropTypes.number,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
setViewportActive: PropTypes.func,
}; };
state = { state = {
@@ -30,42 +33,36 @@ class OHIFDicomPDFViewport extends Component {
componentDidMount() { componentDidMount() {
const { displaySet, studies } = this.props.viewportData; const { displaySet, studies } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders,
} = displaySet;
DicomLoaderService.findDicomDataPromise(displaySet, studies).then( DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => { data => this.setState({ byteArray: new Uint8Array(data) }),
const byteArray = new Uint8Array(data);
this.setState({
byteArray: byteArray,
});
},
error => { error => {
this.setState({ this.setState({ error });
error,
});
throw new Error(error); throw new Error(error);
} }
); );
} }
render() { render() {
const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
const { byteArray, error } = this.state;
const { id, init, destroy } = OHIFDicomPDFViewport; const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy }; const pluginProps = { id, init, destroy };
return ( return (
<OHIFComponentPlugin {...pluginProps}> <OHIFComponentPlugin {...pluginProps}>
{this.state.byteArray && ( {byteArray && (
<DicomPDFViewport byteArray={this.state.byteArray} /> <DicomPDFViewport
byteArray={byteArray}
setViewportActive={setViewportActive}
viewportIndex={viewportIndex}
activeViewportIndex={activeViewportIndex}
/>
)} )}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>} {error && <h2>{JSON.stringify(error)}</h2>}
</OHIFComponentPlugin> </OHIFComponentPlugin>
); );
} }
@@ -0,0 +1,37 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
+9 -4
View File
@@ -1,16 +1,21 @@
import asyncComponent from './asyncComponent.js';
import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js'; import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js';
import OHIFDicomPDFViewport from './OHIFDicomPDFViewport.js';
const ConnectedOHIFDicomPDFViewer = asyncComponent(() =>
import(
/* webpackChunkName: "ConnectedOHIFDicomPDFViewer" */ './ConnectedOHIFDicomPDFViewer'
)
);
export default { export default {
/** /**
* Only required property. Should be a unique value across all extensions. * Only required property. Should be a unique value across all extensions.
*/ */
id: 'pdf', id: 'pdf',
getViewportModule() { getViewportModule() {
return OHIFDicomPDFViewport; return ConnectedOHIFDicomPDFViewer;
}, },
getSopClassHandlerModule() { getSopClassHandlerModule() {
return OHIFDicomPDFSopClassHandler; return OHIFDicomPDFSopClassHandler;
} },
}; };
+161
View File
@@ -3,6 +3,167 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.9...@ohif/extension-vtk@1.2.10) (2020-01-28)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.8...@ohif/extension-vtk@1.2.9) (2020-01-27)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.7...@ohif/extension-vtk@1.2.8) (2020-01-24)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.6...@ohif/extension-vtk@1.2.7) (2020-01-08)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.5...@ohif/extension-vtk@1.2.6) (2020-01-07)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.4...@ohif/extension-vtk@1.2.5) (2020-01-06)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.3...@ohif/extension-vtk@1.2.4) (2019-12-30)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.2...@ohif/extension-vtk@1.2.3) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.1...@ohif/extension-vtk@1.2.2) (2019-12-20)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.0...@ohif/extension-vtk@1.2.1) (2019-12-20)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.7...@ohif/extension-vtk@1.2.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.6...@ohif/extension-vtk@1.1.7) (2019-12-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.5...@ohif/extension-vtk@1.1.6) (2019-12-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.4...@ohif/extension-vtk@1.1.5) (2019-12-16)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.3...@ohif/extension-vtk@1.1.4) (2019-12-16)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.2...@ohif/extension-vtk@1.1.3) (2019-12-13)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.1...@ohif/extension-vtk@1.1.2) (2019-12-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.0...@ohif/extension-vtk@1.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.2...@ohif/extension-vtk@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11) ## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk **Note:** Version bump only for package @ohif/extension-vtk
+5 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-vtk", "name": "@ohif/extension-vtk",
"version": "1.0.2", "version": "1.2.10",
"description": "OHIF extension for VTK.js", "description": "OHIF extension for VTK.js",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -34,7 +34,7 @@
"@ohif/ui": "^0.50.0", "@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"i18next": "^17.0.3", "i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
@@ -52,11 +52,10 @@
"react-vtkjs-viewport": "^0.3.9" "react-vtkjs-viewport": "^0.3.9"
}, },
"devDependencies": { "devDependencies": {
"@ohif/core": "^2.0.2", "@ohif/core": "^2.3.5",
"@ohif/ui": "^1.0.1", "@ohif/ui": "^1.1.8",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.12.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1", "gh-pages": "^2.0.1",
"i18next": "^17.0.3", "i18next": "^17.0.3",
+1 -1
View File
@@ -234,7 +234,7 @@ const commandsModule = ({ commandsManager }) => {
}, },
}, },
{ {
// Sagital // Sagittal
orientation: { orientation: {
sliceNormal: [1, 0, 0], sliceNormal: [1, 0, 0],
viewUp: [0, 0, 1], viewUp: [0, 0, 1],
@@ -0,0 +1,87 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { ToolbarButton } from '@ohif/ui';
import { utils } from '@ohif/core';
const { studyMetadataManager } = utils;
let isVisible = true;
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
if (!viewportSpecificData[activeViewportIndex]) {
return false;
};
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
activeViewportIndex
];
const studies = studyMetadataManager.all();
const study = studies.find(
study => study.studyInstanceUID === studyInstanceUid
);
if (!study) {
return false;
}
const displaySet = study._displaySets.find(set => set.displaySetInstanceUid === displaySetInstanceUid);
if (!displaySet) {
return false;
};
return displaySet.isReconstructable;
};
function VTKMPRToolbarButton({
parentContext,
toolbarClickCallback,
button,
activeButtons,
isActive,
className,
}) {
const { id, label, icon } = button;
const { viewportSpecificData, activeViewportIndex } = useSelector(state => {
const { viewports = {} } = state;
const { viewportSpecificData, activeViewportIndex } = viewports;
return {
viewportSpecificData,
activeViewportIndex,
}
});
isVisible = _isDisplaySetReconstructable(
viewportSpecificData,
activeViewportIndex,
);
return (
<React.Fragment>
{isVisible && (
<ToolbarButton
key={id}
label={label}
icon={icon}
onClick={evt => toolbarClickCallback(button, evt)}
isActive={isActive}
/>
)}
</React.Fragment>
);
}
VTKMPRToolbarButton.propTypes = {
parentContext: PropTypes.object.isRequired,
toolbarClickCallback: PropTypes.func.isRequired,
button: PropTypes.object.isRequired,
activeButtons: PropTypes.array.isRequired,
isActive: PropTypes.bool,
className: PropTypes.string,
};
export default VTKMPRToolbarButton;
+12 -1
View File
@@ -1,4 +1,5 @@
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent'; import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
import VTKMPRToolbarButton from './toolbarComponents/VTKMPRToolbarButton';
const TOOLBAR_BUTTON_TYPES = { const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command', COMMAND: 'command',
@@ -115,9 +116,19 @@ const definitions = [
}, },
], ],
}, },
{
id: '2DMPR',
label: '2D MPR',
icon: 'cube',
//
CustomComponent: VTKMPRToolbarButton,
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'mpr2d',
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
},
]; ];
export default { export default {
definitions, definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK', defaultContext: 'ACTIVE_VIEWPORT::VTK',
}; }
+92
View File
@@ -3,6 +3,98 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.3.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.4...@ohif/core@2.3.5) (2020-01-28)
**Note:** Version bump only for package @ohif/core
## [2.3.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.3...@ohif/core@2.3.4) (2020-01-27)
**Note:** Version bump only for package @ohif/core
## [2.3.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.2...@ohif/core@2.3.3) (2020-01-24)
**Note:** Version bump only for package @ohif/core
## [2.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.1...@ohif/core@2.3.2) (2020-01-06)
**Note:** Version bump only for package @ohif/core
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.0...@ohif/core@2.3.1) (2019-12-30)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.1...@ohif/core@2.3.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.0...@ohif/core@2.2.1) (2019-12-18)
**Note:** Version bump only for package @ohif/core
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.1...@ohif/core@2.2.0) (2019-12-16)
### Features
* 🎸 Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268)
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.0...@ohif/core@2.1.1) (2019-12-16)
**Note:** Version bump only for package @ohif/core
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.2...@ohif/core@2.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11) ## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/core **Note:** Version bump only for package @ohif/core
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/core", "name": "@ohif/core",
"version": "2.0.2", "version": "2.3.5",
"description": "Generic business logic for web-based medical imaging applications", "description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team", "author": "OHIF Core Team",
"license": "MIT", "license": "MIT",
@@ -32,13 +32,14 @@
}, },
"peerDependencies": { "peerDependencies": {
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.12.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3" "dicom-parser": "^1.8.3"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"dcmjs": "^0.8.3",
"dicomweb-client": "^0.5.2", "dicomweb-client": "^0.5.2",
"isomorphic-base64": "^1.0.2", "isomorphic-base64": "^1.0.2",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
+80
View File
@@ -0,0 +1,80 @@
import log from '../log';
import studies from '../studies';
import utils from '../utils';
import {
retrieveMeasurementFromSR,
stowSRFromMeasurements,
} from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
/**
*
* @typedef serverType
* @property {string} type - type of the server
* @property {string} wadoRoot - server wado root url
*
*/
/**
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
*
* @param {serverType} server
* @returns {Promise} Should resolve with OHIF measurementData object
*/
const retrieveMeasurements = server => {
log.info('[DICOMSR] retrieveMeasurements');
if (!server || server.type !== 'dicomWeb') {
log.error('[DICOMSR] DicomWeb server is required!');
return Promise.reject({});
}
const serverUrl = server.wadoRoot;
const studies = utils.studyMetadataManager.all();
const latestSeries = findMostRecentStructuredReport(studies);
if (!latestSeries) return Promise.resolve({});
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
};
/**
* Function to be registered into MeasurementAPI to store measurements into DICOM Structured Reports
*
* @param {Object} measurementData - OHIF measurementData object
* @param {Object} filter
* @param {serverType} server
* @returns {Object} With message to be displayed on success
*/
const storeMeasurements = async (measurementData, filter, server) => {
log.info('[DICOMSR] storeMeasurements');
if (!server || server.type !== 'dicomWeb') {
log.error('[DICOMSR] DicomWeb server is required!');
return Promise.reject({});
}
const serverUrl = server.wadoRoot;
const firstMeasurementKey = Object.keys(measurementData)[0];
const firstMeasurement = measurementData[firstMeasurementKey][0];
const studyInstanceUid =
firstMeasurement && firstMeasurement.studyInstanceUid;
try {
await stowSRFromMeasurements(measurementData, serverUrl);
if (studyInstanceUid) {
studies.deleteStudyMetadataPromise(studyInstanceUid);
}
return {
message: 'Measurements were saved with success',
};
} catch (error) {
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
throw new Error('Error while saving the measurements.');
}
};
export { retrieveMeasurements, storeMeasurements };
@@ -0,0 +1,84 @@
import * as dcmjs from 'dcmjs';
import { api } from 'dicomweb-client';
import DICOMWeb from '../DICOMWeb';
import parseDicomStructuredReport from './parseDicomStructuredReport';
import parseMeasurementsData from './parseMeasurementsData';
import getAllDisplaySets from './utils/getAllDisplaySets';
const VERSION_NAME = 'dcmjs-0.0';
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
/**
* Function to retrieve measurements from DICOM Structured Reports coming from determined server
*
* @param {Array} series - List of all series metaData loaded
* @param {Array} studies - List of all studies metaData loaded
* @param {string} serverUrl - Server URL to be used on request
* @returns {Object} MeasurementData
*/
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
};
const dicomWeb = new api.DICOMwebClient(config);
const instance = series.getFirstInstance();
const options = {
studyInstanceUID: instance.getStudyInstanceUID(),
seriesInstanceUID: instance.getSeriesInstanceUID(),
sopInstanceUID: instance.getSOPInstanceUID(),
};
const part10SRArrayBuffer = await dicomWeb.retrieveInstance(options);
const displaySets = getAllDisplaySets(studies);
const measurementsData = parseDicomStructuredReport(
part10SRArrayBuffer,
displaySets
);
return measurementsData;
};
/**
* Function to store measurements to DICOM Structured Reports in determined server
*
* @param {Object} measurements - OHIF measurementData object
* @param {string} serverUrl - Server URL to be used on request
* @returns {Promise}
*/
const stowSRFromMeasurements = async (measurements, serverUrl) => {
const { dataset } = parseMeasurementsData(measurements);
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const meta = {
FileMetaInformationVersion: dataset._meta.FileMetaInformationVersion.Value,
MediaStorageSOPClassUID: dataset.SOPClassUID,
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
TransferSyntaxUID: TRANSFER_SYNTAX_UID,
ImplementationClassUID: DicomMetaDictionary.uid(),
ImplementationVersionName: VERSION_NAME,
};
const denaturalized = DicomMetaDictionary.denaturalizeDataset(meta);
const dicomDict = new DicomDict(denaturalized);
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset);
const part10Buffer = dicomDict.write();
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
datasets: [part10Buffer],
};
await dicomWeb.storeInstances(options);
};
export { retrieveMeasurementFromSR, stowSRFromMeasurements };
+10
View File
@@ -0,0 +1,10 @@
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
import isToolSupported from './utils/isToolSupported';
const DICOMSR = {
retrieveMeasurements,
storeMeasurements,
isToolSupported,
};
export default DICOMSR;
@@ -0,0 +1,95 @@
import * as dcmjs from 'dcmjs';
import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBySopInstanceUid';
/**
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
* (e.g. measurement table)
*
* @param {ArrayBuffer} part10SRArrayBuffer
* @param {Array} displaySets
* @returns
*/
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
// Get the dicom data as an Object
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const storedMeasurementByToolType = MeasurementReport.generateToolState(
dataset
);
const measurementData = {};
let measurementNumber = 0;
Object.keys(storedMeasurementByToolType).forEach(toolName => {
const measurements = storedMeasurementByToolType[toolName];
measurementData[toolName] = [];
measurements.forEach(measurement => {
const instanceMetadata = findInstanceMetadataBySopInstanceUid(
displaySets,
measurement.sopInstanceUid
);
const { _study: study, _series: series } = instanceMetadata;
const { studyInstanceUid, patientId } = study;
const { seriesInstanceUid } = series;
const { sopInstanceUid, frameIndex } = measurement;
const imagePath = getImagePath(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frameIndex
);
const imageId = instanceMetadata.getImageId();
if (!imageId) {
return;
}
// TODO: We need the currentTimepointID set into the viewer
const currentTimepointId = 'TimepointId';
const toolData = Object.assign({}, measurement, {
imageId,
imagePath,
seriesInstanceUid,
studyInstanceUid,
patientId,
measurementNumber: ++measurementNumber,
timepointId: currentTimepointId,
toolType: toolName,
_id: imageId + measurementNumber,
});
measurementData[toolName].push(toolData);
});
});
return measurementData;
};
/**
* Function to create imagePath with all imageData related
*
* @param {string} studyInstanceUid
* @param {string} seriesInstanceUid
* @param {string} sopInstanceUid
* @param {string} frameIndex
* @returns
*/
const getImagePath = (
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frameIndex
) => {
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join(
'_'
);
};
export default parseDicomStructuredReport;
@@ -0,0 +1,57 @@
import * as dcmjs from 'dcmjs';
import cornerstone from 'cornerstone-core';
import log from '../log';
import measurements from '../measurements';
import isToolSupported from './utils/isToolSupported';
/**
* Function to parse OHIF viewer measurementData into a dcmjs MeasurementReport
*
* @param {Object} measurementsData - OHIF measurementData object
* @returns {Object} Dataset: measurement report from dcmjs
*/
const parseMeasurementsData = measurementsData => {
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const { getImageIdForImagePath } = measurements;
const toolState = {};
const unsupportedTools = [];
Object.keys(measurementsData).forEach(measurementType => {
const annotations = measurementsData[measurementType];
annotations.forEach(annotation => {
const { toolType, imagePath } = annotation;
if (isToolSupported(toolType)) {
const imageId = getImageIdForImagePath(imagePath);
toolState[imageId] = toolState[imageId] || {};
toolState[imageId][toolType] = toolState[imageId][toolType] || {
data: [],
};
toolState[imageId][toolType].data.push(annotation);
} else {
unsupportedTools.push(toolType);
}
});
});
if (unsupportedTools.length > 0) {
log.warn(
`[DICOMSR] Tooltypes not supported: ${unsupportedTools.join(', ')}`
);
}
const report = MeasurementReport.generateReport(
toolState,
cornerstone.metaData
);
return {
dataset: report.dataset,
};
};
export default parseMeasurementsData;
@@ -0,0 +1,24 @@
/**
* Should Find the requested instance metadata into the displaySets and return
*
* @param {Array} displaySets - List of displaySets
* @param {string} sopInstanceUid - sopInstanceUID to look for
* @returns {Object} - instance metadata found
*/
const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
let instanceFound;
displaySets.find(displaySet => {
if (!displaySet.images) return false;
instanceFound = displaySet.images.find(
instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid
);
return !!instanceFound;
});
return instanceFound;
};
export default findInstanceMetadataBySopInstanceUid;
@@ -0,0 +1,60 @@
/**
* Should find the most recent Structured Report metadata
*
* @param {Array} studies
* @returns {Object} Series
*/
const findMostRecentStructuredReport = studies => {
let mostRecentStructuredReport;
studies.forEach(study => {
const allSeries = study.getSeries ? study.getSeries() : [];
allSeries.forEach(series => {
if (isStructuredReportSeries(series)) {
if (
!mostRecentStructuredReport ||
compareSeriesDate(series, mostRecentStructuredReport)
) {
mostRecentStructuredReport = series;
}
}
});
});
return mostRecentStructuredReport;
};
/**
* Checks if series sopClassUID matches with the supported Structured Reports sopClassUID
*
* @param {Object} series - Series metadata
* @returns {boolean}
*/
const isStructuredReportSeries = series => {
const supportedSopClassUIDs = [
'1.2.840.10008.5.1.4.1.1.88.22',
'1.2.840.10008.5.1.4.1.1.11.1',
];
const firstInstance = series.getFirstInstance();
const sopClassUid = firstInstance._instance.sopClassUid;
return supportedSopClassUIDs.includes(sopClassUid);
};
/**
* Checkes if series1 is newer than series2
*
* @param {Object} series1 - Series Metadata 1
* @param {Object} series2 - Series Metadata 2
* @returns {boolean} true/false if series1 is newer than series2
*/
const compareSeriesDate = (series1, series2) => {
return (
series1._data.seriesDate > series2._data.seriesDate ||
(series1._data.seriesDate === series2._data.seriesDate &&
series1._data.seriesTime > series2._data.seriesTime)
);
};
export default findMostRecentStructuredReport;
@@ -0,0 +1,19 @@
/**
* Retrieve a list of all displaySets of all studies
*
* @param {Object} studies - List of studies loaded into the viewer
* @returns {Object} List of DisplaySets
*/
const getAllDisplaySets = studies => {
let allDisplaySets = [];
studies.forEach(study => {
if (study.getDisplaySets) {
allDisplaySets = allDisplaySets.concat(study.getDisplaySets());
}
});
return allDisplaySets;
};
export default getAllDisplaySets;
@@ -0,0 +1,14 @@
import * as dcmjs from 'dcmjs';
/**
* Checks if dcmjs has support to determined tool
*
* @param {string} toolName
* @returns {boolean}
*/
const isToolSupported = toolName => {
const adapter = dcmjs.adapters.Cornerstone;
return !!adapter[toolName];
};
export default isToolSupported;
@@ -2,13 +2,14 @@ import MODULE_TYPES from './MODULE_TYPES.js';
import log from './../log.js'; import log from './../log.js';
export default class ExtensionManager { export default class ExtensionManager {
constructor({ commandsManager, servicesManager }) { constructor({ commandsManager, servicesManager, appConfig = {} }) {
this.modules = {}; this.modules = {};
this.registeredExtensionIds = []; this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES); this.moduleTypeNames = Object.values(MODULE_TYPES);
// //
this._commandsManager = commandsManager; this._commandsManager = commandsManager;
this._servicesManager = servicesManager; this._servicesManager = servicesManager;
this._appConfig = appConfig;
this.moduleTypeNames.forEach(moduleType => { this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = []; this.modules[moduleType] = [];
@@ -70,6 +71,7 @@ export default class ExtensionManager {
extension.preRegistration({ extension.preRegistration({
servicesManager: this._servicesManager, servicesManager: this._servicesManager,
commandsManager: this._commandsManager, commandsManager: this._commandsManager,
appConfig: this._appConfig,
configuration, configuration,
}); });
} }
@@ -79,7 +81,8 @@ export default class ExtensionManager {
const extensionModule = this._getExtensionModule( const extensionModule = this._getExtensionModule(
moduleType, moduleType,
extension, extension,
extensionId extensionId,
configuration
); );
if (extensionModule) { if (extensionModule) {
@@ -102,7 +105,7 @@ export default class ExtensionManager {
* @param {Object} extension * @param {Object} extension
* @param {string} extensionId - Used for logging warnings * @param {string} extensionId - Used for logging warnings
*/ */
_getExtensionModule(moduleType, extension, extensionId) { _getExtensionModule(moduleType, extension, extensionId, configuration) {
const getModuleFnName = 'get' + _capitalizeFirstCharacter(moduleType); const getModuleFnName = 'get' + _capitalizeFirstCharacter(moduleType);
const getModuleFn = extension[getModuleFnName]; const getModuleFn = extension[getModuleFnName];
@@ -114,6 +117,8 @@ export default class ExtensionManager {
const extensionModule = getModuleFn({ const extensionModule = getModuleFn({
servicesManager: this._servicesManager, servicesManager: this._servicesManager,
commandsManager: this._commandsManager, commandsManager: this._commandsManager,
appConfig: this._appConfig,
configuration,
}); });
if (!extensionModule) { if (!extensionModule) {
@@ -6,7 +6,7 @@ import log from './../log.js';
jest.mock('./../log.js'); jest.mock('./../log.js');
describe('ExtensionManager.js', () => { describe('ExtensionManager.js', () => {
let extensionManager, commandsManager; let extensionManager, commandsManager, servicesManager, appConfig;
beforeEach(() => { beforeEach(() => {
commandsManager = { commandsManager = {
@@ -14,7 +14,17 @@ describe('ExtensionManager.js', () => {
getContext: jest.fn(), getContext: jest.fn(),
registerCommand: jest.fn(), registerCommand: jest.fn(),
}; };
extensionManager = new ExtensionManager({ commandsManager }); servicesManager = {
registerService: jest.fn(),
};
appConfig = {
testing: true,
};
extensionManager = new ExtensionManager({
servicesManager,
commandsManager,
appConfig,
});
log.warn.mockClear(); log.warn.mockClear();
jest.clearAllMocks(); jest.clearAllMocks();
}); });
@@ -51,7 +61,7 @@ describe('ExtensionManager.js', () => {
extensionManager.registerExtensions(fakeExtensions); extensionManager.registerExtensions(fakeExtensions);
// Assert // Assert
expect(extensionManager.registerExtension.mock.calls[1]).toContain( expect(extensionManager.registerExtension.mock.calls[1][1]).toEqual(
fakeConfiguration fakeConfiguration
); );
}); });
@@ -67,19 +77,19 @@ describe('ExtensionManager.js', () => {
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1); expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
}); });
it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => { it('calls preRegistration() passing dependencies and extension configuration to extension', () => {
const configuration = { config: 'Some configuration' }; const extensionConfiguration = { config: 'Some configuration' };
extensionManager._servicesManager = { services: { TestService: {} } };
// SUT // SUT
const fakeExtension = { one: '1', preRegistration: jest.fn() }; const extension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(fakeExtension, configuration); extensionManager.registerExtension(extension, extensionConfiguration);
// Assert // Assert
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({ expect(extension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager, servicesManager,
commandsManager: extensionManager._commandsManager, commandsManager,
configuration, appConfig,
configuration: extensionConfiguration,
}); });
}); });
@@ -153,8 +163,8 @@ describe('ExtensionManager.js', () => {
); );
}); });
it('successfully passes a servicesManager and commandsManager instances to each module', () => { it('successfully passes dependencies to each module along with extension configuration', () => {
extensionManager._servicesManager = { services: { TestService: {} } }; const extensionConfiguration = { testing: true };
const extension = { const extension = {
id: 'hello-world', id: 'hello-world',
@@ -165,11 +175,17 @@ describe('ExtensionManager.js', () => {
getCommandsModule: jest.fn(), getCommandsModule: jest.fn(),
}; };
extensionManager.registerExtension(extension); extensionManager.registerExtension(extension, extensionConfiguration);
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({ Object.keys(extension).forEach(module => {
servicesManager: extensionManager._servicesManager, if (typeof extension[module] === 'function') {
commandsManager: extensionManager._commandsManager, expect(extension[module].mock.calls[0][0]).toEqual({
servicesManager,
commandsManager,
appConfig,
configuration: extensionConfiguration,
});
}
}); });
}); });
@@ -210,7 +226,7 @@ describe('ExtensionManager.js', () => {
return { return {
definitions: { definitions: {
exampleDefinition: { exampleDefinition: {
commandFn: () => {}, commandFn: () => { },
storeContexts: [], storeContexts: [],
options: {}, options: {},
}, },
+3 -6
View File
@@ -5,6 +5,7 @@ import { ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes/'; import classes, { CommandsManager, HotkeysManager } from './classes/';
import DICOMWeb from './DICOMWeb'; import DICOMWeb from './DICOMWeb';
import DICOMSR from './DICOMSR';
import cornerstone from './cornerstone.js'; import cornerstone from './cornerstone.js';
import hangingProtocols from './hanging-protocols'; import hangingProtocols from './hanging-protocols';
import header from './header.js'; import header from './header.js';
@@ -23,8 +24,6 @@ import {
UINotificationService, UINotificationService,
UIModalService, UIModalService,
UIDialogService, UIDialogService,
UIContextMenuService,
UILabellingFlowService,
} from './services'; } from './services';
const OHIF = { const OHIF = {
@@ -48,6 +47,7 @@ const OHIF = {
object, object,
log, log,
DICOMWeb, DICOMWeb,
DICOMSR,
viewer: {}, viewer: {},
measurements, measurements,
hangingProtocols, hangingProtocols,
@@ -55,8 +55,6 @@ const OHIF = {
UINotificationService, UINotificationService,
UIModalService, UIModalService,
UIDialogService, UIDialogService,
UIContextMenuService,
UILabellingFlowService,
}; };
export { export {
@@ -80,14 +78,13 @@ export {
object, object,
log, log,
DICOMWeb, DICOMWeb,
DICOMSR,
measurements, measurements,
hangingProtocols, hangingProtocols,
// //
UINotificationService, UINotificationService,
UIModalService, UIModalService,
UIDialogService, UIDialogService,
UIContextMenuService,
UILabellingFlowService,
}; };
export { OHIF }; export { OHIF };
+1 -2
View File
@@ -13,8 +13,6 @@ describe('Top level exports', () => {
'UINotificationService', 'UINotificationService',
'UIModalService', 'UIModalService',
'UIDialogService', 'UIDialogService',
'UIContextMenuService',
'UILabellingFlowService',
// //
'utils', 'utils',
'studies', 'studies',
@@ -30,6 +28,7 @@ describe('Top level exports', () => {
'object', 'object',
'log', 'log',
'DICOMWeb', 'DICOMWeb',
'DICOMSR',
'OHIF', // 'OHIF', //
'measurements', 'measurements',
'hangingProtocols', 'hangingProtocols',
@@ -1,4 +1,5 @@
import cornerstoneTools from 'cornerstone-tools'; import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import log from '../../log'; import log from '../../log';
import getLabel from '../lib/getLabel'; import getLabel from '../lib/getLabel';
import getDescription from '../lib/getDescription'; import getDescription from '../lib/getDescription';
@@ -7,13 +8,32 @@ import guid from '../../utils/guid';
import studyMetadataManager from '../../utils/studyMetadataManager'; import studyMetadataManager from '../../utils/studyMetadataManager';
import { measurementApiDefaultConfig } from './../configuration.js'; import { measurementApiDefaultConfig } from './../configuration.js';
const configuration = { const configuration = {
...measurementApiDefaultConfig, ...measurementApiDefaultConfig
}; };
export default class MeasurementApi { export default class MeasurementApi {
static Instance; static Instance;
/**
* Set configuration: It should merge default configuration with any new one
*
* @static
* @param {Object} config
* @param {Object} config.server
* @param {string} config.server.type - The server type
* @param {string} config.server.wadoRoot - The server wado URL root
* @param {Array} config.measurementTools
* @param {string} config.measurementTools[].id - The tool group id
* @param {string} config.measurementTools[].name - The tool group name
* @param {Array} config.measurementTools[].childTools - The child tool's configuration
* @param {Object} config.dataExchange
* @param {Function} config.dataExchange.store - Function that store measurement data
* @param {Function} config.dataExchange.retrieve - Function that retrieves measurement data
*
* @memberof MeasurementApi
*/
static setConfiguration(config) { static setConfiguration(config) {
Object.assign(configuration, config); Object.assign(configuration, config);
} }
@@ -203,13 +223,14 @@ export default class MeasurementApi {
retrieveMeasurements(patientId, timepointIds) { retrieveMeasurements(patientId, timepointIds) {
const retrievalFn = configuration.dataExchange.retrieve; const retrievalFn = configuration.dataExchange.retrieve;
const { server } = configuration;
if (typeof retrievalFn !== 'function') { if (typeof retrievalFn !== 'function') {
log.error('Measurement retrieval function has not been configured.'); log.error('Measurement retrieval function has not been configured.');
return; return;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
retrievalFn(patientId, timepointIds).then(measurementData => { retrievalFn(server).then(measurementData => {
if (measurementData) { if (measurementData) {
log.info('Measurement data retrieval'); log.info('Measurement data retrieval');
log.info(measurementData); log.info(measurementData);
@@ -230,6 +251,10 @@ export default class MeasurementApi {
// Synchronize the new tool data // Synchronize the new tool data
this.syncMeasurementsAndToolData(); this.syncMeasurementsAndToolData();
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
// Let others know that the measurements are updated // Let others know that the measurements are updated
this.onMeasurementsUpdated(); this.onMeasurementsUpdated();
}, reject); }, reject);
@@ -237,6 +262,7 @@ export default class MeasurementApi {
} }
storeMeasurements(timepointId) { storeMeasurements(timepointId) {
const { server } = configuration;
const storeFn = configuration.dataExchange.store; const storeFn = configuration.dataExchange.store;
if (typeof storeFn !== 'function') { if (typeof storeFn !== 'function') {
log.error('Measurement store function has not been configured.'); log.error('Measurement store function has not been configured.');
@@ -278,8 +304,9 @@ export default class MeasurementApi {
}; };
log.info('Saving Measurements for timepoints:', timepoints); log.info('Saving Measurements for timepoints:', timepoints);
return storeFn(measurementData, filter).then(() => { return storeFn(measurementData, filter, server).then(result => {
log.info('Measurement storage completed'); log.info('Measurement storage completed');
return result;
}); });
} }
@@ -11,6 +11,16 @@ const servers = (state = defaultState, action) => {
servers.forEach(s => (s.active = true)); servers.forEach(s => (s.active = true));
return { ...state, servers }; return { ...state, servers };
case 'ACTIVATE_SERVER': {
const newServer = { ...action.server, active: true };
const newServers = state.servers;
newServers.forEach(s => (s.active = false));
return {
...state,
servers: uniqBy([...newServers, newServer], 'wadoRoot'),
};
}
case 'SET_SERVERS': case 'SET_SERVERS':
return { ...state, servers: action.servers }; return { ...state, servers: action.servers };
@@ -33,7 +33,9 @@ export default class ServicesManager {
} }
if (service.create) { if (service.create) {
this.services[service.name] = service.create({ configuration }); this.services[service.name] = service.create({
configuration,
});
} else { } else {
log.warn(`Service create factory function not defined. Exiting early.`); log.warn(`Service create factory function not defined. Exiting early.`);
return; return;
@@ -33,7 +33,7 @@ describe('ServicesManager.js', () => {
[{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration], [{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration],
]); ]);
expect(servicesManager.registerService.mock.calls[1]).toContain( expect(servicesManager.registerService.mock.calls[1][1]).toEqual(
fakeConfiguration fakeConfiguration
); );
}); });
@@ -85,7 +85,7 @@ describe('ServicesManager.js', () => {
expect(log.warn.mock.calls.length).toBe(1); expect(log.warn.mock.calls.length).toBe(1);
}); });
it('pass configuration to service create factory function', () => { it('pass dependencies and configuration to service create factory function', () => {
const configuration = { config: 'Some configuration' }; const configuration = { config: 'Some configuration' };
servicesManager.registerService(fakeService, configuration); servicesManager.registerService(fakeService, configuration);
@@ -1,66 +0,0 @@
/**
* UI Context Menu
*
* @typedef {Object} ContextMenuProps
* @property {Event} event The event with tool information.
*/
const name = 'UIContextMenuService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const serviceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
};
/**
* Show a new UI ContextMenu dialog;
*
* @param {ContextMenuProps} props { event }
*/
function _show({ event }) {
return serviceImplementation._show({
event,
});
}
/**
* Hide a UI ContextMenu dialog;
*
*/
function _hide() {
return serviceImplementation._hide();
}
/**
*
*
* @param {*} {
* show: showImplementation,
* hide: hideImplementation,
* }
*/
function setServiceImplementation({
show: showImplementation,
hide: hideImplementation,
}) {
if (showImplementation) {
serviceImplementation._show = showImplementation;
}
if (hideImplementation) {
serviceImplementation._hide = hideImplementation;
}
}
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -1,71 +0,0 @@
/**
* UI Labelling Flow
*
* @typedef {Object} LabellingFlowProps
* @property {Object} defaultPosition The position of the labelling dialog.
* @property {boolean} centralize conditional to center the labelling dialog.
* @property {Object} props The labelling props.
*
*/
const name = 'UILabellingFlowService';
const publicAPI = {
name,
show: _show,
hide: _hide,
setServiceImplementation,
};
const serviceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
};
/**
* Hide a UI LabellingFlow dialog;
*
*/
function _hide() {
return serviceImplementation._hide();
}
/**
* Show a new UI LabellingFlow dialog;
*
* @param {LabellingFlowProps} props { defaultPosition, centralize, props }
*/
function _show({ defaultPosition, centralize, props }) {
return serviceImplementation._show({
defaultPosition,
centralize,
props,
});
}
/**
*
*
* @param {*} {
* show: showImplementation,
* hide: hideImplementation,
* }
*/
function setServiceImplementation({
show: showImplementation,
hide: hideImplementation,
}) {
if (showImplementation) {
serviceImplementation._show = showImplementation;
}
if (hideImplementation) {
serviceImplementation._hide = hideImplementation;
}
}
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
-4
View File
@@ -2,14 +2,10 @@ import ServicesManager from './ServicesManager.js';
import UINotificationService from './UINotificationService'; import UINotificationService from './UINotificationService';
import UIModalService from './UIModalService'; import UIModalService from './UIModalService';
import UIDialogService from './UIDialogService'; import UIDialogService from './UIDialogService';
import UIContextMenuService from './UIContextMenuService';
import UILabellingFlowService from './UILabellingFlowService';
export { export {
UINotificationService, UINotificationService,
UIModalService, UIModalService,
UIDialogService, UIDialogService,
UIContextMenuService,
UILabellingFlowService,
ServicesManager, ServicesManager,
}; };
@@ -40,7 +40,9 @@ export default class RetrieveMetadataLoader {
if (result && result.length) { if (result && result.length) {
break; // closes iterator in case data is retrieved successfully break; // closes iterator in case data is retrieved successfully
} }
} catch (e) { } } catch (e) {
throw e;
}
} }
if (loaders.next().done && !result) { if (loaders.next().done && !result) {
@@ -51,8 +53,8 @@ export default class RetrieveMetadataLoader {
} }
// Methods to be overwrite // Methods to be overwrite
async configLoad() { } async configLoad() {}
async preLoad() { } async preLoad() {}
async load(preLoadData) { } async load(preLoadData) {}
async posLoad(loadData) { } async posLoad(loadData) {}
} }
@@ -1,3 +1,4 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb'; import DICOMWeb from '../../../DICOMWeb';
const WADOProxy = { const WADOProxy = {
@@ -21,6 +22,43 @@ function parseFloatArray(obj) {
return result; 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...
}
},
};
/** /**
* Create a plain JS object that describes a study (a study descriptor object) * Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration parameters * @param {Object} server Object with server configuration parameters
-413
View File
@@ -1,413 +0,0 @@
import DICOMWeb from '../DICOMWeb/';
const WADOProxy = {
convertURL: (url, server) => {
// TODO: Remove all WADOProxy stuff from this file
return url;
},
};
function parseFloatArray(obj) {
const result = [];
if (!obj) {
return result;
}
const objs = obj.split('\\');
for (let i = 0; i < objs.length; i++) {
result.push(parseFloat(objs[i]));
}
return result;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration parameters
* @param {Object} aSopInstance a SOP Instance from which study information will be added
*/
function createStudy(server, aSopInstance) {
// TODO: Pass a reference ID to the server instead of including the URLs here
return {
seriesList: [],
seriesMap: Object.create(null),
seriesLoader: null,
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(aSopInstance['00100010']),
patientId: DICOMWeb.getString(aSopInstance['00100020']),
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
modalities: DICOMWeb.getString(aSopInstance['00080061']),
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
};
}
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
* @param seriesInstanceUid
* @param sopInstanceUid
* @returns {string}
*/
function buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frame
) {
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
frame = frame != null || 1;
return `${baseWadoRsUri}/frames/${frame}`;
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime',
};
if (!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if (
radiopharmaceuticalInfo === undefined ||
!radiopharmaceuticalInfo.Value ||
!radiopharmaceuticalInfo.Value.length
) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(
firstPetRadiopharmaceuticalInfo['00181072']
),
radionuclideTotalDose: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181074']
),
radionuclideHalfLife: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181075']
),
};
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length &&
SourceImageSequence.Value[0]['00081155'].Value
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
async function makeSOPInstance(server, study, instance) {
const { studyInstanceUid } = study;
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = study.seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesInstanceUid,
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
study.seriesMap[seriesInstanceUid] = series;
study.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const sopInstance = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
sopInstance.paletteColorLookupTableUID = palettes.uid;
}
sopInstance.redPaletteColorLookupTableData = palettes.red;
sopInstance.greenPaletteColorLookupTableData = palettes.green;
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(sopInstance);
return sopInstance;
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
let entry;
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
const promises = [r, g, b];
Promise.all(promises).then(args => {
entry = {
red: args[0],
green: args[1],
blue: args[2],
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration parameters
* @param {Object} study The study descriptor to which the given SOP instances will be added
* @param {Array} sopInstanceList A list of SOP instance objects
*/
async function addInstancesToStudy(server, study, sopInstanceList) {
return Promise.all(
sopInstanceList.map(function(sopInstance) {
return makeSOPInstance(server, study, sopInstance);
})
);
}
const createStudyFromSOPInstanceList = async (server, sopInstanceList) => {
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
const firstSopInstance = sopInstanceList[0];
const study = createStudy(server, firstSopInstance);
await addInstancesToStudy(server, study, sopInstanceList);
return study;
}
throw new Error('Failed to create study out of provided SOP instance list');
};
export { createStudyFromSOPInstanceList, addInstancesToStudy };
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.3...@ohif/i18n@0.52.4) (2019-12-16)
**Note:** Version bump only for package @ohif/i18n
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.2...@ohif/i18n@0.52.3) (2019-12-12)
### Bug Fixes
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28) ## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/i18n", "name": "@ohif/i18n",
"version": "0.52.2", "version": "0.52.4",
"description": "Internationalization library for The OHIF Viewer", "description": "Internationalization library for The OHIF Viewer",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
+9 -8
View File
@@ -29,19 +29,20 @@ function addLocales(newLocales) {
customDebug(resourceBundle, 'info'); customDebug(resourceBundle, 'info');
} }
// Note: Developers can add the API key to use the in-context editor. /*
// DO NOT commit the API key * Note: Developers can add the API key to use the
const config = (window.config && window.config.i18n) || {}; * in-context editor using environment variables.
* (DO NOT commit the API key)
*/
const locizeOptions = { const locizeOptions = {
projectId: config.LOCIZE_PROJECTID, projectId: process.env.LOCIZE_PROJECTID,
apiKey: config.LOCIZE_API_KEY, apiKey: process.env.LOCIZE_API_KEY,
referenceLng: 'en-US', referenceLng: 'en-US',
fallbacklng: 'en-US', fallbacklng: 'en-US',
}; };
const envUseLocize = !!config.USE_LOCIZE; const envUseLocize = !!process.env.USE_LOCIZE;
const envApiKeyAvailable = !!config.LOCIZE_API_KEY; const envApiKeyAvailable = !!process.env.LOCIZE_API_KEY;
const DEFAULT_LANGUAGE = 'en-US'; const DEFAULT_LANGUAGE = 'en-US';
function initI18n( function initI18n(
@@ -10,6 +10,7 @@
"Clear": "Clear", "Clear": "Clear",
"Coronal": "Coronal", "Coronal": "Coronal",
"Crosshairs": "Crosshairs", "Crosshairs": "Crosshairs",
"Download": "Download",
"Ellipse": "Ellipse", "Ellipse": "Ellipse",
"Elliptical": "Elliptical", "Elliptical": "Elliptical",
"Flip H": "Flip H", "Flip H": "Flip H",
+10 -9
View File
@@ -10,18 +10,19 @@
"Clear": "Limpiar", "Clear": "Limpiar",
"Coronal": "Coronal", "Coronal": "Coronal",
"Crosshairs": "Punto de mira", "Crosshairs": "Punto de mira",
"Download": "Descargar",
"Ellipse": "Elipse", "Ellipse": "Elipse",
"Elliptical": "Elíptico", "Elliptical": "Elíptico",
"Flip H": "Espejo H", "Flip H": "Voltear H",
"Flip V": "Espejo V", "Flip V": "Voltear V",
"Freehand": "Mano alzada", "Freehand": "Mano alzada",
"Invert": "Invertir", "Invert": "Negativo",
"Layout": "$t(Common:Layout)", "Layout": "$t(Common:Layout)",
"Length": "Longitud", "Length": "Longitud",
"Levels": "Niveles", "Levels": "W/L",
"Magnify": "Aumentar", "Magnify": "Lupa",
"Manual": "Manual", "Manual": "Manual",
"Measurements": "Mediciones", "Measurements": "Medidas",
"More": "$t(Common:More)", "More": "$t(Common:More)",
"Next": "$t(Common:Next)", "Next": "$t(Common:Next)",
"Pan": "Mover", "Pan": "Mover",
@@ -32,11 +33,11 @@
"Rectangle": "Rectángulo", "Rectangle": "Rectángulo",
"Reset": "$t(Common:Reset)", "Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:Reset) por defecto", "Reset to Defaults": "$t(Common:Reset) por defecto",
"Rotate Right": "Rotar ->", "Rotate Right": "Girar ->",
"Sagittal": "Sagital", "Sagittal": "Sagital",
"Save": "Guardar", "Save": "Guardar",
"Stack Scroll": "Scroll en pila", "Stack Scroll": "Scroll",
"Stop": "$t(Common:Stop)", "Stop": "$t(Common:Stop)",
"Themes": "Temas", "Themes": "Temas",
"Zoom": "Zoom" "Zoom": "Ampliar"
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"Image": "Imagen", "Image": "Imagen",
"Layout": "Disposición", "Layout": "Formato",
"Measurements": "Medidas", "Measurements": "Medidas",
"More": "Más", "More": "Más",
"Next": "Siguiente", "Next": "Siguiente",
"Play": "Play", "Play": "Play",
"Previous": "Anterior", "Previous": "Anterior",
"Reset": "Resetear", "Reset": "Restaurar",
"RowsPerPage": "filas por página", "RowsPerPage": "filas por página",
"Series": "Secuencia", "Series": "Secuencia",
"Show": "Mostrar", "Show": "Mostrar",
+3 -2
View File
@@ -10,6 +10,7 @@
"Clear": "Limpar", "Clear": "Limpar",
"Coronal": "Coronal", "Coronal": "Coronal",
"Crosshairs": "Localizador", "Crosshairs": "Localizador",
"Download": "Baixar",
"Ellipse": "Elipse", "Ellipse": "Elipse",
"Elliptical": "Elíptico", "Elliptical": "Elíptico",
"Flip H": "Inverter H", "Flip H": "Inverter H",
@@ -30,8 +31,8 @@
"Probe": "Prova", "Probe": "Prova",
"ROI Window": "Janela ROI", "ROI Window": "Janela ROI",
"Rectangle": "Retângulo", "Rectangle": "Retângulo",
"Reset": "Resetar", "Reset": "$t(Common:Reset)",
"Reset to Defaults": "Restaurar para o Padrão", "Reset to Defaults": "$t(Common:Reset) para o Padrão",
"Rotate Right": "Girar à direita", "Rotate Right": "Girar à direita",
"Sagittal": "Sagital", "Sagittal": "Sagital",
"Save": "Salvar", "Save": "Salvar",
+1 -1
View File
@@ -6,6 +6,6 @@
"Next": "Próximo", "Next": "Próximo",
"Play": "Play", "Play": "Play",
"Previous": "Anterior", "Previous": "Anterior",
"Reset": "Reset", "Reset": "Restaurar",
"Stop": "Stop" "Stop": "Stop"
} }
@@ -1,6 +1,6 @@
{ {
"Cancel": "Cancelar", "Cancel": "Cancelar",
"Reset to Defaults": "Restaurar Default", "Reset to Defaults": "$t(Common:Reset) para Padrão",
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.", "ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
"Save": "Salvar", "Save": "Salvar",
"SaveMessage": "Preferências salvas", "SaveMessage": "Preferências salvas",
+87
View File
@@ -3,6 +3,93 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.8](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.7...@ohif/ui@1.1.8) (2020-01-08)
### Bug Fixes
* measurements panel css and delete button visibility ([#1352](https://github.com/OHIF/Viewers/issues/1352)) ([7ab0bbb](https://github.com/OHIF/Viewers/commit/7ab0bbb32581dcba16ee16b49b92406e2856ac76))
## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.6...@ohif/ui@1.1.7) (2019-12-20)
**Note:** Version bump only for package @ohif/ui
## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.5...@ohif/ui@1.1.6) (2019-12-20)
**Note:** Version bump only for package @ohif/ui
## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.4...@ohif/ui@1.1.5) (2019-12-19)
### Bug Fixes
* 🐛 Fix drag-n-drop of local files into OHIF ([#1319](https://github.com/OHIF/Viewers/issues/1319)) ([23305ce](https://github.com/OHIF/Viewers/commit/23305cec9c0f514e73a8dd17f984ffc87ad8d131)), closes [#1307](https://github.com/OHIF/Viewers/issues/1307)
## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.3...@ohif/ui@1.1.4) (2019-12-16)
**Note:** Version bump only for package @ohif/ui
## [1.1.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.2...@ohif/ui@1.1.3) (2019-12-13)
### Bug Fixes
* allow empty values for dimensions ([#1295](https://github.com/OHIF/Viewers/issues/1295)) ([cd2da34](https://github.com/OHIF/Viewers/commit/cd2da349e5212cccdd8e65ffa3f7fdc6bad1057c))
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.1...@ohif/ui@1.1.2) (2019-12-12)
### Bug Fixes
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.0...@ohif/ui@1.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/ui
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.1...@ohif/ui@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.0...@ohif/ui@1.0.1) (2019-12-09) ## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.0...@ohif/ui@1.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/ui **Note:** Version bump only for package @ohif/ui
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/ui", "name": "@ohif/ui",
"version": "1.0.1", "version": "1.1.8",
"description": "A set of React components for Medical Imaging Viewers", "description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors", "author": "OHIF Contributors",
"license": "MIT", "license": "MIT",
@@ -32,69 +32,52 @@ const ViewportDownloadForm = ({
maximumSize, maximumSize,
canvasClass, canvasClass,
}) => { }) => {
const [t] = useTranslation('ViewportDownloadForm'); const [t] = useTranslation('Buttons');
const [filename, setFilename] = useState(DEFAULT_FILENAME); const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg'); const [fileType, setFileType] = useState('jpg');
const [height, setHeight] = useState(defaultSize); const [dimensions, setDimensions] = useState({
const [width, setWidth] = useState(defaultSize); width: defaultSize,
height: defaultSize,
});
const [showAnnotations, setShowAnnotations] = useState(true); const [showAnnotations, setShowAnnotations] = useState(true);
const [keepAspect, setKeepAspect] = useState(true);
const [lastImage, setLastImage] = useState();
const [viewportElement, setViewportElement] = useState(); const [viewportElement, setViewportElement] = useState();
const [viewportElementHeight, setViewportElementHeight] = useState( const [viewportElementDimensions, setViewportElementDimensions] = useState({
minimumSize width: defaultSize,
); height: defaultSize,
const [viewportElementWidth, setViewportElementWidth] = useState(minimumSize); });
const [downloadCanvas, setDownloadCanvas] = useState({ const [downloadCanvas, setDownloadCanvas] = useState({
ref: createRef(), ref: createRef(),
width: minimumSize, width: defaultSize,
height: minimumSize, height: defaultSize,
}); });
const [viewportPreview, setViewportPreview] = useState({ const [viewportPreview, setViewportPreview] = useState({
src: null, src: null,
width: minimumSize, width: defaultSize,
height: minimumSize, height: defaultSize,
}); });
// Cornerstone's `enable/disable`
useEffect(() => { useEffect(() => {
enableViewport(viewportElement); enableViewport(viewportElement);
return () => { return () => {
disableViewport(viewportElement); disableViewport(viewportElement);
setHeight(defaultSize);
setWidth(defaultSize);
}; };
}, [defaultSize, disableViewport, enableViewport, viewportElement]); }, [disableViewport, enableViewport, viewportElement]);
useEffect(() => { useEffect(() => {
const { width, height } = viewportElementDimensions;
const validSize = value => (value >= minimumSize ? value : minimumSize); const validSize = value => (value >= minimumSize ? value : minimumSize);
const loadAndUpdateViewports = async () => { const loadAndUpdateViewports = async () => {
const { await loadImage(activeViewport, viewportElement, width, height);
image,
width: scaledWidth,
height: scaledHeight,
} = await loadImage(activeViewport, viewportElement, width, height);
setLastImage(image);
toggleAnnotations(showAnnotations, viewportElement); toggleAnnotations(showAnnotations, viewportElement);
setViewportElementHeight(validSize(scaledHeight));
setViewportElementWidth(validSize(scaledWidth));
setDownloadCanvas(state => ({
...state,
height: validSize(scaledHeight),
width: validSize(scaledWidth),
}));
const { const {
dataUrl, dataUrl,
width: viewportElementWidth, width: viewportElementWidth,
@@ -118,8 +101,6 @@ const ViewportDownloadForm = ({
activeViewport, activeViewport,
viewportElement, viewportElement,
showAnnotations, showAnnotations,
height,
width,
loadImage, loadImage,
toggleAnnotations, toggleAnnotations,
updateViewportPreview, updateViewportPreview,
@@ -127,54 +108,41 @@ const ViewportDownloadForm = ({
downloadCanvas.ref, downloadCanvas.ref,
minimumSize, minimumSize,
maximumSize, maximumSize,
viewportElementDimensions,
]); ]);
const onHeightChange = event => { /**
const newHeight = Math.min(event.target.value, maximumSize); * @param {object} event - Input change event
setHeight(newHeight); * @param {string} dimension - "height" | "width"
*/
const onDimensionsChange = (event, dimension) => {
const sanitizedTargetValue = event.target.value.replace(/\D/, '');
const isEmpty = sanitizedTargetValue === '';
const updatedDimension = isEmpty
? ''
: Math.min(sanitizedTargetValue, maximumSize);
setViewportElementHeight(newHeight); if (updatedDimension === dimensions.width) {
return;
setDownloadCanvas(state => ({
...state,
height: newHeight,
}));
if (keepAspect) {
const multiplier = newHeight / lastImage.height;
const newWidth = Math.round(lastImage.width * multiplier);
setWidth(newWidth);
setViewportElementWidth(newWidth);
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
} }
};
const onWidthChange = event => { // In current code, keepAspect is always `true`
const newWidth = Math.min(event.target.value, maximumSize); // And we always start w/ a square width/height
setWidth(newWidth); setDimensions({
width: updatedDimension,
setViewportElementWidth(newWidth); height: updatedDimension,
});
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
if (keepAspect) {
const multiplier = newWidth / lastImage.width;
const newHeight = Math.round(lastImage.height * multiplier);
setHeight(newHeight);
setViewportElementHeight(newHeight);
// Only update if value is non-empty
if (!isEmpty) {
setViewportElementDimensions({
height: updatedDimension,
width: updatedDimension,
});
setDownloadCanvas(state => ({ setDownloadCanvas(state => ({
...state, ...state,
height: newHeight, height: updatedDimension,
width: updatedDimension,
})); }));
} }
}; };
@@ -196,26 +164,22 @@ const ViewportDownloadForm = ({
)} )}
</div> </div>
<div className="file-info-container"> <div className="file-info-container" data-cy="file-info-container">
<div className="col"> <div className="col">
<div className="width"> <div className="width">
<TextInput <TextInput
type="number" data-cy="image-width"
min={minimumSize} value={dimensions.width}
max={maximumSize}
value={width}
label={t('Image width (px)')} label={t('Image width (px)')}
onChange={onWidthChange} onChange={evt => onDimensionsChange(evt, 'height')}
/> />
</div> </div>
<div className="height"> <div className="height">
<TextInput <TextInput
type="number" data-cy="image-height"
min={minimumSize} value={dimensions.height}
max={maximumSize}
value={height}
label={t('Image height (px)')} label={t('Image height (px)')}
onChange={onHeightChange} onChange={evt => onDimensionsChange(evt, 'width')}
/> />
</div> </div>
</div> </div>
@@ -224,6 +188,7 @@ const ViewportDownloadForm = ({
<div className="file-name"> <div className="file-name">
<TextInput <TextInput
type="text" type="text"
data-cy="file-name"
value={filename} value={filename}
onChange={event => setFilename(event.target.value)} onChange={event => setFilename(event.target.value)}
label={t('File name')} label={t('File name')}
@@ -233,6 +198,7 @@ const ViewportDownloadForm = ({
<div className="file-type"> <div className="file-type">
<Select <Select
value={fileType} value={fileType}
data-cy="file-type"
onChange={event => setFileType(event.target.value)} onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS} options={FILE_TYPE_OPTIONS}
label={t('File type')} label={t('File type')}
@@ -245,6 +211,7 @@ const ViewportDownloadForm = ({
<label htmlFor="show-annotations" className="form-check-label"> <label htmlFor="show-annotations" className="form-check-label">
<input <input
id="show-annotations" id="show-annotations"
data-cy="show-annotations"
type="checkbox" type="checkbox"
className="form-check-input" className="form-check-input"
checked={showAnnotations} checked={showAnnotations}
@@ -258,8 +225,8 @@ const ViewportDownloadForm = ({
<div <div
style={{ style={{
height: viewportElementHeight, height: viewportElementDimensions.height,
width: viewportElementWidth, width: viewportElementDimensions.width,
position: 'absolute', position: 'absolute',
left: '9999px', left: '9999px',
}} }}
@@ -278,23 +245,33 @@ const ViewportDownloadForm = ({
></canvas> ></canvas>
</div> </div>
<div className="preview"> <div className="preview" data-cy="image-preview">
<h4> {t('Image Preview')}</h4> <h4> {t('Image Preview')}</h4>
<img <img
className="viewport-preview" className="viewport-preview"
src={viewportPreview.src} src={viewportPreview.src}
alt="Viewport Preview" alt="Viewport Preview"
data-cy="viewport-preview-img"
/> />
</div> </div>
<div className="actions"> <div className="actions">
<div className="action-cancel"> <div className="action-cancel">
<button type="button" className="btn btn-danger" onClick={onClose}> <button
type="button"
data-cy="cancel-btn"
className="btn btn-danger"
onClick={onClose}
>
{t('Cancel')} {t('Cancel')}
</button> </button>
</div> </div>
<div className="action-save"> <div className="action-save">
<button onClick={downloadImage} className="btn btn-primary"> <button
onClick={downloadImage}
className="btn btn-primary"
data-cy="download-btn"
>
{t('Download')} {t('Download')}
</button> </button>
</div> </div>
@@ -312,6 +289,7 @@ ViewportDownloadForm.propTypes = {
toggleAnnotations: PropTypes.func.isRequired, toggleAnnotations: PropTypes.func.isRequired,
loadImage: PropTypes.func.isRequired, loadImage: PropTypes.func.isRequired,
downloadBlob: PropTypes.func.isRequired, downloadBlob: PropTypes.func.isRequired,
/** A default width & height, between the minimum and maximum size */
defaultSize: PropTypes.number.isRequired, defaultSize: PropTypes.number.isRequired,
minimumSize: PropTypes.number.isRequired, minimumSize: PropTypes.number.isRequired,
maximumSize: PropTypes.number.isRequired, maximumSize: PropTypes.number.isRequired,
@@ -1,20 +1,19 @@
.ToolContextMenu { .ContextMenu {
position: relative; position: relative;
background-color: white; background-color: white;
border: 1px solid white;
border-radius: 5px; border-radius: 5px;
z-index: 1000; z-index: 1000;
display: block; display: block;
width: 170px; width: 170px;
} }
.ToolContextMenu > ul { .ContextMenu>ul {
list-style-type: none; list-style-type: none;
padding-left: 0; padding-left: 0;
margin: 0; margin: 0;
} }
.ToolContextMenu > ul > li > button { .ContextMenu>ul>li>button {
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
border: none; border: none;
@@ -25,10 +24,10 @@
background: none; background: none;
} }
.ToolContextMenu > ul > li > button:hover { .ContextMenu>ul>li>button:hover {
color: #16202b; color: #16202b;
} }
.ToolContextMenu > ul > li > button:active { .ContextMenu>ul>li>button:active {
color: #79f9fe; color: #79f9fe;
} }
@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import './ContextMenu.css';
const ContextMenu = ({ items, onClick }) => {
return (
<div className="ContextMenu">
<ul>
{items.map((item, index) => (
<li key={index}>
<button className="form-action" onClick={() => onClick(item)}>
<span key={index}>{item.label}</span>
</button>
</li>
))}
</ul>
</div>
);
};
ContextMenu.propTypes = {
items: PropTypes.array.isRequired,
onClick: PropTypes.func.isRequired,
};
export default ContextMenu;
@@ -0,0 +1 @@
export { default as ContextMenu } from './ContextMenu.js';
+2
View File
@@ -17,6 +17,7 @@ import { RoundedButtonGroup } from './roundedButtonGroup';
import { SelectTree } from './selectTree'; import { SelectTree } from './selectTree';
import { SimpleDialog } from './simpleDialog'; import { SimpleDialog } from './simpleDialog';
import { OHIFModal } from './ohifModal'; import { OHIFModal } from './ohifModal';
import { ContextMenu } from './contextMenu';
import { import {
PageToolbar, PageToolbar,
StudyList, StudyList,
@@ -27,6 +28,7 @@ import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip'; import { Tooltip } from './tooltip';
export { export {
ContextMenu,
Checkbox, Checkbox,
CineDialog, CineDialog,
ViewportDownloadForm, ViewportDownloadForm,
@@ -22,8 +22,9 @@ class MeasurementTable extends Component {
onDeleteClick: PropTypes.func, onDeleteClick: PropTypes.func,
onEditDescriptionClick: PropTypes.func, onEditDescriptionClick: PropTypes.func,
selectedMeasurementNumber: PropTypes.number, selectedMeasurementNumber: PropTypes.number,
overwallWarnings: PropTypes.object,
t: PropTypes.func, t: PropTypes.func,
saveFunction: PropTypes.func,
onSaveComplete: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
@@ -38,8 +39,9 @@ class MeasurementTable extends Component {
}; };
render() { render() {
const hasOverallWarnings = const { overallWarnings, saveFunction, t } = this.props;
this.props.overallWarnings.warningList.length > 0; const hasOverallWarnings = overallWarnings.warningList.length > 0;
return ( return (
<div className="measurementTable"> <div className="measurementTable">
<div className="measurementTableHeader"> <div className="measurementTableHeader">
@@ -55,7 +57,7 @@ class MeasurementTable extends Component {
style={{}} style={{}}
> >
<div className="warningTitle"> <div className="warningTitle">
{this.props.t('Criteria nonconformities')} {t('Criteria nonconformities')}
</div> </div>
<div className="warningContent"> <div className="warningContent">
{this.getWarningContent()} {this.getWarningContent()}
@@ -75,10 +77,46 @@ class MeasurementTable extends Component {
<ScrollableArea> <ScrollableArea>
<div>{this.getMeasurementsGroups()}</div> <div>{this.getMeasurementsGroups()}</div>
</ScrollableArea> </ScrollableArea>
<div className="measurementTableFooter">
{saveFunction && (
<button
onClick={this.saveFunction}
className="saveBtn"
data-cy="save-measurements-btn"
>
<Icon name="save" width="14px" height="14px" />
Save measurements
</button>
)}
</div>
</div> </div>
); );
} }
saveFunction = async event => {
const { saveFunction, onSaveComplete } = this.props;
if (saveFunction) {
try {
const result = await saveFunction();
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: result.message,
type: 'success',
});
}
} catch (error) {
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: error.message,
type: 'error',
});
}
}
}
};
getMeasurementsGroups = () => { getMeasurementsGroups = () => {
return this.props.measurementCollection.map((measureGroup, index) => { return this.props.measurementCollection.map((measureGroup, index) => {
return ( return (
@@ -157,7 +195,7 @@ class MeasurementTable extends Component {
}; };
getWarningContent = () => { getWarningContent = () => {
const { warningList = '' } = this.props.overwallWarnings; const { warningList = '' } = this.props.overallWarnings;
if (Array.isArray(warningList)) { if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => { const listedWarnings = warningList.map((warn, index) => {
@@ -1,92 +1,117 @@
.measurementTable .measurementTable
background-color: var(--primary-background-color) background-color: var(--primary-background-color)
height: 100%; height: 100%
width: 100%; width: 100%
display: flex; display: flex
flex-direction: column; flex-direction: column
.maxMeasurements .maxMeasurements
border-radius: 3px; border-radius: 3px
color: var(--primary-background-color); color: var(--primary-background-color)
font-size: 12px; font-size: 12px
font-weight: 500; font-weight: 500
height: 19px; height: 19px
line-height: 17px; line-height: 17px
margin-top: 22px; margin-top: 22px
margin-right: 15px; margin-right: 15px
padding: 2px 6px 0; padding: 2px 6px 0
text-transform: uppercase; text-transform: uppercase
transition: all 0.3s ease; transition: all 0.3s ease
background-color: var(--default-color); background-color: var(--default-color)
.measurementTableHeader .measurementTableHeader
padding-left: 45px; padding-left: 45px
.measurementTableHeaderItem .measurementTableHeaderItem
display: inline-block; display: inline-block
margin-top: 9px; margin-top: 9px
margin-left: 9px; margin-left: 9px
padding-left: 9px; padding-left: 9px
width: 90px; width: 90px
font-size: 14px; font-size: 14px
line-height: 20px; line-height: 20px
border-left: 1px solid var(--text-secondary-color); border-left: 1px solid var(--text-secondary-color)
color: var(--text-primary-color); color: var(--text-primary-color)
.timepointLabel .timepointLabel
color: var(--text-secondary-color); color: var(--text-secondary-color)
font-size: 12px; font-size: 12px
.warning-status .warning-status
display: block; display: block
position: absolute; position: absolute
width: 50px; width: 50px
height: 40px; height: 40px
padding-left: 7px; padding-left: 7px
left: 15px; left: 15px
top: 7px; top: 7px
.warning-border .warning-border
display: block; display: block
border: 2px solid #e29e4a; border: 2px solid #e29e4a
border-radius: 36px; border-radius: 36px
padding: 6px 7px; padding: 6px 7px
width: 40px; width: 40px
height: 40px; height: 40px
svg svg
color: #e29e4a; color: #e29e4a
width: 22px; width: 22px
height: 20px; height: 20px
pointer-events: inherit; pointer-events: inherit
.measurementTableFooter
padding: 10px
color: #fff
text-align: center
z-index: 1
.saveBtn
border: 1px solid var(--ui-border-color-active)
border-radius: 25px
background-color: var(--active-color)
color: var(--ui-gray-dark)
line-height: 25px
padding: 0 20px
outline: none
cursor: pointer
&:hover, &:active
background-color: var(--ui-sky-blue)
svg
margin-right: 4px
position: relative
top: 2px
.tooltip-warning .tooltip-warning
&.in &.in
opacity 1; opacity 1
.tooltip-inner .tooltip-inner
padding: 0 padding: 0
.warningTitle .warningTitle
text-align: left text-align: left
background-color: var(--ui-gray-darkest); background-color: var(--ui-gray-darkest)
padding: 7px 15px; padding: 7px 15px
border-top-left-radius: 7px; border-top-left-radius: 7px
border-top-right-radius: 7px; border-top-right-radius: 7px
.warningContent .warningContent
text-align center text-align center
background-color: var(--ui-gray); background-color: var(--ui-gray)
border-bottom-left-radius: 7px; border-bottom-left-radius: 7px
border-bottom-right-radius: 7px; border-bottom-right-radius: 7px
ol ol
margin-top: auto; margin-top: auto
padding-inline-start: 20px; padding-inline-start: 20px
padding-top: 10px; padding-top: 10px
padding-bottom: 10px; padding-bottom: 10px
padding-left: 25px; padding-left: 25px
text-align: left; text-align: left
li li
padding-right: 10px; padding-right: 10px
@@ -1,5 +1,6 @@
.measurementItem .measurementItem
.rowActions .rowActions
margin-left: -1px;
background-color: var(--ui-gray-darker) background-color: var(--ui-gray-darker)
height: 0 height: 0
overflow: hidden overflow: hidden
@@ -28,7 +29,6 @@
visibility: visible visibility: visible
.measurementLocation .measurementLocation
margin-left: 9px;
margin-top: 9px; margin-top: 9px;
overflow: hidden; overflow: hidden;
max-width: calc(100% - 50px); max-width: calc(100% - 50px);
@@ -39,7 +39,6 @@
.measurementDisplayText .measurementDisplayText
display: inline-block; display: inline-block;
margin-top: 9px; margin-top: 9px;
margin-left: 9px;
padding-left: 9px; padding-left: 9px;
width: 90px; width: 90px;
border-left: 1px solid var(--text-secondary-color); border-left: 1px solid var(--text-secondary-color);
@@ -29,7 +29,7 @@ const OHIFModal = ({
const renderHeader = () => { const renderHeader = () => {
return ( return (
title && ( title && (
<div className="OHIFModal__header"> <div className="OHIFModal__header" data-cy="modal-header">
<h4>{title}</h4> <h4>{title}</h4>
{closeButton && ( {closeButton && (
<button data-cy="close-button" onClick={onClose}> <button data-cy="close-button" onClick={onClose}>
@@ -44,6 +44,7 @@ const OHIFModal = ({
return ( return (
<Modal <Modal
className={classNames('OHIFModal', className)} className={classNames('OHIFModal', className)}
data-cy="modal"
shouldCloseOnEsc={shouldCloseOnEsc} shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen} isOpen={isOpen}
title={title} title={title}
@@ -51,7 +52,9 @@ const OHIFModal = ({
> >
<> <>
{renderHeader()} {renderHeader()}
<div className="OHIFModal__content">{children}</div> <div className="OHIFModal__content" data-cy="modal-content">
{children}
</div>
</> </>
</Modal> </Modal>
); );
@@ -28,7 +28,7 @@
transition(all 0.3s ease) transition(all 0.3s ease)
font-size: 14px font-size: 14px
font-weight: 400 font-weight: 400
margin-right: 5px margin-right: 6px
padding: 10px padding: 10px
text-align: center text-align: center
-webkit-box-sizing: initial; -webkit-box-sizing: initial;
@@ -38,9 +38,8 @@
.itemContent .itemContent
flex: 1 flex: 1
color: var(--text-secondary-color) color: var(--text-secondary-color)
width: calc(100% - 30px); width: calc(100% - 51px);
font-weight: 400 font-weight: 400
margin-left: 9px;
margin-top: 9px; margin-top: 9px;
.warning-icon .warning-icon
@@ -43,6 +43,7 @@ const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
? tabs.map((tab, index) => { ? tabs.map((tab, index) => {
const { name, hidden = false } = tab; const { name, hidden = false } = tab;
const cypressSelectorId = name.toLowerCase();
const tabClass = const tabClass =
index === activeTabIndex ? 'nav-link active' : 'nav-link'; index === activeTabIndex ? 'nav-link active' : 'nav-link';
return !hidden ? ( return !hidden ? (
@@ -52,6 +53,7 @@ const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
onHeaderChanged(index); onHeaderChanged(index);
}} }}
className={tabClass} className={tabClass}
data-cy={cypressSelectorId}
> >
<button>{name}</button> <button>{name}</button>
</li> </li>
@@ -1,147 +0,0 @@
import React, {
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { useDialog } from '@ohif/ui';
import { useLabellingFlow } from '@ohif/ui';
const ContextMenuContext = createContext(null);
const { Provider } = ContextMenuContext;
export const useContextMenu = () => useContext(ContextMenuContext);
const ContextMenuProvider = ({
children,
service,
contextMenuComponent: ContextMenuComponent,
onDelete,
}) => {
const { create, dismiss } = useDialog();
const { show: showLabellingFlow } = useLabellingFlow();
/**
* Sets the implementation of a context menu service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({
show,
hide,
});
}
}, [hide, service, show]);
const hide = useCallback(() => dismiss({ id: 'context-menu' }), [dismiss]);
/**
* Show the context menu and override its configuration props.
*
* @param {ContextMenuProps} props { eventData, isTouchEvent, onClose, visible }
* @returns void
*/
const show = useCallback(
({ event }) => {
hide();
create({
id: 'context-menu',
isDraggable: false,
preservePosition: false,
content: ContextMenuComponent,
contentProps: {
eventData: event,
onDelete: (nearbyToolData, eventData) =>
onDelete(nearbyToolData, eventData),
onClose: () => dismiss({ id: 'context-menu' }),
onSetLabel: (eventData, measurementData) =>
showLabellingFlow({
event: eventData,
centralize: true,
props: {
measurementData,
skipAddLabelButton: true,
editLocation: true,
},
}),
onSetDescription: (eventData, measurementData) =>
showLabellingFlow({
event: eventData,
centralize: false,
defaultPosition: _getDefaultPosition(eventData),
props: {
measurementData,
editDescriptionOnDialog: true,
},
}),
},
defaultPosition: _getDefaultPosition(event),
});
},
[ContextMenuComponent, create, dismiss, hide, onDelete, showLabellingFlow]
);
const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client.x) || 0,
y: (event && event.currentPoints.client.y) || 0,
});
return (
<Provider
value={{
show,
hide,
}}
>
{children}
</Provider>
);
};
/**
* Higher Order Component to use the context menu methods through a Class Component.
*
* @returns
*/
export const withContextMenu = Component => {
return function WrappedComponent(props) {
const { show, hide } = useContextMenu();
return (
<Component
{...props}
modal={{
show,
hide,
}}
/>
);
};
};
ContextMenuProvider.defaultProps = {
service: null,
};
ContextMenuProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
contextMenuComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
onDelete: PropTypes.func.isRequired,
};
export default ContextMenuProvider;
export const ContextMenuConsumer = ContextMenuContext.Consumer;
@@ -152,6 +152,7 @@ const DialogProvider = ({ children, service }) => {
onStart, onStart,
onStop, onStop,
onDrag, onDrag,
showOverlay,
} = dialog; } = dialog;
let position = let position =
@@ -160,7 +161,7 @@ const DialogProvider = ({ children, service }) => {
position = centerPositions.find(position => position.id === id); position = centerPositions.find(position => position.id === id);
} }
return ( const dragableItem = () => (
<Draggable <Draggable
key={id} key={id}
disabled={!isDraggable} disabled={!isDraggable}
@@ -217,6 +218,16 @@ const DialogProvider = ({ children, service }) => {
</div> </div>
</Draggable> </Draggable>
); );
return (
showOverlay ? (
<div className="Overlay" key={id}>
{dragableItem()}
</div>
) : (
dragableItem()
)
);
}); });
/** /**
@@ -238,13 +249,11 @@ const DialogProvider = ({ children, service }) => {
return ( return (
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}> <DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
<div className="DraggableArea"> {!isEmpty() &&
{dialogs.some(dialog => dialog.showOverlay) ? ( <div className="DraggableArea">
<div className="Overlay active">{renderDialogs()}</div> {renderDialogs()}
) : ( </div>
renderDialogs() }
)}
</div>
{children} {children}
</DialogContext.Provider> </DialogContext.Provider>
); );
@@ -6,17 +6,16 @@
div div
cursor: grabbing !important cursor: grabbing !important
.DraggableArea, .Overlay .DraggableArea
width: 100% width: 100%
height: 100% height: 100%
position: absolute position: absolute
.Overlay
.Overlay.active position: fixed
position: fixed z-index: 999
z-index: 999 left: 0
left: 0 top: 0
top: 0 width: 100%
width: 100% height: 100%
height: 100% overflow: auto
overflow: auto background: rgba(0,0,0,.1)
background: rgba(0,0,0,.1)
@@ -1,136 +0,0 @@
import React, {
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { useDialog } from './DialogProvider';
const LabellingFlowContext = createContext(null);
const { Provider } = LabellingFlowContext;
export const useLabellingFlow = () => useContext(LabellingFlowContext);
const LabellingFlowProvider = ({
children,
service,
labellingComponent: LabellingComponent,
onUpdateLabelling,
}) => {
const { create, dismiss } = useDialog();
/**
* Sets the implementation of a labelling flow service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({
show,
hide,
});
}
}, [hide, service, show]);
const hide = useCallback(() => dismiss({ id: 'labelling' }), [dismiss]);
const show = useCallback(
({ centralize, defaultPosition, props }) => {
hide();
create({
id: 'labelling',
centralize,
isDraggable: false,
showOverlay: true,
content: LabellingComponent,
defaultPosition,
contentProps: {
visible: true,
measurementData: props.measurementData,
labellingDoneCallback: () => dismiss({ id: 'labelling' }),
updateLabelling: labellingData =>
_updateLabellingHandler(labellingData, props.measurementData),
...props,
},
});
},
[LabellingComponent, _updateLabellingHandler, create, dismiss, hide]
);
const _updateLabellingHandler = useCallback(
(labellingData, measurementData) => {
const { location, description, response } = labellingData;
if (location) {
measurementData.location = location;
}
measurementData.description = description || '';
if (response) {
measurementData.response = response;
}
onUpdateLabelling(labellingData, measurementData);
},
[onUpdateLabelling]
);
return (
<Provider
value={{
show,
hide,
}}
>
{children}
</Provider>
);
};
/**
* Higher Order Component to use the labelling flow methods through a Class Component.
*
* @returns
*/
export const withLabellingFlow = Component => {
return function WrappedComponent(props) {
const { show, hide } = useLabellingFlow();
return (
<Component
{...props}
modal={{
show,
hide,
}}
/>
);
};
};
LabellingFlowProvider.defaultProps = {
service: null,
};
LabellingFlowProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
labellingComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
onUpdateLabelling: PropTypes.func.isRequired,
};
export default LabellingFlowProvider;
export const LabellingFlowConsumer = LabellingFlowContext.Consumer;
-12
View File
@@ -18,15 +18,3 @@ export {
withDialog, withDialog,
useDialog, useDialog,
} from './DialogProvider.js'; } from './DialogProvider.js';
export {
default as ContextMenuProvider,
withContextMenu,
useContextMenu,
ContextMenuConsumer,
} from './ContextMenuProvider.js';
export {
default as LabellingFlowProvider,
withLabellingFlow,
useLabellingFlow,
LabellingFlowConsumer,
} from './LabellingFlowProvider.js';
+2
View File
@@ -62,6 +62,7 @@ import powerOff from './icons/power-off.svg';
import reset from './icons/reset.svg'; import reset from './icons/reset.svg';
import rotate from './icons/rotate.svg'; import rotate from './icons/rotate.svg';
import rotateRight from './icons/rotate-right.svg'; import rotateRight from './icons/rotate-right.svg';
import saveRegular from './icons/save-regular.svg';
import search from './icons/search.svg'; import search from './icons/search.svg';
import searchPlus from './icons/search-plus.svg'; import searchPlus from './icons/search-plus.svg';
import softTissue from './icons/soft-tissue.svg'; import softTissue from './icons/soft-tissue.svg';
@@ -164,6 +165,7 @@ const ICONS = {
'soft-tissue': softTissue, 'soft-tissue': softTissue,
lung, lung,
liver, liver,
save: saveRegular,
}; };
/** /**
@@ -0,0 +1,11 @@
<svg
aria-hidden="true"
focusable="false"
data-icon="save"
role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<path
fill="currentColor"
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM272 80v80H144V80h128zm122 352H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h42v104c0 13.255 10.745 24 24 24h176c13.255 0 24-10.745 24-24V83.882l78.243 78.243a6 6 0 0 1 1.757 4.243V426a6 6 0 0 1-6 6zM224 232c-48.523 0-88 39.477-88 88s39.477 88 88 88 88-39.477 88-88-39.477-88-88-88zm0 128c-22.056 0-40-17.944-40-40s17.944-40 40-40 40 17.944 40 40-17.944 40-40 40z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 738 B

+2 -16
View File
@@ -1,4 +1,5 @@
import { import {
ContextMenu,
Checkbox, Checkbox,
CineDialog, CineDialog,
ViewportDownloadForm, ViewportDownloadForm,
@@ -60,14 +61,6 @@ import {
ModalConsumer, ModalConsumer,
useModal, useModal,
withModal, withModal,
ContextMenuProvider,
ContextMenuConsumer,
useContextMenu,
withContextMenu,
LabellingFlowProvider,
LabellingFlowConsumer,
useLabellingFlow,
withLabellingFlow,
} from './contextProviders'; } from './contextProviders';
export { export {
@@ -80,6 +73,7 @@ export {
TextArea, TextArea,
TextInput, TextInput,
CineDialog, CineDialog,
ContextMenu,
ViewportDownloadForm, ViewportDownloadForm,
ExpandableToolMenu, ExpandableToolMenu,
Icon, Icon,
@@ -125,14 +119,6 @@ export {
DialogProvider, DialogProvider,
withDialog, withDialog,
useDialog, useDialog,
ContextMenuProvider,
ContextMenuConsumer,
useContextMenu,
withContextMenu,
LabellingFlowProvider,
LabellingFlowConsumer,
useLabellingFlow,
withLabellingFlow,
// Hooks // Hooks
useDebounce, useDebounce,
useMedia, useMedia,
+7 -1
View File
@@ -23,8 +23,14 @@ export function ToolbarButton(props) {
} }
}; };
const cypressSelectorId = props.label.toLowerCase();
return ( return (
<div className={className} onClick={handleClick}> <div
className={className}
onClick={handleClick}
data-cy={cypressSelectorId}
>
{iconProps && <Icon {...iconProps} />} {iconProps && <Icon {...iconProps} />}
<div className="toolbar-button-label"> <div className="toolbar-button-label">
{t(label)} {t(label)}
+280
View File
@@ -3,6 +3,286 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.4.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.5...@ohif/viewer@3.4.6) (2020-01-28)
**Note:** Version bump only for package @ohif/viewer
## [3.4.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.4...@ohif/viewer@3.4.5) (2020-01-27)
**Note:** Version bump only for package @ohif/viewer
## [3.4.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.3...@ohif/viewer@3.4.4) (2020-01-27)
**Note:** Version bump only for package @ohif/viewer
## [3.4.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.2...@ohif/viewer@3.4.3) (2020-01-24)
**Note:** Version bump only for package @ohif/viewer
## [3.4.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.1...@ohif/viewer@3.4.2) (2020-01-17)
**Note:** Version bump only for package @ohif/viewer
## [3.4.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.0...@ohif/viewer@3.4.1) (2020-01-15)
### Bug Fixes
* 🐛 Metadata is being mistakenly purged ([#1360](https://github.com/OHIF/Viewers/issues/1360)) ([b9a66d4](https://github.com/OHIF/Viewers/commit/b9a66d44241f2896ef184511287fb4984671e16d)), closes [#1326](https://github.com/OHIF/Viewers/issues/1326)
# [3.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.8...@ohif/viewer@3.4.0) (2020-01-14)
### Features
* Custom Healthcare API endpoint ([#1367](https://github.com/OHIF/Viewers/issues/1367)) ([a5d6bc6](https://github.com/OHIF/Viewers/commit/a5d6bc6a51784ed3a8a40d4ae773de9099f116b9))
## [3.3.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.7...@ohif/viewer@3.3.8) (2020-01-10)
**Note:** Version bump only for package @ohif/viewer
## [3.3.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.6...@ohif/viewer@3.3.7) (2020-01-08)
**Note:** Version bump only for package @ohif/viewer
## [3.3.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.5...@ohif/viewer@3.3.6) (2020-01-07)
**Note:** Version bump only for package @ohif/viewer
## [3.3.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.4...@ohif/viewer@3.3.5) (2020-01-06)
**Note:** Version bump only for package @ohif/viewer
## [3.3.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.3...@ohif/viewer@3.3.4) (2019-12-30)
**Note:** Version bump only for package @ohif/viewer
## [3.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.2...@ohif/viewer@3.3.3) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
## [3.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.1...@ohif/viewer@3.3.2) (2019-12-20)
**Note:** Version bump only for package @ohif/viewer
## [3.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.0...@ohif/viewer@3.3.1) (2019-12-20)
**Note:** Version bump only for package @ohif/viewer
# [3.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.2...@ohif/viewer@3.3.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [3.2.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.1...@ohif/viewer@3.2.2) (2019-12-19)
### Bug Fixes
* 🐛 Fix drag-n-drop of local files into OHIF ([#1319](https://github.com/OHIF/Viewers/issues/1319)) ([23305ce](https://github.com/OHIF/Viewers/commit/23305cec9c0f514e73a8dd17f984ffc87ad8d131)), closes [#1307](https://github.com/OHIF/Viewers/issues/1307)
## [3.2.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.0...@ohif/viewer@3.2.1) (2019-12-18)
**Note:** Version bump only for package @ohif/viewer
# [3.2.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.12...@ohif/viewer@3.2.0) (2019-12-16)
### Features
* 🎸 Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268)
## [3.1.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.11...@ohif/viewer@3.1.12) (2019-12-16)
### Bug Fixes
* 🐛 Dismiss all dialogs if leaving viewer route [#1242](https://github.com/OHIF/Viewers/issues/1242) ([#1301](https://github.com/OHIF/Viewers/issues/1301)) ([5c3d8b3](https://github.com/OHIF/Viewers/commit/5c3d8b37b6f723fbd8edcc447c37984e7eee8d40))
## [3.1.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.10...@ohif/viewer@3.1.11) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
## [3.1.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.9...@ohif/viewer@3.1.10) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
## [3.1.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.8...@ohif/viewer@3.1.9) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
## [3.1.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.7...@ohif/viewer@3.1.8) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
## [3.1.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.6...@ohif/viewer@3.1.7) (2019-12-13)
**Note:** Version bump only for package @ohif/viewer
## [3.1.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.5...@ohif/viewer@3.1.6) (2019-12-13)
**Note:** Version bump only for package @ohif/viewer
## [3.1.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.4...@ohif/viewer@3.1.5) (2019-12-13)
**Note:** Version bump only for package @ohif/viewer
## [3.1.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.3...@ohif/viewer@3.1.4) (2019-12-13)
**Note:** Version bump only for package @ohif/viewer
## [3.1.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.2...@ohif/viewer@3.1.3) (2019-12-12)
**Note:** Version bump only for package @ohif/viewer
## [3.1.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.1...@ohif/viewer@3.1.2) (2019-12-12)
**Note:** Version bump only for package @ohif/viewer
## [3.1.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.0...@ohif/viewer@3.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
# [3.1.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.3...@ohif/viewer@3.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [3.0.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.2...@ohif/viewer@3.0.3) (2019-12-11) ## [3.0.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.2...@ohif/viewer@3.0.3) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer **Note:** Version bump only for package @ohif/viewer
+2 -2
View File
@@ -1,10 +1,10 @@
{ {
"baseUrl": "http://localhost:3000", "baseUrl": "http://localhost:3000",
"video": false,
"chromeWebSecurity": false, "chromeWebSecurity": false,
"waitForAnimations": true, "waitForAnimations": true,
"defaultCommandTimeout": 10000, "defaultCommandTimeout": 10000,
"requestTimeout": 10000, "requestTimeout": 10000,
"responseTimeout": 10000, "responseTimeout": 10000,
"projectId": "4oe38f" "projectId": "4oe38f",
"video": false
} }
@@ -47,7 +47,6 @@ describe('OHIF Cornerstone Hotkeys', () => {
cy.get('body').type('I'); cy.get('body').type('I');
// Visual comparison // Visual comparison
cy.screenshot('Hotkey I - Should Invert Image'); cy.screenshot('Hotkey I - Should Invert Image');
cy.percyCanvasSnapshot('Hotkey I - Should Invert Image');
}); });
it('checks if hotkeys "+", "-" and "=" can zoom in, out and fit to viewport', () => { it('checks if hotkeys "+", "-" and "=" can zoom in, out and fit to viewport', () => {
@@ -77,14 +76,11 @@ describe('OHIF Cornerstone Hotkeys', () => {
// Visual comparison to make sure the 'inverted' image was reset // Visual comparison to make sure the 'inverted' image was reset
cy.screenshot('Hotkey SPACEBAR - Should Reset Image'); cy.screenshot('Hotkey SPACEBAR - Should Reset Image');
cy.percyCanvasSnapshot('Hotkey SPACEBAR - Should Reset Image');
}); });
it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => { it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => {
//Click on Layout button //Select viewport layout (3,1)
cy.get('@layoutBtn').click(); cy.setLayout(3, 1);
//Select 3 viewports
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
cy.waitViewportImageLoading(); cy.waitViewportImageLoading();
// Press multiples hotkeys on viewport #1 // Press multiples hotkeys on viewport #1
@@ -119,7 +119,6 @@ describe('OHIF Cornerstone Toolbar', () => {
// Visual comparison // Visual comparison
cy.screenshot('Pan tool moved the image inside the viewport'); cy.screenshot('Pan tool moved the image inside the viewport');
cy.percyCanvasSnapshot('Pan tool moved the image inside the viewport');
}); });
it('checks if Length annotation can be added on viewport and on measurements panel', () => { it('checks if Length annotation can be added on viewport and on measurements panel', () => {
@@ -266,7 +265,7 @@ describe('OHIF Cornerstone Toolbar', () => {
let iconName; let iconName;
//Click on one of the secondary tools from the overlay //Click on one of the secondary tools from the overlay
cy.get('.tooltip-inner > :nth-child(1)') cy.get('[data-cy="magnify"]')
.click() .click()
.then($magnifyBtn => { .then($magnifyBtn => {
cy.wrap($magnifyBtn) cy.wrap($magnifyBtn)
@@ -407,9 +406,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.as('toolbarOverlay') .as('toolbarOverlay')
.should('be.visible'); .should('be.visible');
//Click on Clear button //Click on Clear button
cy.get('.tooltip-inner') cy.get('[data-cy="clear"]').click();
.contains('Clear')
.click();
//Verify if measurements were removed from the measurements panel //Verify if measurements were removed from the measurements panel
cy.get('.measurementItem').should('not.exist'); cy.get('.measurementItem').should('not.exist');
@@ -446,9 +443,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.should('be.visible') .should('be.visible')
.then(() => { .then(() => {
//Click Eraser button //Click Eraser button
cy.get('.tooltip-inner') cy.get('[data-cy="eraser"]').click({ force: true });
.contains('Eraser')
.click({ force: true });
}); });
//Erase measurement #1 and Verify if it was removed from the measurements panel //Erase measurement #1 and Verify if it was removed from the measurements panel
@@ -471,13 +466,10 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible'); cy.get('.tooltip-toolbar-overlay').should('be.visible');
// Click on Invert button // Click on Invert button
cy.get('.tooltip-inner') cy.get('[data-cy="invert"]').click();
.contains('Invert')
.click();
// Visual comparison // Visual comparison
cy.screenshot('Invert tool - Should Invert Canvas'); cy.screenshot('Invert tool - Should Invert Canvas');
cy.percyCanvasSnapshot('Invert tool - Should Invert Canvas');
}); });
it('check if Rotate tool will change the image orientation in the viewport', () => { it('check if Rotate tool will change the image orientation in the viewport', () => {
@@ -488,14 +480,11 @@ describe('OHIF Cornerstone Toolbar', () => {
.should('be.visible') .should('be.visible')
.then(() => { .then(() => {
//Click on Rotate button //Click on Rotate button
cy.get('.tooltip-inner') cy.get('[data-cy="rotate right"]').click({ force: true });
.contains('Rotate')
.click({ force: true });
}); });
// Visual comparison // Visual comparison
cy.screenshot('Rotate tool - Should Rotate Image to Right'); cy.screenshot('Rotate tool - Should Rotate Image to Right');
cy.percyCanvasSnapshot('Rotate tool - Should Rotate Image to Right');
}); });
it('check if Flip H tool will flip the image horizontally in the viewport', () => { it('check if Flip H tool will flip the image horizontally in the viewport', () => {
@@ -505,13 +494,10 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible'); cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip H button //Click on Flip H button
cy.get('.tooltip-inner') cy.get('[data-cy="flip h"]').click();
.contains('Flip H')
.click();
// Visual comparison // Visual comparison
cy.screenshot('Flip H tool - Should Flip Image on Y axis'); cy.screenshot('Flip H tool - Should Flip Image on Y axis');
cy.percyCanvasSnapshot('Flip H tool - Should Flip Image on Y axis');
}); });
it('check if Flip V tool will flip the image vertically in the viewport', () => { it('check if Flip V tool will flip the image vertically in the viewport', () => {
@@ -521,12 +507,9 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible'); cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip V button //Click on Flip V button
cy.get('.tooltip-inner') cy.get('[data-cy="flip v"]').click();
.contains('Flip V')
.click();
// Visual comparison // Visual comparison
cy.screenshot('Flip V tool - Should Flip Image on X axis'); cy.screenshot('Flip V tool - Should Flip Image on X axis');
cy.percyCanvasSnapshot('Flip V tool - Should Flip Image on X axis');
}); });
}); });
@@ -0,0 +1,123 @@
describe('OHIF Download Snapshot File', () => {
before(() => {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(5);
});
beforeEach(() => {
cy.openDownloadImageModal();
});
afterEach(() => {
// Close modal
cy.get('[data-cy="close-button"]')
.scrollIntoView()
.click();
});
it('checks displayed information for Tablet experience', function() {
// Set Tablet resolution
cy.viewport(1000, 660);
// Visual comparison
cy.screenshot('Download Image Modal - Tablet experience');
});
it('checks displayed information for Desktop experience', function() {
// Set Desktop resolution
cy.viewport(1750, 720);
// Visual comparison
cy.screenshot('Download Image Modal - Desktop experience');
//Check if all elements are displayed
cy.get('[data-cy=modal-header]')
.as('downloadImageModal')
.should('contain.text', 'Download High Quality Image');
// Check input fields
cy.get('[data-cy=file-type]')
.select('png')
.should('have.value', 'png')
.select('jpg')
.should('have.value', 'jpg');
// Check image preview
cy.get('[data-cy="image-preview"]').should('contain.text', 'Image Preview');
cy.get('[data-cy="viewport-preview-img"]')
.should('have.attr', 'src')
.and('include', 'data:image');
// Check buttons
cy.get('[data-cy="cancel-btn"]')
.scrollIntoView()
.should('be.visible');
cy.get('[data-cy="download-btn"]')
.scrollIntoView()
.should('be.visible');
});
it('cancel changes on download modal', function() {
//Change Image Width, Filename and File Type
cy.get('[data-cy="image-width"]')
.clear()
.type('300');
cy.get('[data-cy="image-height"]') //Image Height should be the same as width
.should('have.value', '300');
cy.get('[data-cy="file-name"]')
.clear()
.type('new-filename');
cy.get('[data-cy=file-type]').select('png');
//Click on Cancel button
cy.get('[data-cy="cancel-btn"]')
.scrollIntoView()
.click();
//Check modal is closed
cy.get('[data-cy="modal"]').should('not.exist');
//Open Modal
cy.openDownloadImageModal();
//Verify default values was restored
cy.get('[data-cy="image-width"]').should('have.value', '512');
cy.get('[data-cy="file-name"]').should('have.value', 'image');
cy.get('[data-cy=file-type]').should('have.value', 'jpg');
});
// TO-DO once issue is fixed: https://github.com/OHIF/Viewers/issues/1217
// it('checks error messages for empty fields', function() {
// //Clear fields Image Width and Filename
// cy.get('[data-cy="image-width"]')
// .clear();
// cy.get('[data-cy="file-name"]').clear();
// //Click on Download button
// cy.get('[data-cy="download-btn"]')
// .scrollIntoView()
// .click();
// //Check error message
// });
it('checks if "Show Annotations" checkbox will display annotations', function() {
// Close modal that is initially opened
cy.get('[data-cy="close-button"]').click();
// Add measurements in the viewport
cy.addLengthMeasurement();
cy.addAngleMeasurement();
// Open Modal
cy.openDownloadImageModal();
// Select "Show Annotations" option
cy.get('[data-cy="show-annotations"]').check();
// Check image preview
cy.get('[data-cy="image-preview"]').scrollIntoView();
// Visual comparison
cy.screenshot('Download Image Modal - Show Annotations checked');
//Compare classes that exists on Image Preview with Annotations and Without Annotation
cy.get('[data-cy="modal-content"]')
.find('canvas')
.should('have.class', 'magnifyTool'); //Class "MagnifyTool" exists with annotations displayed on Image preview
// Uncheck "Show Annotations" option
cy.get('[data-cy="show-annotations"]')
.uncheck()
.wait(300);
// Check that class "MagnifyTool" should not exist
cy.get('[data-cy="modal-content"]')
.find('canvas')
.should('not.have.class', 'magnifyTool');
});
});
@@ -0,0 +1,21 @@
//We excluded the tests for '**/studies/**' because the bulk/all of our other study/viewer tests use this route
describe('OHIF Routes', function() {
beforeEach(function() {
cy.openStudyList();
});
it('checks PT/CT json url study route', function() {
cy.visit(
'/viewer?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/JSON/PTCTStudy.json'
);
cy.server();
cy.route('GET', '**/PTCTStudy/**').as('getPTCTStudy');
cy.wait('@getPTCTStudy.all');
cy.get('@getPTCTStudy').should($route => {
expect($route.status).to.be.eq(200);
});
});
});
@@ -4,9 +4,6 @@ describe('OHIFStandaloneViewer', () => {
}); });
it('loads route with at least 2 rows', () => { it('loads route with at least 2 rows', () => {
cy.screenshot('Study List');
cy.percyCanvasSnapshot('Study List');
cy.get('[data-cy="study-list-results"] tr') cy.get('[data-cy="study-list-results"] tr')
.its('length') .its('length')
.should('be.gt', 2); .should('be.gt', 2);
@@ -1,14 +1,14 @@
//We are keeping the hardcoded results values for the study list tests //We are keeping the hardcoded results values for the study list tests
//this is intended to be running in a controled docker environment with test data. //this is intended to be running in a controled docker environment with test data.
describe('OHIF Study List', function () { describe('OHIF Study List', function() {
context('Desktop resolution', function () { context('Desktop resolution', function() {
beforeEach(function () { beforeEach(function() {
cy.viewport(1750, 720); cy.viewport(1750, 720);
cy.openStudyList(); cy.openStudyList();
cy.initStudyListAliasesOnDesktop(); cy.initStudyListAliasesOnDesktop();
}); });
it('searches Patient Name with exact string', function () { it('searches Patient Name with exact string', function() {
cy.get('@patientName').type('Juno'); cy.get('@patientName').type('Juno');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
@@ -18,7 +18,7 @@ describe('OHIF Study List', function () {
}); });
}); });
it('searches MRN with exact string', function () { it('searches MRN with exact string', function() {
cy.get('@MRN').type('ProstateX-0000'); cy.get('@MRN').type('ProstateX-0000');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
@@ -28,7 +28,7 @@ describe('OHIF Study List', function () {
}); });
}); });
it('searches Accession with exact string', function () { it('searches Accession with exact string', function() {
cy.get('@accessionNumber').type('fpcben98890'); cy.get('@accessionNumber').type('fpcben98890');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
@@ -38,17 +38,17 @@ describe('OHIF Study List', function () {
}); });
}); });
it('searches Modality with camel case', function () { it('searches Modality with camel case', function() {
cy.get('@modalities').type('Mr'); cy.get('@modalities').type('Mr');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
cy.get('@searchResult').should($list => { cy.get('@searchResult').should($list => {
expect($list.length).to.be.eq(18); expect($list.length).to.be.eq(16);
expect($list).to.contain('MR'); expect($list).to.contain('MR');
}); });
}); });
it('searches Description with exact string', function () { it('searches Description with exact string', function() {
cy.get('@studyDescription').type('CHEST'); cy.get('@studyDescription').type('CHEST');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
@@ -58,7 +58,7 @@ describe('OHIF Study List', function () {
}); });
}); });
it('changes rows per page and checks the study count', function () { it('changes rows per page and checks the study count', function() {
//Show rows per page options //Show rows per page options
const pageRows = [25, 50, 100]; const pageRows = [25, 50, 100];
@@ -100,14 +100,14 @@ describe('OHIF Study List', function () {
// }); // });
}); });
context('Tablet resolution', function () { context('Tablet resolution', function() {
beforeEach(function () { beforeEach(function() {
cy.viewport(1000, 660); cy.viewport(1000, 660);
cy.openStudyList(); cy.openStudyList();
cy.initStudyListAliasesOnTablet(); cy.initStudyListAliasesOnTablet();
}); });
it('searches Patient Name with exact string', function () { it('searches Patient Name with exact string', function() {
cy.get('@patientNameOrMRN').type('Juno'); cy.get('@patientNameOrMRN').type('Juno');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
@@ -117,7 +117,7 @@ describe('OHIF Study List', function () {
}); });
}); });
it('searches MRN with with exact string', function () { it('searches MRN with with exact string', function() {
cy.get('@patientNameOrMRN').type('ProstateX-0000'); cy.get('@patientNameOrMRN').type('ProstateX-0000');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
@@ -127,17 +127,17 @@ describe('OHIF Study List', function () {
}); });
}); });
it('searches Modality with exact string', function () { it('searches Modality with exact string', function() {
cy.get('@accessionModalityDescription').type('MR'); cy.get('@accessionModalityDescription').type('MR');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
cy.get('@searchResult').should($list => { cy.get('@searchResult').should($list => {
expect($list.length).to.be.eq(18); expect($list.length).to.be.eq(16);
expect($list).to.contain('MR'); expect($list).to.contain('MR');
}); });
}); });
it('searches Accession with exact string', function () { it('searches Accession with exact string', function() {
cy.get('@accessionModalityDescription').type('fpcben98890'); cy.get('@accessionModalityDescription').type('fpcben98890');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
@@ -147,7 +147,7 @@ describe('OHIF Study List', function () {
}); });
}); });
it('searches Description with exact string', function () { it('searches Description with exact string', function() {
cy.get('@accessionModalityDescription').type('CHEST'); cy.get('@accessionModalityDescription').type('CHEST');
//Wait result list to be displayed //Wait result list to be displayed
cy.waitStudyList(); cy.waitStudyList();
@@ -157,7 +157,7 @@ describe('OHIF Study List', function () {
}); });
}); });
it('changes rows per page and checks the study count', function () { it('changes rows per page and checks the study count', function() {
//Show rows per page options //Show rows per page options
const pageRows = [25, 50, 100]; const pageRows = [25, 50, 100];
@@ -12,7 +12,6 @@ describe('OHIF Study Viewer Page', function() {
it('checks if series thumbnails are being displayed', function() { it('checks if series thumbnails are being displayed', function() {
cy.screenshot('Series Thumbnails - Should Display Thumbnails'); cy.screenshot('Series Thumbnails - Should Display Thumbnails');
cy.percyCanvasSnapshot('Series Thumbnails - Should Display Thumbnails');
cy.get('[data-cy="thumbnail-list"]') cy.get('[data-cy="thumbnail-list"]')
.its('length') .its('length')
@@ -293,7 +292,6 @@ describe('OHIF Study Viewer Page', function() {
// Visual comparison // Visual comparison
cy.screenshot('About modal - Should display modal'); cy.screenshot('About modal - Should display modal');
cy.percyCanvasSnapshot('About modal - Should display modal');
//close modal //close modal
cy.get('[data-cy="close-button"]').click(); cy.get('[data-cy="close-button"]').click();
@@ -24,10 +24,7 @@ describe('OHIF User Preferences', () => {
// Visual comparison // Visual comparison
cy.screenshot( cy.screenshot(
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page' 'User Preferences Modal - Hotkeys tab initial state in Study List page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
); );
cy.get('[data-cy="close-button"]').click(); cy.get('[data-cy="close-button"]').click();
}); });
@@ -42,16 +39,10 @@ describe('OHIF User Preferences', () => {
// Visual comparison // Visual comparison
cy.screenshot( cy.screenshot(
'User Preferences Modal - General tab initial state in Study Viewer page' 'User Preferences Modal - General tab initial state in Study List page'
); );
cy.percyCanvasSnapshot( // Set language to Spanish and save
'User Preferences Modal - General tab initial state in Study Viewer page' cy.setLanguage('Spanish');
);
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Header should be translated to Spanish // Header should be translated to Spanish
cy.get('.research-use') cy.get('.research-use')
@@ -161,11 +152,8 @@ describe('OHIF User Preferences', () => {
// Go to General tab // Go to General tab
cy.get('@userPreferencesGeneralTab').click(); cy.get('@userPreferencesGeneralTab').click();
// Select Spanish and Save // Set language to Spanish
cy.get('#language-select').select('Spanish'); cy.setLanguage('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Go to Study Viewer page // Go to Study Viewer page
cy.openStudy('MISTER^MR'); cy.openStudy('MISTER^MR');
@@ -191,7 +179,9 @@ describe('OHIF User Preferences', () => {
.should('contain.text', 'Preferencias'); .should('contain.text', 'Preferencias');
// Check if new hotkey is working on viewport // Check if new hotkey is working on viewport
cy.get('body').type('{shift}Q', { release: false }); cy.get('body').type('{shift}Q', {
release: false,
});
cy.get('@viewportInfoMidTop').should('contains.text', 'R'); cy.get('@viewportInfoMidTop').should('contains.text', 'R');
}); });
}); });
@@ -225,10 +215,7 @@ describe('OHIF User Preferences', () => {
// Visual comparison // Visual comparison
cy.screenshot( cy.screenshot(
'User Preferences Modal - Hotkeys tab initial state in Study List page' 'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in Study List page'
); );
cy.get('[data-cy="close-button"]').click(); //close User Preferences modal cy.get('[data-cy="close-button"]').click(); //close User Preferences modal
}); });
@@ -243,16 +230,10 @@ describe('OHIF User Preferences', () => {
// Visual comparison // Visual comparison
cy.screenshot( cy.screenshot(
'User Preferences Modal - General tab initial state in Study List page' 'User Preferences Modal - General tab initial state in Study Viewer page'
); );
cy.percyCanvasSnapshot( // Set language to Spanish
'User Preferences Modal - General tab initial state in Study List page' cy.setLanguage('Spanish');
);
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Header should be translated to Spanish // Header should be translated to Spanish
cy.get('.research-use') cy.get('.research-use')
@@ -310,19 +291,11 @@ describe('OHIF User Preferences', () => {
// Language dropdown should be displayed // Language dropdown should be displayed
cy.get('#language-select').should('be.visible'); cy.get('#language-select').should('be.visible');
// Select Spanish and Save // Set language to Spanish
cy.get('#language-select').select('Spanish'); cy.setLanguage('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
//Open Preferences again // Open User Preferences modal
cy.get('[data-cy="options-menu"]') cy.openPreferences();
.scrollIntoView()
.click();
cy.get('[data-cy="dd-item-menu"]')
.last()
.click();
// Go to general tab // Go to general tab
cy.get('@userPreferencesGeneralTab').click(); cy.get('@userPreferencesGeneralTab').click();
@@ -437,9 +410,7 @@ describe('OHIF User Preferences', () => {
// Visual comparison // Visual comparison
cy.screenshot('Viewport Navigation - 2nd viewport inverted and rotated'); cy.screenshot('Viewport Navigation - 2nd viewport inverted and rotated');
cy.percyCanvasSnapshot(
'Viewport Navigation - 2nd viewport inverted and rotated'
);
// Set 1 viewport layout // Set 1 viewport layout
cy.setLayout(1, 1); cy.setLayout(1, 1);
}); });
@@ -1,24 +0,0 @@
describe('ViewerRouting', () => {
beforeEach(() => {
cy.visit('/');
cy.contains('Study List');
cy.get(
'[data-cy="study-list-results"]> :nth-child(1) > .patientId'
).click();
});
// it('thumbnails list has more than 2 items', () => {
// cy.get('.scrollable-study-thumbnails div.thumbnail-container')
// .its('length')
// .should('be.gte', 2);
// });
// it('loads route with at least 2 thumbnails', () => {
// cy.get(
// ':nth-child(1) > .ThumbnailEntry > .p-x-1 > .ImageThumbnail > .image-thumbnail-canvas > canvas'
// ).should('be.visible');
// cy.get(
// ':nth-child(2) > .ThumbnailEntry > .p-x-1 > .ImageThumbnail > .image-thumbnail-canvas > canvas'
// ).should('be.visible');
// });
});
@@ -23,8 +23,5 @@ describe('OHIF Microscopy Extension', () => {
cy.wait(3000); //Waiting for image to render before taking the snapshot cy.wait(3000); //Waiting for image to render before taking the snapshot
// Visual comparison // Visual comparison
cy.screenshot('Microscopy Extension - Should display loaded canvas'); cy.screenshot('Microscopy Extension - Should display loaded canvas');
cy.percyCanvasSnapshot(
'Microscopy Extension - Should display loaded canvas'
);
}); });
}); });
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more