Compare commits

...
Author SHA1 Message Date
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
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
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
ohif-bot 5cda5159da chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.2
 - @ohif/viewer@3.0.3
2019-12-11 15:41:29 +00:00
Danny Brown 115a3ec0b0 refactor: remove-labelling-manager
Igor/refactor/1267 remove labelling manager
2019-12-11 10:39:36 -05:00
Danny Brown ac82a111e7 Merge branch 'master' into igor/refactor/1267-remove-labelling-manager 2019-12-11 09:16:18 -05:00
Igor a22fd67e13 Fix config check in extension init 2019-12-11 06:54:41 -03:00
Danny Brown 91f0af058f Merge pull request #1278 from OHIF/dannyrb/ci/visual-test
ci: make sure we're updating the base branch for comparisons (percy)
2019-12-10 23:58:53 -05:00
dannyrb 5a1186388d Merge branch 'dannyrb/ci/visual-test' of github.com:OHIF/Viewers into dannyrb/ci/visual-test 2019-12-10 23:52:54 -05:00
dannyrb 2f0f92c095 ci: make sure we're updating the base branch for comparisons (percy) 2019-12-10 23:45:29 -05:00
Danny Brown 5c686268ab Merge pull request #1274 from OHIF/refactor/igor/1267-remove-labelling-manager
refactor: 💡 Remove LabellingManager and Cleanup LabellingFlow #1267
2019-12-10 23:24:14 -05:00
Danny Brown 974aabc9b2 Merge pull request #1276 from OHIF/dannyrb/ci/visual-test
ci: optional visual test step
2019-12-10 23:12:30 -05:00
Danny Brown 17e3b49b06 Merge branch 'master' into dannyrb/ci/visual-test 2019-12-10 23:08:09 -05:00
dannyrb e3c119489a cI: Better job name for percy 2019-12-10 23:07:54 -05:00
dannyrb c91b6dddd8 ci: optional visual test step 2019-12-10 22:17:21 -05:00
Igor 92548191e4 refactor: 💡 Remove LabellingManager and Clean LabellingFlow
Migrate logic from LabellingManager to LabellingFlow and cleanup
LabellingFlow component

Closes: #1267
2019-12-10 18:31:55 -03:00
Igor 68fcaef19a Cleanup labelling flow 2019-12-10 18:18:37 -03:00
Igor ef611e4849 Refactoring labellingflow to function component 2019-12-10 18:08:51 -03:00
Igor d128fd6bf0 Remove LabellingManager component 2019-12-10 17:41:43 -03:00
Igor 3bdd434488 Cleanup LabellingFlow and LabellingManager components 2019-12-10 17:29:40 -03:00
116 changed files with 3131 additions and 3137 deletions

No files matched your search

+60 -27
View File
@@ -13,14 +13,7 @@ version: 2.1
##
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@1.11.0
executors:
# Custom executor to override Cypress config
deploy-to-prod-executor:
docker:
- image: 'cypress/browsers:node10.16.0-chrome77'
environment:
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/
cypress: cypress-io/cypress@1.13.0
defaults: &defaults
docker:
@@ -341,6 +334,8 @@ workflows:
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires:
- UNIT_TESTS
# E2E: script-tag
@@ -364,9 +359,39 @@ workflows:
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires:
- UNIT_TESTS
PR_OPTIONAL_VISUAL_TESTS:
jobs:
- AWAIT_APPROVAL:
type: approval
# Update hub.docker.org
- cypress/run:
name: 'Generate Percy Snapshots'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires:
- AWAIT_APPROVAL
PR_OPTIONAL_DOCKER_PUBLISH:
jobs:
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
@@ -419,25 +444,33 @@ workflows:
filters:
branches:
only: master
# Update base branch snapshots
# and record a Cypress dashboard test run
- cypress/run:
name: 'Generate Percy Snapshots'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
- store_test_results:
path: platform/viewer/cypress/results
filters:
branches:
only: master
- DOCKER_MASTER_PUBLISH:
requires:
- NPM_PUBLISH
#
#
# Potentially use to E2E test STAGING environment
# before publishing to production
# - cypress/run:
# name: 'E2E: PWA'
# executor: deploy-to-prod-executor
# browser: chrome
# working_directory: platform/viewer
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
# yarn: true
# record: false
# no-workspace: true
# store_artifacts: true
# post-steps:
# - store_artifacts:
# path: platform/viewer/cypress/screenshots
# - store_test_results:
# path: cypress/results
+1
View File
@@ -30,6 +30,7 @@ docker/dcm4che/dcm4che-arc
# Cypress test results
videos/
screenshots/
# Locize settings
.locize
+5
View File
@@ -72,12 +72,17 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
},
plugins: [
new webpack.DefinePlugin({
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
/* 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
-2
View File
@@ -1,7 +1,6 @@
# OHIF Viewers
- [Our Process](our-process.md)
- [Quick Start](quick-start.md)
- Development
- [Getting Started](development/getting-started.md)
- [Contributing](development/contributing.md)
@@ -31,7 +30,6 @@
- [OHIF Maintained](extensions/index.md#maintained-extensions)
- [Services](services/index.md)
- [Default](services/default/index.md)
- [Measurements](services/default/measurements.md)
- [UI](services/ui/index.md)
- [Dialog Service](services/ui/ui-dialog-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`
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
const commandsManager = new CommandsManager();
@@ -201,6 +202,7 @@ const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
appConfig
});
```
@@ -210,8 +212,8 @@ The `ExtensionManager` only has a few public members:
- `registerExtensions` - Registers an array of extensions
- `modules` - An object containing registered extensions by `MODULE_TYPE`
During registration, lifecycle hooks and modules have access to
`ExtensionManager`'s `ServicesManager` and `CommandsManager`.
During registration, lifecycle hooks and modules have access to the extension's config,
the application's config and `ExtensionManager`'s `ServicesManager` and `CommandsManager` instances.
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
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
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.
These can be delivered to your end users however you would like:
+65 -1
View File
@@ -1,3 +1,67 @@
# 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({
servicesManager = {},
commandsManager = {},
appConfig = {},
configuration = {},
}) {},
+52
View File
@@ -3,6 +3,58 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
**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
### Repositories
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.0.1",
"version": "2.2.1",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -32,9 +32,9 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -2,6 +2,7 @@ import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll');
@@ -242,6 +243,9 @@ const commandsModule = ({ servicesManager }) => {
cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element);
},
setCornerstoneLayout: () => {
setCornerstoneLayout();
}
};
const definitions = {
@@ -348,6 +352,12 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
setCornerstoneLayout: {
commandFn: actions.setCornerstoneLayout,
storeContexts: [],
options: {},
context: 'VIEWER',
},
};
return {
+87 -41
View File
@@ -1,10 +1,10 @@
import OHIF from '@ohif/core';
import { SimpleDialog } from '@ohif/ui';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
import queryString from 'query-string';
import { SimpleDialog } from '@ohif/ui';
import merge from 'lodash.merge';
import queryString from 'query-string';
import initCornerstoneTools from './initCornerstoneTools.js';
function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) {
@@ -76,33 +76,38 @@ export default function init({ servicesManager, configuration }) {
initCornerstoneTools(defaultCsToolsConfig);
// ~~ Toooools 🙌
const tools = [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
// Touch
csTools.PanMultiTouchTool,
csTools.ZoomTouchPinchTool,
// Annotations
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
// Segmentation
csTools.BrushTool,
];
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
segmentation: [csTools.BrushTool],
other: [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
],
};
let tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
/* Add extension tools configuration here. */
const extensionToolsConfiguration = {
const internalToolsConfig = {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
@@ -113,19 +118,60 @@ export default function init({ servicesManager, configuration }) {
},
};
const isEmpty = obj => Object.keys(obj).length < 1;
if (!isEmpty(configuration.tools) || !isEmpty(extensionToolsConfiguration)) {
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const configurationToolProps = configuration.tools[toolName] || {};
const extensionToolProps = extensionToolsConfiguration[toolName];
let props = merge(extensionToolProps, configurationToolProps);
csTools.addTool(tool, props);
});
} else {
tools.forEach(tool => csTools.addTool(tool));
}
/* 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. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const externalToolsConfig = configuration.tools || {};
const externalToolProps = externalToolsConfig[toolName] || {};
const internalToolProps = internalToolsConfig[toolName] || {};
const props = merge(
internalToolProps,
parseToolProps(externalToolProps, tool)
);
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('Zoom', { mouseButtonMask: 2 });
@@ -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 {
@@ -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.
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)
**Note:** Version bump only for package @ohif/extension-dicom-html
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-html",
"version": "1.0.2",
"version": "1.1.0",
"description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"prop-types": "^15.6.2",
"react": "^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.
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)
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-pdf",
"version": "1.0.0",
"version": "1.0.1",
"description": "OHIF extension for Dicom PDF",
"author": "OHIF",
"license": "MIT",
@@ -37,7 +37,8 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"lodash.isequal": "^4.5.0"
"lodash.isequal": "^4.5.0",
"pdfjs-dist": "^2.2.228"
},
"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 PDFJS from 'pdfjs-dist';
import PropTypes from 'prop-types';
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
const SOP_CLASS_UIDS = {
@@ -8,16 +16,88 @@ const SOP_CLASS_UIDS = {
};
class DicomPDFViewport extends Component {
state = {
fileURL: null,
error: null,
};
constructor(props) {
super(props);
this.state = {
fileURL: null,
error: null,
currentPageIndex: 1,
pdf: null,
scale: 1,
};
this.canvas = createRef();
this.textLayer = createRef();
}
static propTypes = {
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;
if (dataSet) {
@@ -36,50 +116,144 @@ class DicomPDFViewport extends Component {
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(PDF);
this.setState({
fileURL,
});
return 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 => {
const options = {
untilTag: '',
};
const options = { untilTag: '' };
let dataSet;
try {
dataSet = dicomParser.parseDicom(byteArray, options);
} catch (error) {
this.setState({
error,
});
this.setState(state => ({ ...state, error }));
}
return dataSet;
};
componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
setViewportActiveHandler = () => {
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() {
const { fileURL, pdf, error } = this.state;
return (
<div
className={'DicomPDFViewport'}
onClick={this.setViewportActiveHandler}
onScroll={this.setViewportActiveHandler}
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
data={this.state.fileURL}
aria-label="PDF Viewer"
data={fileURL}
type="application/pdf"
width="100%"
height="100%"
/>
)}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
{error && <h2>{JSON.stringify(error)}</h2>}
</div>
);
}
@@ -11,6 +11,9 @@ class OHIFDicomPDFViewport extends Component {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
setViewportActive: PropTypes.func,
};
state = {
@@ -30,42 +33,36 @@ class OHIFDicomPDFViewport extends Component {
componentDidMount() {
const { displaySet, studies } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders,
} = displaySet;
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => {
const byteArray = new Uint8Array(data);
this.setState({
byteArray: byteArray,
});
},
data => this.setState({ byteArray: new Uint8Array(data) }),
error => {
this.setState({
error,
});
this.setState({ error });
throw new Error(error);
}
);
}
render() {
const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
const { byteArray, error } = this.state;
const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy };
return (
<OHIFComponentPlugin {...pluginProps}>
{this.state.byteArray && (
<DicomPDFViewport byteArray={this.state.byteArray} />
{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>
);
}
@@ -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 OHIFDicomPDFViewport from './OHIFDicomPDFViewport.js';
const ConnectedOHIFDicomPDFViewer = asyncComponent(() =>
import(
/* webpackChunkName: "ConnectedOHIFDicomPDFViewer" */ './ConnectedOHIFDicomPDFViewer'
)
);
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'pdf',
getViewportModule() {
return OHIFDicomPDFViewport;
return ConnectedOHIFDicomPDFViewer;
},
getSopClassHandlerModule() {
return OHIFDicomPDFSopClassHandler;
}
},
};
+121
View File
@@ -3,6 +3,127 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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)
**Note:** Version bump only for package @ohif/extension-vtk
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.0.2",
"version": "1.2.5",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -34,7 +34,7 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
@@ -52,11 +52,10 @@
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^2.0.2",
"@ohif/ui": "^1.0.1",
"cornerstone-tools": "^4.8.0",
"@ohif/core": "^2.3.2",
"@ohif/ui": "^1.1.7",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1",
"i18next": "^17.0.3",
@@ -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 VTKMPRToolbarButton from './toolbarComponents/VTKMPRToolbarButton';
const TOOLBAR_BUTTON_TYPES = {
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 {
definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK',
};
}
+68
View File
@@ -3,6 +3,74 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.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)
**Note:** Version bump only for package @ohif/core
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.0.2",
"version": "2.3.2",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -32,13 +32,14 @@
},
"peerDependencies": {
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"ajv": "^6.10.0",
"dcmjs": "^0.8.3",
"dicomweb-client": "^0.5.2",
"isomorphic-base64": "^1.0.2",
"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';
export default class ExtensionManager {
constructor({ commandsManager, servicesManager }) {
constructor({ commandsManager, servicesManager, appConfig = {} }) {
this.modules = {};
this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES);
//
this._commandsManager = commandsManager;
this._servicesManager = servicesManager;
this._appConfig = appConfig;
this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = [];
@@ -70,6 +71,7 @@ export default class ExtensionManager {
extension.preRegistration({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
appConfig: this._appConfig,
configuration,
});
}
@@ -79,7 +81,8 @@ export default class ExtensionManager {
const extensionModule = this._getExtensionModule(
moduleType,
extension,
extensionId
extensionId,
configuration
);
if (extensionModule) {
@@ -102,7 +105,7 @@ export default class ExtensionManager {
* @param {Object} extension
* @param {string} extensionId - Used for logging warnings
*/
_getExtensionModule(moduleType, extension, extensionId) {
_getExtensionModule(moduleType, extension, extensionId, configuration) {
const getModuleFnName = 'get' + _capitalizeFirstCharacter(moduleType);
const getModuleFn = extension[getModuleFnName];
@@ -114,6 +117,8 @@ export default class ExtensionManager {
const extensionModule = getModuleFn({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
appConfig: this._appConfig,
configuration,
});
if (!extensionModule) {
@@ -6,7 +6,7 @@ import log from './../log.js';
jest.mock('./../log.js');
describe('ExtensionManager.js', () => {
let extensionManager, commandsManager;
let extensionManager, commandsManager, servicesManager, appConfig;
beforeEach(() => {
commandsManager = {
@@ -14,7 +14,17 @@ describe('ExtensionManager.js', () => {
getContext: 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();
jest.clearAllMocks();
});
@@ -51,7 +61,7 @@ describe('ExtensionManager.js', () => {
extensionManager.registerExtensions(fakeExtensions);
// Assert
expect(extensionManager.registerExtension.mock.calls[1]).toContain(
expect(extensionManager.registerExtension.mock.calls[1][1]).toEqual(
fakeConfiguration
);
});
@@ -67,19 +77,19 @@ describe('ExtensionManager.js', () => {
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
});
it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => {
const configuration = { config: 'Some configuration' };
extensionManager._servicesManager = { services: { TestService: {} } };
it('calls preRegistration() passing dependencies and extension configuration to extension', () => {
const extensionConfiguration = { config: 'Some configuration' };
// SUT
const fakeExtension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(fakeExtension, configuration);
const extension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(extension, extensionConfiguration);
// Assert
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
commandsManager: extensionManager._commandsManager,
configuration,
expect(extension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager,
commandsManager,
appConfig,
configuration: extensionConfiguration,
});
});
@@ -153,8 +163,8 @@ describe('ExtensionManager.js', () => {
);
});
it('successfully passes a servicesManager and commandsManager instances to each module', () => {
extensionManager._servicesManager = { services: { TestService: {} } };
it('successfully passes dependencies to each module along with extension configuration', () => {
const extensionConfiguration = { testing: true };
const extension = {
id: 'hello-world',
@@ -165,11 +175,17 @@ describe('ExtensionManager.js', () => {
getCommandsModule: jest.fn(),
};
extensionManager.registerExtension(extension);
extensionManager.registerExtension(extension, extensionConfiguration);
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
commandsManager: extensionManager._commandsManager,
Object.keys(extension).forEach(module => {
if (typeof extension[module] === 'function') {
expect(extension[module].mock.calls[0][0]).toEqual({
servicesManager,
commandsManager,
appConfig,
configuration: extensionConfiguration,
});
}
});
});
@@ -210,7 +226,7 @@ describe('ExtensionManager.js', () => {
return {
definitions: {
exampleDefinition: {
commandFn: () => {},
commandFn: () => { },
storeContexts: [],
options: {},
},
+3 -6
View File
@@ -5,6 +5,7 @@ import { ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes/';
import DICOMWeb from './DICOMWeb';
import DICOMSR from './DICOMSR';
import cornerstone from './cornerstone.js';
import hangingProtocols from './hanging-protocols';
import header from './header.js';
@@ -23,8 +24,6 @@ import {
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
} from './services';
const OHIF = {
@@ -48,6 +47,7 @@ const OHIF = {
object,
log,
DICOMWeb,
DICOMSR,
viewer: {},
measurements,
hangingProtocols,
@@ -55,8 +55,6 @@ const OHIF = {
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
};
export {
@@ -80,14 +78,13 @@ export {
object,
log,
DICOMWeb,
DICOMSR,
measurements,
hangingProtocols,
//
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
};
export { OHIF };
+1 -2
View File
@@ -13,8 +13,6 @@ describe('Top level exports', () => {
'UINotificationService',
'UIModalService',
'UIDialogService',
'UIContextMenuService',
'UILabellingFlowService',
//
'utils',
'studies',
@@ -30,6 +28,7 @@ describe('Top level exports', () => {
'object',
'log',
'DICOMWeb',
'DICOMSR',
'OHIF', //
'measurements',
'hangingProtocols',
@@ -1,4 +1,5 @@
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import log from '../../log';
import getLabel from '../lib/getLabel';
import getDescription from '../lib/getDescription';
@@ -7,13 +8,32 @@ import guid from '../../utils/guid';
import studyMetadataManager from '../../utils/studyMetadataManager';
import { measurementApiDefaultConfig } from './../configuration.js';
const configuration = {
...measurementApiDefaultConfig,
...measurementApiDefaultConfig
};
export default class MeasurementApi {
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) {
Object.assign(configuration, config);
}
@@ -203,13 +223,14 @@ export default class MeasurementApi {
retrieveMeasurements(patientId, timepointIds) {
const retrievalFn = configuration.dataExchange.retrieve;
const { server } = configuration;
if (typeof retrievalFn !== 'function') {
log.error('Measurement retrieval function has not been configured.');
return;
}
return new Promise((resolve, reject) => {
retrievalFn(patientId, timepointIds).then(measurementData => {
retrievalFn(server).then(measurementData => {
if (measurementData) {
log.info('Measurement data retrieval');
log.info(measurementData);
@@ -230,6 +251,10 @@ export default class MeasurementApi {
// Synchronize the new tool data
this.syncMeasurementsAndToolData();
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
// Let others know that the measurements are updated
this.onMeasurementsUpdated();
}, reject);
@@ -237,6 +262,7 @@ export default class MeasurementApi {
}
storeMeasurements(timepointId) {
const { server } = configuration;
const storeFn = configuration.dataExchange.store;
if (typeof storeFn !== 'function') {
log.error('Measurement store function has not been configured.');
@@ -278,8 +304,9 @@ export default class MeasurementApi {
};
log.info('Saving Measurements for timepoints:', timepoints);
return storeFn(measurementData, filter).then(() => {
return storeFn(measurementData, filter, server).then(result => {
log.info('Measurement storage completed');
return result;
});
}
@@ -33,7 +33,9 @@ export default class ServicesManager {
}
if (service.create) {
this.services[service.name] = service.create({ configuration });
this.services[service.name] = service.create({
configuration,
});
} else {
log.warn(`Service create factory function not defined. Exiting early.`);
return;
@@ -33,7 +33,7 @@ describe('ServicesManager.js', () => {
[{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration],
]);
expect(servicesManager.registerService.mock.calls[1]).toContain(
expect(servicesManager.registerService.mock.calls[1][1]).toEqual(
fakeConfiguration
);
});
@@ -85,7 +85,7 @@ describe('ServicesManager.js', () => {
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' };
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 UIModalService from './UIModalService';
import UIDialogService from './UIDialogService';
import UIContextMenuService from './UIContextMenuService';
import UILabellingFlowService from './UILabellingFlowService';
export {
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
ServicesManager,
};
@@ -40,7 +40,9 @@ export default class RetrieveMetadataLoader {
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
} catch (e) { }
} catch (e) {
throw e;
}
}
if (loaders.next().done && !result) {
@@ -51,8 +53,8 @@ export default class RetrieveMetadataLoader {
}
// Methods to be overwrite
async configLoad() { }
async preLoad() { }
async load(preLoadData) { }
async posLoad(loadData) { }
async configLoad() {}
async preLoad() {}
async load(preLoadData) {}
async posLoad(loadData) {}
}
-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.
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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.52.2",
"version": "0.52.4",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
+9 -8
View File
@@ -29,19 +29,20 @@ function addLocales(newLocales) {
customDebug(resourceBundle, 'info');
}
// Note: Developers can add the API key to use the in-context editor.
// DO NOT commit the API key
const config = (window.config && window.config.i18n) || {};
/*
* Note: Developers can add the API key to use the
* in-context editor using environment variables.
* (DO NOT commit the API key)
*/
const locizeOptions = {
projectId: config.LOCIZE_PROJECTID,
apiKey: config.LOCIZE_API_KEY,
projectId: process.env.LOCIZE_PROJECTID,
apiKey: process.env.LOCIZE_API_KEY,
referenceLng: 'en-US',
fallbacklng: 'en-US',
};
const envUseLocize = !!config.USE_LOCIZE;
const envApiKeyAvailable = !!config.LOCIZE_API_KEY;
const envUseLocize = !!process.env.USE_LOCIZE;
const envApiKeyAvailable = !!process.env.LOCIZE_API_KEY;
const DEFAULT_LANGUAGE = 'en-US';
function initI18n(
@@ -10,6 +10,7 @@
"Clear": "Clear",
"Coronal": "Coronal",
"Crosshairs": "Crosshairs",
"Download": "Download",
"Ellipse": "Ellipse",
"Elliptical": "Elliptical",
"Flip H": "Flip H",
+10 -9
View File
@@ -10,18 +10,19 @@
"Clear": "Limpiar",
"Coronal": "Coronal",
"Crosshairs": "Punto de mira",
"Download": "Descargar",
"Ellipse": "Elipse",
"Elliptical": "Elíptico",
"Flip H": "Espejo H",
"Flip V": "Espejo V",
"Flip H": "Voltear H",
"Flip V": "Voltear V",
"Freehand": "Mano alzada",
"Invert": "Invertir",
"Invert": "Negativo",
"Layout": "$t(Common:Layout)",
"Length": "Longitud",
"Levels": "Niveles",
"Magnify": "Aumentar",
"Levels": "W/L",
"Magnify": "Lupa",
"Manual": "Manual",
"Measurements": "Mediciones",
"Measurements": "Medidas",
"More": "$t(Common:More)",
"Next": "$t(Common:Next)",
"Pan": "Mover",
@@ -32,11 +33,11 @@
"Rectangle": "Rectángulo",
"Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:Reset) por defecto",
"Rotate Right": "Rotar ->",
"Rotate Right": "Girar ->",
"Sagittal": "Sagital",
"Save": "Guardar",
"Stack Scroll": "Scroll en pila",
"Stack Scroll": "Scroll",
"Stop": "$t(Common:Stop)",
"Themes": "Temas",
"Zoom": "Zoom"
"Zoom": "Ampliar"
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"Image": "Imagen",
"Layout": "Disposición",
"Layout": "Formato",
"Measurements": "Medidas",
"More": "Más",
"Next": "Siguiente",
"Play": "Play",
"Previous": "Anterior",
"Reset": "Resetear",
"Reset": "Restaurar",
"RowsPerPage": "filas por página",
"Series": "Secuencia",
"Show": "Mostrar",
+3 -2
View File
@@ -10,6 +10,7 @@
"Clear": "Limpar",
"Coronal": "Coronal",
"Crosshairs": "Localizador",
"Download": "Baixar",
"Ellipse": "Elipse",
"Elliptical": "Elíptico",
"Flip H": "Inverter H",
@@ -30,8 +31,8 @@
"Probe": "Prova",
"ROI Window": "Janela ROI",
"Rectangle": "Retângulo",
"Reset": "Resetar",
"Reset to Defaults": "Restaurar para o Padrão",
"Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:Reset) para o Padrão",
"Rotate Right": "Girar à direita",
"Sagittal": "Sagital",
"Save": "Salvar",
+1 -1
View File
@@ -6,6 +6,6 @@
"Next": "Próximo",
"Play": "Play",
"Previous": "Anterior",
"Reset": "Reset",
"Reset": "Restaurar",
"Stop": "Stop"
}
@@ -1,6 +1,6 @@
{
"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.",
"Save": "Salvar",
"SaveMessage": "Preferências salvas",
+76
View File
@@ -3,6 +3,82 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @ohif/ui
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "1.0.1",
"version": "1.1.7",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -32,69 +32,52 @@ const ViewportDownloadForm = ({
maximumSize,
canvasClass,
}) => {
const [t] = useTranslation('ViewportDownloadForm');
const [t] = useTranslation('Buttons');
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
const [height, setHeight] = useState(defaultSize);
const [width, setWidth] = useState(defaultSize);
const [dimensions, setDimensions] = useState({
width: defaultSize,
height: defaultSize,
});
const [showAnnotations, setShowAnnotations] = useState(true);
const [keepAspect, setKeepAspect] = useState(true);
const [lastImage, setLastImage] = useState();
const [viewportElement, setViewportElement] = useState();
const [viewportElementHeight, setViewportElementHeight] = useState(
minimumSize
);
const [viewportElementWidth, setViewportElementWidth] = useState(minimumSize);
const [viewportElementDimensions, setViewportElementDimensions] = useState({
width: defaultSize,
height: defaultSize,
});
const [downloadCanvas, setDownloadCanvas] = useState({
ref: createRef(),
width: minimumSize,
height: minimumSize,
width: defaultSize,
height: defaultSize,
});
const [viewportPreview, setViewportPreview] = useState({
src: null,
width: minimumSize,
height: minimumSize,
width: defaultSize,
height: defaultSize,
});
// Cornerstone's `enable/disable`
useEffect(() => {
enableViewport(viewportElement);
return () => {
disableViewport(viewportElement);
setHeight(defaultSize);
setWidth(defaultSize);
};
}, [defaultSize, disableViewport, enableViewport, viewportElement]);
}, [disableViewport, enableViewport, viewportElement]);
useEffect(() => {
const { width, height } = viewportElementDimensions;
const validSize = value => (value >= minimumSize ? value : minimumSize);
const loadAndUpdateViewports = async () => {
const {
image,
width: scaledWidth,
height: scaledHeight,
} = await loadImage(activeViewport, viewportElement, width, height);
setLastImage(image);
await loadImage(activeViewport, viewportElement, width, height);
toggleAnnotations(showAnnotations, viewportElement);
setViewportElementHeight(validSize(scaledHeight));
setViewportElementWidth(validSize(scaledWidth));
setDownloadCanvas(state => ({
...state,
height: validSize(scaledHeight),
width: validSize(scaledWidth),
}));
const {
dataUrl,
width: viewportElementWidth,
@@ -118,8 +101,6 @@ const ViewportDownloadForm = ({
activeViewport,
viewportElement,
showAnnotations,
height,
width,
loadImage,
toggleAnnotations,
updateViewportPreview,
@@ -127,54 +108,41 @@ const ViewportDownloadForm = ({
downloadCanvas.ref,
minimumSize,
maximumSize,
viewportElementDimensions,
]);
const onHeightChange = event => {
const newHeight = Math.min(event.target.value, maximumSize);
setHeight(newHeight);
/**
* @param {object} event - Input change event
* @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);
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,
}));
if (updatedDimension === dimensions.width) {
return;
}
};
const onWidthChange = event => {
const newWidth = Math.min(event.target.value, maximumSize);
setWidth(newWidth);
setViewportElementWidth(newWidth);
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
if (keepAspect) {
const multiplier = newWidth / lastImage.width;
const newHeight = Math.round(lastImage.height * multiplier);
setHeight(newHeight);
setViewportElementHeight(newHeight);
// In current code, keepAspect is always `true`
// And we always start w/ a square width/height
setDimensions({
width: updatedDimension,
height: updatedDimension,
});
// Only update if value is non-empty
if (!isEmpty) {
setViewportElementDimensions({
height: updatedDimension,
width: updatedDimension,
});
setDownloadCanvas(state => ({
...state,
height: newHeight,
height: updatedDimension,
width: updatedDimension,
}));
}
};
@@ -196,26 +164,22 @@ const ViewportDownloadForm = ({
)}
</div>
<div className="file-info-container">
<div className="file-info-container" data-cy="file-info-container">
<div className="col">
<div className="width">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={width}
data-cy="image-width"
value={dimensions.width}
label={t('Image width (px)')}
onChange={onWidthChange}
onChange={evt => onDimensionsChange(evt, 'height')}
/>
</div>
<div className="height">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={height}
data-cy="image-height"
value={dimensions.height}
label={t('Image height (px)')}
onChange={onHeightChange}
onChange={evt => onDimensionsChange(evt, 'width')}
/>
</div>
</div>
@@ -224,6 +188,7 @@ const ViewportDownloadForm = ({
<div className="file-name">
<TextInput
type="text"
data-cy="file-name"
value={filename}
onChange={event => setFilename(event.target.value)}
label={t('File name')}
@@ -233,6 +198,7 @@ const ViewportDownloadForm = ({
<div className="file-type">
<Select
value={fileType}
data-cy="file-type"
onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS}
label={t('File type')}
@@ -245,6 +211,7 @@ const ViewportDownloadForm = ({
<label htmlFor="show-annotations" className="form-check-label">
<input
id="show-annotations"
data-cy="show-annotations"
type="checkbox"
className="form-check-input"
checked={showAnnotations}
@@ -258,8 +225,8 @@ const ViewportDownloadForm = ({
<div
style={{
height: viewportElementHeight,
width: viewportElementWidth,
height: viewportElementDimensions.height,
width: viewportElementDimensions.width,
position: 'absolute',
left: '9999px',
}}
@@ -278,23 +245,33 @@ const ViewportDownloadForm = ({
></canvas>
</div>
<div className="preview">
<div className="preview" data-cy="image-preview">
<h4> {t('Image Preview')}</h4>
<img
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
data-cy="viewport-preview-img"
/>
</div>
<div className="actions">
<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')}
</button>
</div>
<div className="action-save">
<button onClick={downloadImage} className="btn btn-primary">
<button
onClick={downloadImage}
className="btn btn-primary"
data-cy="download-btn"
>
{t('Download')}
</button>
</div>
@@ -312,6 +289,7 @@ ViewportDownloadForm.propTypes = {
toggleAnnotations: PropTypes.func.isRequired,
loadImage: PropTypes.func.isRequired,
downloadBlob: PropTypes.func.isRequired,
/** A default width & height, between the minimum and maximum size */
defaultSize: PropTypes.number.isRequired,
minimumSize: PropTypes.number.isRequired,
maximumSize: PropTypes.number.isRequired,
@@ -1,20 +1,19 @@
.ToolContextMenu {
.ContextMenu {
position: relative;
background-color: white;
border: 1px solid white;
border-radius: 5px;
z-index: 1000;
display: block;
width: 170px;
}
.ToolContextMenu > ul {
.ContextMenu>ul {
list-style-type: none;
padding-left: 0;
margin: 0;
}
.ToolContextMenu > ul > li > button {
.ContextMenu>ul>li>button {
padding: 10px;
font-size: 14px;
border: none;
@@ -25,10 +24,10 @@
background: none;
}
.ToolContextMenu > ul > li > button:hover {
.ContextMenu>ul>li>button:hover {
color: #16202b;
}
.ToolContextMenu > ul > li > button:active {
.ContextMenu>ul>li>button:active {
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 { SimpleDialog } from './simpleDialog';
import { OHIFModal } from './ohifModal';
import { ContextMenu } from './contextMenu';
import {
PageToolbar,
StudyList,
@@ -27,6 +28,7 @@ import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip';
export {
ContextMenu,
Checkbox,
CineDialog,
ViewportDownloadForm,
@@ -22,8 +22,9 @@ class MeasurementTable extends Component {
onDeleteClick: PropTypes.func,
onEditDescriptionClick: PropTypes.func,
selectedMeasurementNumber: PropTypes.number,
overwallWarnings: PropTypes.object,
t: PropTypes.func,
saveFunction: PropTypes.func,
onSaveComplete: PropTypes.func,
};
static defaultProps = {
@@ -38,8 +39,9 @@ class MeasurementTable extends Component {
};
render() {
const hasOverallWarnings =
this.props.overallWarnings.warningList.length > 0;
const { overallWarnings, saveFunction, t } = this.props;
const hasOverallWarnings = overallWarnings.warningList.length > 0;
return (
<div className="measurementTable">
<div className="measurementTableHeader">
@@ -55,7 +57,7 @@ class MeasurementTable extends Component {
style={{}}
>
<div className="warningTitle">
{this.props.t('Criteria nonconformities')}
{t('Criteria nonconformities')}
</div>
<div className="warningContent">
{this.getWarningContent()}
@@ -75,10 +77,46 @@ class MeasurementTable extends Component {
<ScrollableArea>
<div>{this.getMeasurementsGroups()}</div>
</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>
);
}
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 = () => {
return this.props.measurementCollection.map((measureGroup, index) => {
return (
@@ -157,7 +195,7 @@ class MeasurementTable extends Component {
};
getWarningContent = () => {
const { warningList = '' } = this.props.overwallWarnings;
const { warningList = '' } = this.props.overallWarnings;
if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => {
@@ -1,92 +1,117 @@
.measurementTable
background-color: var(--primary-background-color)
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
height: 100%
width: 100%
display: flex
flex-direction: column
.maxMeasurements
border-radius: 3px;
color: var(--primary-background-color);
font-size: 12px;
font-weight: 500;
height: 19px;
line-height: 17px;
margin-top: 22px;
margin-right: 15px;
padding: 2px 6px 0;
text-transform: uppercase;
transition: all 0.3s ease;
background-color: var(--default-color);
border-radius: 3px
color: var(--primary-background-color)
font-size: 12px
font-weight: 500
height: 19px
line-height: 17px
margin-top: 22px
margin-right: 15px
padding: 2px 6px 0
text-transform: uppercase
transition: all 0.3s ease
background-color: var(--default-color)
.measurementTableHeader
padding-left: 45px;
padding-left: 45px
.measurementTableHeaderItem
display: inline-block;
margin-top: 9px;
margin-left: 9px;
padding-left: 9px;
width: 90px;
font-size: 14px;
line-height: 20px;
border-left: 1px solid var(--text-secondary-color);
color: var(--text-primary-color);
display: inline-block
margin-top: 9px
margin-left: 9px
padding-left: 9px
width: 90px
font-size: 14px
line-height: 20px
border-left: 1px solid var(--text-secondary-color)
color: var(--text-primary-color)
.timepointLabel
color: var(--text-secondary-color);
font-size: 12px;
color: var(--text-secondary-color)
font-size: 12px
.warning-status
display: block;
position: absolute;
width: 50px;
height: 40px;
padding-left: 7px;
left: 15px;
top: 7px;
display: block
position: absolute
width: 50px
height: 40px
padding-left: 7px
left: 15px
top: 7px
.warning-border
display: block;
border: 2px solid #e29e4a;
border-radius: 36px;
padding: 6px 7px;
width: 40px;
height: 40px;
display: block
border: 2px solid #e29e4a
border-radius: 36px
padding: 6px 7px
width: 40px
height: 40px
svg
color: #e29e4a;
width: 22px;
height: 20px;
pointer-events: inherit;
color: #e29e4a
width: 22px
height: 20px
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
&.in
opacity 1;
opacity 1
.tooltip-inner
padding: 0
.warningTitle
text-align: left
background-color: var(--ui-gray-darkest);
padding: 7px 15px;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
background-color: var(--ui-gray-darkest)
padding: 7px 15px
border-top-left-radius: 7px
border-top-right-radius: 7px
.warningContent
text-align center
background-color: var(--ui-gray);
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
background-color: var(--ui-gray)
border-bottom-left-radius: 7px
border-bottom-right-radius: 7px
ol
margin-top: auto;
padding-inline-start: 20px;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 25px;
text-align: left;
margin-top: auto
padding-inline-start: 20px
padding-top: 10px
padding-bottom: 10px
padding-left: 25px
text-align: left
li
padding-right: 10px;
padding-right: 10px
@@ -29,7 +29,7 @@ const OHIFModal = ({
const renderHeader = () => {
return (
title && (
<div className="OHIFModal__header">
<div className="OHIFModal__header" data-cy="modal-header">
<h4>{title}</h4>
{closeButton && (
<button data-cy="close-button" onClick={onClose}>
@@ -44,6 +44,7 @@ const OHIFModal = ({
return (
<Modal
className={classNames('OHIFModal', className)}
data-cy="modal"
shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen}
title={title}
@@ -51,7 +52,9 @@ const OHIFModal = ({
>
<>
{renderHeader()}
<div className="OHIFModal__content">{children}</div>
<div className="OHIFModal__content" data-cy="modal-content">
{children}
</div>
</>
</Modal>
);
@@ -43,6 +43,7 @@ const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
? tabs.map((tab, index) => {
const { name, hidden = false } = tab;
const cypressSelectorId = name.toLowerCase();
const tabClass =
index === activeTabIndex ? 'nav-link active' : 'nav-link';
return !hidden ? (
@@ -52,6 +53,7 @@ const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
onHeaderChanged(index);
}}
className={tabClass}
data-cy={cypressSelectorId}
>
<button>{name}</button>
</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,
onStop,
onDrag,
showOverlay,
} = dialog;
let position =
@@ -160,7 +161,7 @@ const DialogProvider = ({ children, service }) => {
position = centerPositions.find(position => position.id === id);
}
return (
const dragableItem = () => (
<Draggable
key={id}
disabled={!isDraggable}
@@ -217,6 +218,16 @@ const DialogProvider = ({ children, service }) => {
</div>
</Draggable>
);
return (
showOverlay ? (
<div className="Overlay" key={id}>
{dragableItem()}
</div>
) : (
dragableItem()
)
);
});
/**
@@ -238,13 +249,11 @@ const DialogProvider = ({ children, service }) => {
return (
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
<div className="DraggableArea">
{dialogs.some(dialog => dialog.showOverlay) ? (
<div className="Overlay active">{renderDialogs()}</div>
) : (
renderDialogs()
)}
</div>
{!isEmpty() &&
<div className="DraggableArea">
{renderDialogs()}
</div>
}
{children}
</DialogContext.Provider>
);
@@ -6,17 +6,16 @@
div
cursor: grabbing !important
.DraggableArea, .Overlay
.DraggableArea
width: 100%
height: 100%
position: absolute
.Overlay.active
position: fixed
z-index: 999
left: 0
top: 0
width: 100%
height: 100%
overflow: auto
background: rgba(0,0,0,.1)
.Overlay
position: fixed
z-index: 999
left: 0
top: 0
width: 100%
height: 100%
overflow: auto
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,
useDialog,
} 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 rotate from './icons/rotate.svg';
import rotateRight from './icons/rotate-right.svg';
import saveRegular from './icons/save-regular.svg';
import search from './icons/search.svg';
import searchPlus from './icons/search-plus.svg';
import softTissue from './icons/soft-tissue.svg';
@@ -164,6 +165,7 @@ const ICONS = {
'soft-tissue': softTissue,
lung,
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 {
ContextMenu,
Checkbox,
CineDialog,
ViewportDownloadForm,
@@ -60,14 +61,6 @@ import {
ModalConsumer,
useModal,
withModal,
ContextMenuProvider,
ContextMenuConsumer,
useContextMenu,
withContextMenu,
LabellingFlowProvider,
LabellingFlowConsumer,
useLabellingFlow,
withLabellingFlow,
} from './contextProviders';
export {
@@ -80,6 +73,7 @@ export {
TextArea,
TextInput,
CineDialog,
ContextMenu,
ViewportDownloadForm,
ExpandableToolMenu,
Icon,
@@ -125,14 +119,6 @@ export {
DialogProvider,
withDialog,
useDialog,
ContextMenuProvider,
ContextMenuConsumer,
useContextMenu,
withContextMenu,
LabellingFlowProvider,
LabellingFlowConsumer,
useLabellingFlow,
withLabellingFlow,
// Hooks
useDebounce,
useMedia,
+7 -1
View File
@@ -23,8 +23,14 @@ export function ToolbarButton(props) {
}
};
const cypressSelectorId = props.label.toLowerCase();
return (
<div className={className} onClick={handleClick}>
<div
className={className}
onClick={handleClick}
data-cy={cypressSelectorId}
>
{iconProps && <Icon {...iconProps} />}
<div className="toolbar-button-label">
{t(label)}
+202
View File
@@ -3,6 +3,208 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @ohif/viewer
## [3.0.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.1...@ohif/viewer@3.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
-1
View File
@@ -1,6 +1,5 @@
{
"baseUrl": "http://localhost:3000",
"video": false,
"chromeWebSecurity": false,
"waitForAnimations": true,
"defaultCommandTimeout": 10000,
@@ -81,10 +81,8 @@ describe('OHIF Cornerstone Hotkeys', () => {
});
it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => {
//Click on Layout button
cy.get('@layoutBtn').click();
//Select 3 viewports
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
//Select viewport layout (3,1)
cy.setLayout(3, 1);
cy.waitViewportImageLoading();
// Press multiples hotkeys on viewport #1
@@ -266,7 +266,7 @@ describe('OHIF Cornerstone Toolbar', () => {
let iconName;
//Click on one of the secondary tools from the overlay
cy.get('.tooltip-inner > :nth-child(1)')
cy.get('[data-cy="magnify"]')
.click()
.then($magnifyBtn => {
cy.wrap($magnifyBtn)
@@ -407,9 +407,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.as('toolbarOverlay')
.should('be.visible');
//Click on Clear button
cy.get('.tooltip-inner')
.contains('Clear')
.click();
cy.get('[data-cy="clear"]').click();
//Verify if measurements were removed from the measurements panel
cy.get('.measurementItem').should('not.exist');
@@ -446,9 +444,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.should('be.visible')
.then(() => {
//Click Eraser button
cy.get('.tooltip-inner')
.contains('Eraser')
.click({ force: true });
cy.get('[data-cy="eraser"]').click({ force: true });
});
//Erase measurement #1 and Verify if it was removed from the measurements panel
@@ -471,9 +467,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible');
// Click on Invert button
cy.get('.tooltip-inner')
.contains('Invert')
.click();
cy.get('[data-cy="invert"]').click();
// Visual comparison
cy.screenshot('Invert tool - Should Invert Canvas');
@@ -488,9 +482,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.should('be.visible')
.then(() => {
//Click on Rotate button
cy.get('.tooltip-inner')
.contains('Rotate')
.click({ force: true });
cy.get('[data-cy="rotate right"]').click({ force: true });
});
// Visual comparison
@@ -505,9 +497,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip H button
cy.get('.tooltip-inner')
.contains('Flip H')
.click();
cy.get('[data-cy="flip h"]').click();
// Visual comparison
cy.screenshot('Flip H tool - Should Flip Image on Y axis');
@@ -521,9 +511,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip V button
cy.get('.tooltip-inner')
.contains('Flip V')
.click();
cy.get('[data-cy="flip v"]').click();
// Visual comparison
cy.screenshot('Flip V tool - Should Flip Image on X axis');
@@ -0,0 +1,126 @@
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');
cy.percyCanvasSnapshot('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');
cy.percyCanvasSnapshot('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');
cy.percyCanvasSnapshot('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,22 @@
//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);
});
cy.screenshot();
cy.percyCanvasSnapshot('PT/CT json study route');
});
});
@@ -1,14 +1,14 @@
//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.
describe('OHIF Study List', function () {
context('Desktop resolution', function () {
beforeEach(function () {
describe('OHIF Study List', function() {
context('Desktop resolution', function() {
beforeEach(function() {
cy.viewport(1750, 720);
cy.openStudyList();
cy.initStudyListAliasesOnDesktop();
});
it('searches Patient Name with exact string', function () {
it('searches Patient Name with exact string', function() {
cy.get('@patientName').type('Juno');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult').should($list => {
expect($list.length).to.be.eq(18);
expect($list.length).to.be.eq(17);
expect($list).to.contain('MR');
});
});
it('searches Description with exact string', function () {
it('searches Description with exact string', function() {
cy.get('@studyDescription').type('CHEST');
//Wait result list to be displayed
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
const pageRows = [25, 50, 100];
@@ -100,14 +100,14 @@ describe('OHIF Study List', function () {
// });
});
context('Tablet resolution', function () {
beforeEach(function () {
context('Tablet resolution', function() {
beforeEach(function() {
cy.viewport(1000, 660);
cy.openStudyList();
cy.initStudyListAliasesOnTablet();
});
it('searches Patient Name with exact string', function () {
it('searches Patient Name with exact string', function() {
cy.get('@patientNameOrMRN').type('Juno');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult').should($list => {
expect($list.length).to.be.eq(18);
expect($list.length).to.be.eq(17);
expect($list).to.contain('MR');
});
});
it('searches Accession with exact string', function () {
it('searches Accession with exact string', function() {
cy.get('@accessionModalityDescription').type('fpcben98890');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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
const pageRows = [25, 50, 100];
@@ -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');
// });
});
@@ -24,10 +24,9 @@ describe('OHIF VTK Extension', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('CT WB 5.0 B35f')
.drag('.viewport-drop-target')
.wait(1000); // TODO: Once issue #1167 is fixed, the wait command should be removed. (https://github.com/OHIF/Viewers/issues/1167)
//Select 2D MPR button
cy.get('.PluginSwitch > .toolbar-button').click();
cy.get('[data-cy="2d mpr"]').click();
//Wait Reformatting Images
cy.waitVTKReformatting();
@@ -0,0 +1,149 @@
describe('OHIF Save Measurements', function() {
before(() => {
cy.openStudy('Fall 1');
cy.expectMinimumThumbnails(2);
});
beforeEach(() => {
// Drags Study thumbnail into viewport
cy.get('[data-cy="thumbnail-list"]:nth-child(1)')
.scrollIntoView()
.drag('.viewport-drop-target');
// Wait image to load on viewport
cy.wait(2000);
cy.resetViewport();
cy.initCommonElementsAliases();
});
it('saves new measurement annotation', function() {
// Add measurement in the viewport
cy.addLengthMeasurement();
// Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.at.least', 1);
// Save new measurement
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that success message overlay is displayed
cy.get('.sb-success')
.should('be.visible')
.and('contains.text', 'Measurements were saved with success');
// Visual test comparison
cy.screenshot('Save Measurements - new measurement added');
cy.percyCanvasSnapshot('Save Measurements - new measurement added');
});
it('retrieves saved measurements', function() {
// Add measurement in the viewport
cy.addLengthMeasurement();
// Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementDisplayText') // Get label size of the recently added measurement
.last()
.then($measurementSizeLabel => {
// Save new measurement
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that success message overlay is displayed
cy.get('.sb-success').should('be.visible');
// Reload the page
cy.reload();
//Verify that recently added measurement was retrieved
cy.get('@measurementsBtn').click();
cy.get('.measurementDisplayText') // Get label size of the recently added measurement
.last()
.then($retrivedMeasurementSizeLabel => {
expect($retrivedMeasurementSizeLabel.textContent).to.eq(
$measurementSizeLabel.textContent
);
});
});
});
it('checks error message when saving without any measurement', function() {
// Checks that measurement list is empty
cy.get('.numberOfItems').should('have.text', '0');
// Click on Save Measurement button
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that error message overlay is displayed
cy.get('.sb-error')
.should('be.visible')
.and('contains.text', 'Error while saving the measurements');
// Close message overlay
cy.get('.sb-closeIcon').click();
});
it('checks if warning message is displayed on measurements of unsupported tools', function() {
// Add measurement for unsupported tool in the viewport
cy.addAngleMeasurement();
// Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.at.least', 1);
// Check that warning is displayed for unsupported tool
cy.get('.hasWarnings').should('be.visible');
// Save new measurement
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that error message overlay is displayed
cy.get('.sb-error')
.should('be.visible')
.and('contains.text', 'Error while saving the measurements');
// Close message overlay
cy.get('.sb-closeIcon').click();
// Close Measurements panel
cy.get('@measurementsBtn').click();
});
it('checks if measurements of unsupported tools were not saved', function() {
// Add measurement for supported tool in the viewport
cy.addLengthMeasurement();
// Add measurement for unsupported tool in the viewport
cy.addAngleMeasurement();
// Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.eq', 2);
// Check that warning is displayed for unsupported tool
cy.get('.hasWarnings').should('be.visible');
// Save new measurement
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that success message overlay is displayed
cy.get('.sb-success')
.should('be.visible')
.and('contains.text', 'Measurements were saved with success');
// Reload the page
cy.reload();
//Verify that measurement for unsupported tool was not saved
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.eq', 1);
// Close Measurements panel
cy.get('@measurementsBtn').click();
});
});
+19 -22
View File
@@ -1,16 +1,17 @@
//Creating aliases for Cornerstone tools buttons
export function initCornerstoneToolsAliases() {
cy.get('.ToolbarRow > :nth-child(2)').as('stackScrollBtn');
cy.get('.ToolbarRow > :nth-child(3)').as('zoomBtn');
cy.get('.ToolbarRow > :nth-child(4)').as('levelsBtn');
cy.get('.ToolbarRow > :nth-child(5)').as('panBtn');
cy.get('.ToolbarRow > :nth-child(6)').as('lengthBtn');
cy.get('.ToolbarRow > :nth-child(7)').as('annotateBtn');
cy.get('.ToolbarRow > :nth-child(8)').as('angleBtn');
cy.get('.ToolbarRow > :nth-child(9)').as('resetBtn');
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
cy.get('.expandableToolMenu').as('moreBtn');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
cy.get('[data-cy="stack scroll"]').as('stackScrollBtn');
cy.get('[data-cy="zoom"]').as('zoomBtn');
cy.get('[data-cy="levels"]').as('levelsBtn');
cy.get('[data-cy="pan"]').as('panBtn');
cy.get('[data-cy="length"]').as('lengthBtn');
cy.get('[data-cy="annotate"]').as('annotateBtn');
cy.get('[data-cy="angle"]').as('angleBtn');
cy.get('[data-cy="reset"]').as('resetBtn');
cy.get('[data-cy="cine"]').as('cineBtn');
cy.get('[data-cy="more"]').as('moreBtn');
cy.get('[data-cy="layout"]').as('layoutBtn');
cy.get('.viewport-element').as('viewport');
}
//Creating aliases for Common page elements
@@ -43,14 +44,14 @@ export function initRouteAliases() {
//Creating aliases for VTK tools buttons
export function initVTKToolsAliases() {
cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
cy.get('.ToolbarRow > :nth-child(2)').as('crosshairsBtn');
cy.get('.ToolbarRow > :nth-child(3)').as('wwwcBtn');
cy.get('.ToolbarRow > :nth-child(4)').as('rotateBtn');
cy.get('[data-cy="exit 2d mpr"]').as('exit2dmprBtn');
cy.get('[data-cy="crosshairs"]').as('crosshairsBtn');
cy.get('[data-cy="wwwc"]').as('wwwcBtn');
cy.get('[data-cy="rotate"]').as('rotateBtn');
cy.get('.slab-thickness').as('slabSlider');
cy.get('.select-ohif').as('modeDropdown');
cy.get('.ohif-check-label').as('modeCheckbox');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
cy.get('[data-cy="layout"]').as('layoutBtn');
}
//Creating aliases for Study List page elements on Desktop experience
@@ -81,12 +82,8 @@ export function initStudyListAliasesOnTablet() {
//Creating aliases for User Preferences modal
export function initPreferencesModalAliases() {
cy.get('.OHIFModal').as('preferencesModal');
cy.get('.nav-link')
.first()
.as('userPreferencesHotkeysTab');
cy.get('.nav-link')
.last()
.as('userPreferencesGeneralTab');
cy.get('[data-cy="hotkeys"]').as('userPreferencesHotkeysTab');
cy.get('[data-cy="general"]').as('userPreferencesGeneralTab');
cy.get('[data-cy="reset-default-btn"]').as('restoreBtn');
cy.get('[data-cy="cancel-btn"]').as('cancelBtn');
cy.get('[data-cy="save-btn"]').as('saveBtn');
+20 -10
View File
@@ -209,7 +209,7 @@ Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
//Command to reset and clear all the changes made to the viewport
Cypress.Commands.add('resetViewport', () => {
//Click on More button
cy.get('.expandableToolMenu')
cy.get('[data-cy="more"]')
.as('moreBtn')
.click();
//Verify if overlay is displayed
@@ -219,11 +219,11 @@ Cypress.Commands.add('resetViewport', () => {
}
});
//Click on Clear button
cy.get('.tooltip-inner > :nth-child(10)')
cy.get('[data-cy="clear"]')
.as('clearBtn')
.click();
//Click on Reset button
cy.get('.ToolbarRow > :nth-child(9)')
cy.get('[data-cy="reset"]')
.as('resetBtn')
.click();
});
@@ -284,9 +284,8 @@ Cypress.Commands.add('initStudyListAliasesOnTablet', () => {
Cypress.Commands.add(
'addLengthMeasurement',
(firstClick = [150, 100], secondClick = [130, 170]) => {
cy.initCornerstoneToolsAliases();
cy.get('@lengthBtn').click();
cy.addLine('@viewport', firstClick, secondClick);
cy.get('[data-cy="length"]').click();
cy.addLine('.viewport-element', firstClick, secondClick);
}
);
@@ -294,9 +293,8 @@ Cypress.Commands.add(
Cypress.Commands.add(
'addAngleMeasurement',
(initPos = [180, 390], midPos = [300, 410], finalPos = [180, 450]) => {
cy.initCornerstoneToolsAliases();
cy.get('@angleBtn').click();
cy.addAngle('@viewport', initPos, midPos, finalPos);
cy.get('[data-cy="angle"]').click();
cy.addAngle('.viewport-element', initPos, midPos, finalPos);
}
);
@@ -369,7 +367,7 @@ Cypress.Commands.add('percyCanvasSnapshot', (name, options = {}) => {
});
Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => {
cy.get('.btn-group > .toolbar-button').click();
cy.get('[data-cy="layout"]').click();
cy.get('.layoutChooser')
.find('tr')
@@ -469,6 +467,18 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add('openDownloadImageModal', () => {
// Click on More button
cy.get('[data-cy="more"]')
.as('moreBtn')
.click();
// Click on Download button
cy.get('[data-cy="download"]')
.as('downloadBtn')
.click();
});
Cypress.Commands.add('setLanguage', (language, save = true) => {
cy.openPreferences();
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "3.0.2",
"version": "3.3.5",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -27,7 +27,7 @@
"dev:viewer": "yarn run dev",
"start": "yarn run dev",
"test:e2e": "cypress open",
"test:e2e:ci": "percy exec -- cypress run --browser chrome",
"test:e2e:ci": "percy exec -- cypress run --record --browser chrome",
"test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci",
"test:e2e:serve": "serve -n -l 3000 -s dist",
"test:unit": "jest --watchAll",
@@ -45,22 +45,22 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^2.0.2",
"@ohif/extension-cornerstone": "^2.0.1",
"@ohif/extension-dicom-html": "^1.0.2",
"@ohif/core": "^2.3.2",
"@ohif/extension-cornerstone": "^2.2.1",
"@ohif/extension-dicom-html": "^1.1.0",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.0",
"@ohif/extension-vtk": "^1.0.2",
"@ohif/i18n": "^0.52.2",
"@ohif/ui": "^1.0.1",
"@ohif/extension-dicom-pdf": "^1.0.1",
"@ohif/extension-vtk": "^1.2.5",
"@ohif/i18n": "^0.52.4",
"@ohif/ui": "^1.1.7",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4",
"hammerjs": "^2.0.8",
@@ -85,7 +85,7 @@
"vtk.js": "^11.7.2"
},
"devDependencies": {
"cypress": "^3.3.1",
"cypress": "^3.8.0",
"gh-pages": "2.0.1",
"identity-obj-proxy": "3.0.x",
"lodash": "4.17.11",
+1 -1
View File
@@ -70,5 +70,5 @@ window.config = {
// ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
],
cornerstoneExtensionConfig: { tools: {} },
cornerstoneExtensionConfig: {},
};
File diff suppressed because it is too large. Load diff
+15 -53
View File
@@ -8,9 +8,6 @@ import { hot } from 'react-hot-loader/root';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import ToolContextMenu from './connectedComponents/ToolContextMenu';
import LabellingManager from './components/Labelling/LabellingManager';
import {
SnackbarProvider,
ModalProvider,
@@ -18,11 +15,6 @@ import {
OHIFModal,
} from '@ohif/ui';
import {
LabellingFlowProvider,
ContextMenuProvider,
} from './appCustomProviders';
import {
CommandsManager,
ExtensionManager,
@@ -31,8 +23,6 @@ import {
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
utils,
redux as reduxOHIF,
} from '@ohif/core';
@@ -74,10 +64,7 @@ const commandsManagerConfig = {
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
let extensionManager;
/** ~~~~~~~~~~~~~ End Application Setup */
// TODO[react] Use a provider when the whole tree is React
@@ -135,16 +122,11 @@ class App extends Component {
} = this._appConfig;
this.initUserManager(oidc);
_initServices([
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
]);
_initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(
[...defaultExtensions, ...extensions],
cornerstoneExtensionConfig
cornerstoneExtensionConfig,
this._appConfig
);
/*
@@ -161,9 +143,7 @@ class App extends Component {
const {
UINotificationService,
UIDialogService,
UILabellingFlowService,
UIModalService,
UIContextMenuService,
} = servicesManager.services;
if (this._userManager) {
@@ -181,21 +161,9 @@ class App extends Component {
modal={OHIFModal}
service={UIModalService}
>
<LabellingFlowProvider
service={UILabellingFlowService}
labellingComponent={LabellingManager}
commandsManager={commandsManager}
>
<ContextMenuProvider
service={UIContextMenuService}
contextMenuComponent={ToolContextMenu}
commandsManager={commandsManager}
>
<OHIFStandaloneViewer
userManager={this._userManager}
/>
</ContextMenuProvider>
</LabellingFlowProvider>
<OHIFStandaloneViewer
userManager={this._userManager}
/>
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
@@ -218,19 +186,7 @@ class App extends Component {
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<LabellingFlowProvider
service={UILabellingFlowService}
labellingComponent={LabellingManager}
commandsManager={commandsManager}
>
<ContextMenuProvider
service={UIContextMenuService}
contextMenuComponent={ToolContextMenu}
commandsManager={commandsManager}
>
<OHIFStandaloneViewer />
</ContextMenuProvider>
</LabellingFlowProvider>
<OHIFStandaloneViewer />
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
@@ -283,7 +239,13 @@ function _initServices(services) {
/**
* @param
*/
function _initExtensions(extensions, cornerstoneExtensionConfig) {
function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) {
extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
appConfig,
});
const requiredExtensions = [
GenericViewerCommands,
[OHIFCornerstoneExtension, cornerstoneExtensionConfig],
@@ -1,51 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ContextMenuProvider } from '@ohif/ui';
const CustomContextMenuProvider = ({
children,
service,
contextMenuComponent,
commandsManager,
}) => {
const onDeleteHandler = (nearbyToolData, eventData) => {
const element = eventData.element;
commandsManager.runCommand('removeToolState', {
element,
toolType: nearbyToolData.toolType,
tool: nearbyToolData.tool,
});
};
return (
<ContextMenuProvider
service={service}
contextMenuComponent={contextMenuComponent}
onDelete={onDeleteHandler}
>
{children}
</ContextMenuProvider>
);
};
CustomContextMenuProvider.defaultProps = {
service: null,
};
CustomContextMenuProvider.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,
commandsManager: PropTypes.object.isRequired,
};
export default CustomContextMenuProvider;
@@ -1,49 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { LabellingFlowProvider } from '@ohif/ui';
const CustomLabellingFlowProvider = ({
children,
service,
labellingComponent,
commandsManager,
}) => {
const onUpdateLabellingHandler = (labellingData, measurementData) => {
commandsManager.runCommand(
'updateTableWithNewMeasurementData',
measurementData
);
};
return (
<LabellingFlowProvider
service={service}
labellingComponent={labellingComponent}
onUpdateLabelling={onUpdateLabellingHandler}
>
{children}
</LabellingFlowProvider>
);
};
CustomLabellingFlowProvider.defaultProps = {
service: null,
};
CustomLabellingFlowProvider.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,
commandsManager: PropTypes.object.isRequired,
};
export default CustomLabellingFlowProvider;
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more