Compare commits

..
Author SHA1 Message Date
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
81 changed files with 1855 additions and 2324 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
+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)
+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:
+54 -1
View File
@@ -1,3 +1,56 @@
# 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'],
},
],
// Config to pass to the bundled cornerstone extension
cornerstoneExtensionConfig: {},
};
```
<!--
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
+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.
# [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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.0.1",
"version": "2.1.0",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -34,7 +34,7 @@
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.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",
+20 -14
View File
@@ -102,7 +102,7 @@ export default function init({ servicesManager, configuration }) {
];
/* Add extension tools configuration here. */
const extensionToolsConfiguration = {
const internalToolsConfig = {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
@@ -113,20 +113,26 @@ 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));
}
/* 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, externalToolProps);
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 });
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
+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"
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.0.2",
"version": "1.1.4",
"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",
"@ohif/core": "^2.1.1",
"@ohif/ui": "^1.1.4",
"cornerstone-tools": "^4.8.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",
+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.
## [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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.0.2",
"version": "2.1.1",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
-6
View File
@@ -23,8 +23,6 @@ import {
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
} from './services';
const OHIF = {
@@ -55,8 +53,6 @@ const OHIF = {
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
};
export {
@@ -86,8 +82,6 @@ export {
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
};
export { OHIF };
-2
View File
@@ -13,8 +13,6 @@ describe('Top level exports', () => {
'UINotificationService',
'UIModalService',
'UIDialogService',
'UIContextMenuService',
'UILabellingFlowService',
//
'utils',
'studies',
@@ -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;
});
}
@@ -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,
};
+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",
+49
View File
@@ -3,6 +3,55 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.4",
"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,42 @@ class MeasurementTable extends Component {
<ScrollableArea>
<div>{this.getMeasurementsGroups()}</div>
</ScrollableArea>
<div className="measurementTableFooter">
{saveFunction && (
<button onClick={this.saveFunction} className="saveBtn">
<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 +191,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>
);
@@ -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;
@@ -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,
+91
View File
@@ -3,6 +3,97 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
@@ -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,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');
// });
});
@@ -11,6 +11,7 @@ export function initCornerstoneToolsAliases() {
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
cy.get('.expandableToolMenu').as('moreBtn');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
cy.get('.viewport-element').as('viewport');
}
//Creating aliases for Common page elements
@@ -469,6 +469,18 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add('openDownloadImageModal', () => {
// Click on More button
cy.get('.expandableToolMenu')
.as('moreBtn')
.click();
// Click on Download button
cy.get('.tooltip-inner > :nth-child(13)')
.as('downloadBtn')
.click();
});
Cypress.Commands.add('setLanguage', (language, save = true) => {
cy.openPreferences();
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "3.0.2",
"version": "3.1.9",
"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,14 +45,14 @@
},
"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.1.1",
"@ohif/extension-cornerstone": "^2.1.0",
"@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-vtk": "^1.1.4",
"@ohif/i18n": "^0.52.4",
"@ohif/ui": "^1.1.4",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
@@ -60,7 +60,7 @@
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.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
+5 -47
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';
@@ -135,13 +125,7 @@ class App extends Component {
} = this._appConfig;
this.initUserManager(oidc);
_initServices([
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
]);
_initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(
[...defaultExtensions, ...extensions],
cornerstoneExtensionConfig
@@ -161,9 +145,7 @@ class App extends Component {
const {
UINotificationService,
UIDialogService,
UILabellingFlowService,
UIModalService,
UIContextMenuService,
} = servicesManager.services;
if (this._userManager) {
@@ -181,21 +163,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 +188,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>
@@ -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;
@@ -1,6 +0,0 @@
export {
default as LabellingFlowProvider,
} from './LabellingFlowProvider/LabellingFlowProvider.js';
export {
default as ContextMenuProvider,
} from './ContextMenuProvider/ContextMenuProvider.js';
@@ -3,12 +3,20 @@ import { MeasurementTable } from '@ohif/ui';
import OHIF from '@ohif/core';
import moment from 'moment';
import cornerstone from 'cornerstone-core';
import DICOMSR from './../../lib/DICOMSR';
import jumpToRowItem from './jumpToRowItem.js';
const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements;
/**
* Takes a list of objects and a property and return the list grouped by the property
*
* @param {Array} list - The objects to be grouped by
* @param {string} props - The property to group the objects
* @returns {Object}
*/
function groupBy(list, props) {
return list.reduce((a, b) => {
(a[b[props]] = a[b[props]] || []).push(b);
@@ -16,16 +24,29 @@ function groupBy(list, props) {
}, {});
}
function getAllTools() {
const config = OHIF.measurements.MeasurementApi.getConfiguration();
/**
* Takes a list of tools grouped and return all tools separately
*
* @param {Array} [toolGroups=[]] - The grouped tools
* @returns {Array} - The list of all tools on all groups
*/
function getAllTools(toolGroups = []) {
let tools = [];
config.measurementTools.forEach(
toolGroups.forEach(
toolGroup => (tools = tools.concat(toolGroup.childTools))
);
return tools;
}
/**
* Takes measurementData and build the measurement text to be used into the table
*
* @param {Object} [measurementData={}]
* @param {string} measurementData.location - The measurement location
* @param {string} measurementData.description - The measurement description
* @returns {string}
*/
function getMeasurementText(measurementData = {}) {
const defaultText = '...';
const { location = '', description = '' } = measurementData;
@@ -34,6 +55,14 @@ function getMeasurementText(measurementData = {}) {
return result || defaultText;
}
/**
* Takes a list of measurements grouped by measurement numbers and return each measurement data by available timepoint
*
* @param {Array} measurementNumberList - The list of measurements
* @param {Array} timepoints - The list of available timepoints
* @param {Function} displayFunction - The function that builds the display text by each tool
* @returns
*/
function getDataForEachMeasurementNumber(
measurementNumberList,
timepoints,
@@ -56,10 +85,35 @@ function getDataForEachMeasurementNumber(
return data;
}
/**
* Take a measurement toolName and return if any warnings
*
* @param {string} toolName - The tool name
* @returns {string}
*/
function getWarningsForMeasurement(toolName) {
const isToolSupported = DICOMSR.isToolSupported(toolName);
return {
hasWarnings: !isToolSupported,
warningTitle: isToolSupported ? '' : 'Unsupported Tool',
warningList: isToolSupported
? []
: [`${toolName} cannot be persisted at this time`],
};
}
/**
* Take measurements from MeasurementAPI structure and convert into a measurementTable structure
*
* @param {Object} toolCollections - The list of all measurement grouped by groupTool and toolName
* @param {Array} timepoints - The list of available timepoints
* @returns
*/
function convertMeasurementsToTableData(toolCollections, timepoints) {
const config = OHIF.measurements.MeasurementApi.getConfiguration();
const toolGroups = config.measurementTools;
const tools = getAllTools();
const tools = getAllTools(toolGroups);
const tableMeasurements = toolGroups.map(toolGroup => {
return {
@@ -88,6 +142,12 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
} = measurementData;
const measurementId = measurementData._id;
const {
hasWarnings,
warningTitle,
warningList,
} = getWarningsForMeasurement(toolType);
//check if all measurements with same measurementNumber will have same LABEL
const tableMeasurement = {
itemNumber: lesionNamingNumber,
@@ -96,10 +156,10 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
measurementNumber,
lesionNamingNumber,
toolType,
hasWarnings: false, //TODO
warningTitle: '', //TODO
hasWarnings,
warningTitle,
warningList,
isSplitLesion: false, //TODO
warningList: [], //TODO
data: getDataForEachMeasurementNumber(
measurementNumberList,
timepoints,
@@ -126,6 +186,13 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
return tableMeasurements;
}
/**
* Take a list of available timepoints and return a list header information for each timepoint
*
* @param {Array} timepoints - The list of available timepoints
* @param {string} timepoints[].latestDate - The date of the last study taken on the timepoint
* @returns {{label: string, key: string, date: string}[]}
*/
function convertTimepointsToTableData(timepoints) {
if (!timepoints || !timepoints.length) {
return [];
@@ -140,8 +207,28 @@ function convertTimepointsToTableData(timepoints) {
];
}
/**
* Takes server type and return a function or undefined
*
* @param {string} serverType - The server type
* @returns {undefined|Function}
*/
function getSaveFunction(serverType) {
if (serverType === 'dicomWeb') {
return () => {
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const promise = measurementApi.storeMeasurements();
return promise;
};
}
}
const mapStateToProps = state => {
const { timepoints, measurements } = state.timepointManager;
const { timepointManager, servers } = state;
const { timepoints, measurements } = timepointManager;
const activeServer = servers.servers.find(a => a.active === true);
const saveFunction = getSaveFunction(activeServer.type);
return {
timepoints: convertTimepointsToTableData(timepoints),
measurementCollection: convertMeasurementsToTableData(
@@ -150,6 +237,7 @@ const mapStateToProps = state => {
),
timepointManager: state.timepointManager,
viewports: state.viewports,
saveFunction,
};
};
@@ -265,10 +353,15 @@ const mapDispatchToProps = (dispatch, ownProps) => {
};
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
const { timepoints, saveFunction, measurementCollection } = propsFromState;
const { onSaveComplete, selectedMeasurementNumber } = ownProps;
return {
timepoints: propsFromState.timepoints,
measurementCollection: propsFromState.measurementCollection,
selectedMeasurementNumber: ownProps.selectedMeasurementNumber,
timepoints,
saveFunction,
measurementCollection,
onSaveComplete,
selectedMeasurementNumber,
...propsFromDispatch,
onItemClick: (event, measurementData) => {
// TODO: Add timepointId to .data for measurementData?
@@ -2,6 +2,8 @@ import React from 'react';
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
import init from './init.js';
import LabellingFlow from '../../components/Labelling/LabellingFlow';
export default {
/**
* Only required property. Should be a unique value across all extensions.
@@ -11,31 +13,56 @@ export default {
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration });
},
getPanelModule({ servicesManager, commandsManager }) {
const { UILabellingFlowService } = servicesManager.services;
const { UINotificationService, UIDialogService } = servicesManager.services;
const showLabellingDialog = (props, measurementData) => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.dismiss({ id: 'labelling' });
UIDialogService.create({
id: 'labelling',
centralize: true,
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
contentProps: {
measurementData,
labellingDoneCallback: () =>
UIDialogService.dismiss({ id: 'labelling' }),
updateLabelling: ({ location, description, response }) => {
measurementData.location = location || measurementData.location;
measurementData.description = description || '';
measurementData.response = response || measurementData.response;
commandsManager.runCommand(
'updateTableWithNewMeasurementData',
measurementData
);
},
...props,
},
});
};
const ExtendedConnectedMeasurementTable = () => (
<ConnectedMeasurementTable
onRelabel={tool => {
if (UILabellingFlowService) {
UILabellingFlowService.show({
centralize: true,
props: {
skipAddLabelButton: true,
editLocation: true,
measurementData: tool,
},
});
}
}}
onEditDescription={tool => {
if (UILabellingFlowService) {
UILabellingFlowService.show({
centralize: true,
props: {
editDescriptionOnDialog: true,
measurementData: tool,
},
});
onRelabel={tool =>
showLabellingDialog(
{ editLocation: true, skipAddLabelButton: true },
tool
)
}
onEditDescription={tool =>
showLabellingDialog({ editDescriptionOnDialog: true }, tool)
}
onSaveComplete={message => {
if (UINotificationService) {
UINotificationService.show(message);
}
}}
/>
@@ -3,6 +3,9 @@ import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import throttle from 'lodash.throttle';
import LabellingFlow from '../../components/Labelling/LabellingFlow';
import ToolContextMenu from '../../connectedComponents/ToolContextMenu';
const {
onAdded,
onRemoved,
@@ -29,10 +32,7 @@ export default function init({
commandsManager,
configuration,
}) {
const {
UIContextMenuService,
UILabellingFlowService,
} = servicesManager.services;
const { UIDialogService } = servicesManager.services;
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
const onMeasurementsChanged = (action, event) => {
@@ -46,21 +46,108 @@ export default function init({
'labelmapModified'
);
const onRightClick = event => {
if (UIContextMenuService) {
UIContextMenuService.show({ event: event.detail });
const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client.x) || 0,
y: (event && event.currentPoints.client.y) || 0,
});
const _updateLabellingHandler = (labellingData, measurementData) => {
const { location, description, response } = labellingData;
if (location) {
measurementData.location = location;
}
measurementData.description = description || '';
if (response) {
measurementData.response = response;
}
commandsManager.runCommand(
'updateTableWithNewMeasurementData',
measurementData
);
};
const showLabellingDialog = (props, contentProps, measurementData) => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.create({
id: 'labelling',
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
contentProps: {
measurementData,
labellingDoneCallback: () =>
UIDialogService.dismiss({ id: 'labelling' }),
updateLabelling: labellingData =>
_updateLabellingHandler(labellingData, measurementData),
...contentProps,
},
...props,
});
};
const onRightClick = event => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.dismiss({ id: 'context-menu' });
UIDialogService.create({
id: 'context-menu',
isDraggable: false,
preservePosition: false,
defaultPosition: _getDefaultPosition(event.detail),
content: ToolContextMenu,
contentProps: {
eventData: event.detail,
onDelete: (nearbyToolData, eventData) => {
const element = eventData.element;
commandsManager.runCommand('removeToolState', {
element,
toolType: nearbyToolData.toolType,
tool: nearbyToolData.tool,
});
},
onClose: () => UIDialogService.dismiss({ id: 'context-menu' }),
onSetLabel: (eventData, measurementData) => {
showLabellingDialog(
{ centralize: true, isDraggable: false },
{ skipAddLabelButton: true, editLocation: true },
measurementData
);
},
onSetDescription: (eventData, measurementData) => {
showLabellingDialog(
{ defaultPosition: _getDefaultPosition(eventData) },
{ editDescriptionOnDialog: true },
measurementData
);
},
},
});
};
const onTouchPress = event => {
if (UIContextMenuService) {
UIContextMenuService.show({
event: event.detail,
props: {
isTouchEvent: true,
},
});
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.create({
eventData: event.detail,
content: ToolContextMenu,
contentProps: {
isTouchEvent: true,
},
});
};
const onTouchStart = () => resetLabelligAndContextMenu();
@@ -68,10 +155,13 @@ export default function init({
const onMouseClick = () => resetLabelligAndContextMenu();
const resetLabelligAndContextMenu = () => {
if (UILabellingFlowService && UIContextMenuService) {
UILabellingFlowService.hide();
UIContextMenuService.hide();
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.dismiss({ id: 'context-menu' });
UIDialogService.dismiss({ id: 'labelling' });
};
// TODO: This makes scrolling painfully slow
@@ -6,8 +6,7 @@
max-height: 500px;
}
.labellingComponent .selectedLabel,
.labellingComponent .selectedDescription {
.labellingComponent .selectedLabel, .labellingComponent .selectedDescription {
padding: 5px;
background-color: white;
width: 150px;
@@ -86,14 +85,12 @@
color: #337ab7;
}
.labellingComponent .commonButtons,
.labellingComponent.editDescription .editDescriptionButtons {
.labellingComponent .commonButtons, .labellingComponent.editDescription .editDescriptionButtons {
display: block;
margin-left: 55px;
}
.labellingComponent.editDescription .commonButtons,
.labellingComponent .editDescriptionButtons {
.labellingComponent.editDescription .commonButtons, .labellingComponent .editDescriptionButtons {
display: none;
}
@@ -1,112 +1,165 @@
import { Icon, SelectTree } from '@ohif/ui';
import React, { Component } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import LabellingTransition from './LabellingTransition.js';
import OHIFLabellingData from './OHIFLabellingData.js';
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
import './LabellingFlow.css';
export default class LabellingFlow extends Component {
static propTypes = {
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
initialTopDistance: PropTypes.number,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
const LabellingFlow = ({
measurementData,
editLocation,
editDescription,
skipAddLabelButton,
updateLabelling,
labellingDoneCallback,
editDescriptionOnDialog,
}) => {
const [fadeOutTimer, setFadeOutTimer] = useState();
const [showComponent, setShowComponent] = useState(true);
const descriptionInput = useRef();
const [state, setState] = useState({
measurementData,
editLocation,
editDescription,
skipAddLabelButton,
});
useEffect(() => {
const newMeasurementData = cloneDeep(measurementData);
if (editDescription) {
newMeasurementData.description = undefined;
}
if (editLocation) {
newMeasurementData.location = undefined;
}
let newEditLocation = editLocation;
if (!editDescription && !editLocation) {
newEditLocation = true;
}
setState(state => ({
...state,
editLocation: newEditLocation,
measurementData: newMeasurementData,
}));
}, [editDescription, editLocation, measurementData]);
useEffect(() => {
if (descriptionInput.current) {
descriptionInput.current.focus();
}
}, [state]);
const relabel = event =>
setState(state => ({ ...state, editLocation: true }));
const setDescriptionUpdateMode = () => {
descriptionInput.current.focus();
setState(state => ({ ...state, editDescription: true }));
};
constructor(props) {
super(props);
const descriptionCancel = () => {
const { description = '' } = cloneDeep(state);
descriptionInput.current.value = description;
setState(state => ({ ...state, editDescription: false }));
};
const { location, locationLabel, description } = props.measurementData;
this.state = {
location,
locationLabel,
description,
skipAddLabelButton: props.skipAddLabelButton,
editDescription: props.editDescription,
editLocation: props.editLocation,
confirmationState: false,
displayComponent: true,
};
this.mainElement = React.createRef();
this.descriptionInput = React.createRef();
this.initialItems = OHIFLabellingData;
this.currentItems = cloneDeep(this.initialItems);
}
componentDidUpdate = () => {
if (this.state.editDescription) {
this.descriptionInput.current.focus();
const handleKeyPress = event => {
if (event.key === 'Enter') {
descriptionSave();
}
};
render() {
let mainElementClassName = 'labellingComponent';
if (this.state.editDescription) {
mainElementClassName += ' editDescription';
}
const descriptionSave = () => {
const description = descriptionInput.current.value;
updateLabelling({ description });
return (
<LabellingTransition
displayComponent={this.state.displayComponent}
onTransitionExit={this.props.labellingDoneCallback}
>
<>
<div
className={mainElementClassName}
ref={this.mainElement}
onMouseLeave={this.fadeOutAndLeave}
onMouseEnter={this.clearFadeOutTimer}
>
{this.labellingStateFragment()}
</div>
</>
</LabellingTransition>
);
}
labellingStateFragment = () => {
const {
skipAddLabelButton,
editLocation,
setState(state => ({
...state,
description,
locationLabel,
} = this.state;
editDescription: false,
}));
};
const selectTreeSelectCallback = (event, itemSelected) => {
const location = itemSelected.value;
const locationLabel = itemSelected.label;
updateLabelling({ location });
setState(state => ({
...state,
editLocation: false,
measurementData: {
...state.measurementData,
location,
locationLabel,
},
}));
};
const showLabelling = () => {
setState(state => ({
...state,
skipAddLabelButton: true,
editLocation: false,
}));
};
/*
* Waits for 1 sec to dismiss the labelling component.
*
*/
const fadeOutAndLeave = () =>
setFadeOutTimer(setTimeout(fadeOutAndLeaveFast, 1000));
const fadeOutAndLeaveFast = () => setShowComponent(false);
const clearFadeOutTimer = () => {
if (fadeOutTimer) {
clearTimeout(fadeOutTimer);
setFadeOutTimer(null);
}
};
const descriptionDialogUpdate = description => {
updateLabelling({ description });
labellingDoneCallback();
};
const labellingStateFragment = () => {
const { skipAddLabelButton, editLocation, measurementData } = state;
const { description, locationLabel, location } = measurementData;
if (!skipAddLabelButton) {
return (
<>
<button
type="button"
className="addLabelButton"
onClick={this.showLabelling}
>
{this.state.location ? 'Edit' : 'Add'} Label
</button>
</>
<button
type="button"
className="addLabelButton"
onClick={showLabelling}
>
{location ? 'Edit' : 'Add'} Label
</button>
);
} else {
if (editLocation) {
return (
<SelectTree
items={this.currentItems}
items={OHIFLabellingData}
columns={1}
onSelected={this.selectTreeSelectCallback}
onSelected={selectTreeSelectCallback}
selectTreeFirstTitle="Assign Label"
/>
);
} else {
return (
<>
<div
className="checkIconWrapper"
onClick={this.fadeOutAndLeaveFast}
>
<div className="checkIconWrapper" onClick={fadeOutAndLeaveFast}>
<Icon name="check" className="checkIcon" />
</div>
<div className="locationDescriptionWrapper">
@@ -114,10 +167,10 @@ export default class LabellingFlow extends Component {
<div className="description">
<input
id="descriptionInput"
ref={this.descriptionInput}
ref={descriptionInput}
defaultValue={description || ''}
autoComplete="off"
onKeyPress={this.handleKeyPress}
onKeyPress={handleKeyPress}
/>
</div>
</div>
@@ -125,14 +178,14 @@ export default class LabellingFlow extends Component {
<button
type="button"
className="commonButton left"
onClick={this.relabel}
onClick={relabel}
>
Relabel
</button>
<button
type="button"
className="commonButton right"
onClick={this.setDescriptionUpdateMode}
onClick={setDescriptionUpdateMode}
>
{description ? 'Edit ' : 'Add '}
Description
@@ -142,14 +195,14 @@ export default class LabellingFlow extends Component {
<button
type="button"
className="commonButton left"
onClick={this.descriptionCancel}
onClick={descriptionCancel}
>
Cancel
</button>
<button
type="button"
className="commonButton right"
onClick={this.descriptionSave}
onClick={descriptionSave}
>
Save
</button>
@@ -160,77 +213,51 @@ export default class LabellingFlow extends Component {
}
};
relabel = event => this.setState({ editLocation: true });
setDescriptionUpdateMode = () => {
this.descriptionInput.current.focus();
this.setState({ editDescription: true });
};
descriptionCancel = () => {
const { description = '' } = cloneDeep(this.state);
this.descriptionInput.current.value = description;
this.setState({ editDescription: false });
};
handleKeyPress = e => {
if (e.key === 'Enter') {
this.descriptionSave();
}
};
descriptionSave = () => {
const description = this.descriptionInput.current.value;
this.props.updateLabelling({ description });
this.setState({
description,
editDescription: false,
});
};
selectTreeSelectCallback = (event, itemSelected) => {
const location = itemSelected.value;
this.props.updateLabelling({ location });
this.setState({
editLocation: false,
confirmationState: true,
location: itemSelected.value,
locationLabel: itemSelected.label,
});
if (this.isTouchScreen) {
this.setTimeout = setTimeout(() => {
this.setState({
displayComponent: false,
});
}, 2000);
}
};
showLabelling = () => {
this.setState({
skipAddLabelButton: true,
editLocation: false,
});
};
fadeOutAndLeave = () => {
// Wait for 1 sec to dismiss the labelling component
this.fadeOutTimer = setTimeout(
() => this.setState({ displayComponent: false }),
1000
if (editDescriptionOnDialog) {
return (
<EditDescriptionDialog
onCancel={labellingDoneCallback}
onUpdate={descriptionDialogUpdate}
measurementData={state.measurementData}
/>
);
};
}
fadeOutAndLeaveFast = () => this.setState({ displayComponent: false });
return (
<LabellingTransition
displayComponent={showComponent}
onTransitionExit={labellingDoneCallback}
>
<>
<div
className={`labellingComponent ${state.editDescription &&
'editDescription'}`}
onMouseLeave={fadeOutAndLeave}
onMouseEnter={clearFadeOutTimer}
>
{labellingStateFragment()}
</div>
</>
</LabellingTransition>
);
};
clearFadeOutTimer = () => {
if (!this.fadeOutTimer) {
return;
}
LabellingFlow.propTypes = {
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
initialTopDistance: PropTypes.number,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
editDescriptionOnDialog: PropTypes.bool,
};
clearTimeout(this.fadeOutTimer);
};
}
LabellingFlow.defaultProps = {
skipAddLabelButton: false,
editLocation: false,
editDescription: false,
editDescriptionOnDialog: false,
};
export default LabellingFlow;
@@ -1,103 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
import LabellingFlow from './LabellingFlow.js';
import './LabellingManager.css';
export default class LabellingManager extends Component {
static propTypes = {
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
editDescriptionOnDialog: PropTypes.bool,
};
static defaultProps = {
skipAddLabelButton: false,
editLocation: false,
editDescription: false,
editDescriptionOnDialog: false,
};
constructor(props) {
super(props);
const measurementData = cloneDeep(props.measurementData);
this.treatMeasurementData(measurementData);
let editLocation = props.editLocation;
if (!props.editDescription && !props.editLocation) {
editLocation = true;
}
this.state = {
skipAddLabelButton: props.skipAddLabelButton,
editLocation: editLocation,
editDescription: props.editDescription,
editDescriptionOnDialog: props.editDescriptionOnDialog,
measurementData: measurementData,
};
}
componentDidMount = () => {
document.addEventListener('touchstart', this.onTouchStart);
};
componentWillUnmount = () => {
document.removeEventListener('touchstart', this.onTouchStart);
};
render() {
return this.getRenderComponent();
}
getRenderComponent = () => {
const {
editLocation,
editDescription,
editDescriptionOnDialog,
measurementData,
} = this.state;
if (editDescriptionOnDialog) {
return (
<EditDescriptionDialog
onCancel={this.props.labellingDoneCallback}
onUpdate={this.descriptionDialogUpdate}
measurementData={measurementData}
/>
);
}
if (editLocation || editDescription) {
return <LabellingFlow {...this.props} />;
}
};
treatMeasurementData = measurementData => {
const { editDescription, editLocation } = this.props;
if (editDescription) {
measurementData.description = undefined;
}
if (editLocation) {
measurementData.location = undefined;
}
};
responseDialogUpdate = response => {
this.props.updateLabelling({ response });
this.props.labellingDoneCallback();
};
descriptionDialogUpdate = description => {
this.props.updateLabelling({ description });
this.props.labellingDoneCallback();
};
}
@@ -1,13 +1,20 @@
import { connect } from "react-redux";
import Viewer from "./Viewer.js";
import OHIF from "@ohif/core";
import { connect } from 'react-redux';
import Viewer from './Viewer.js';
import OHIF from '@ohif/core';
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
const mapStateToProps = (state, ownProps) => {
const getActiveServer = servers => {
const isActive = a => a.active === true;
return servers.servers.find(isActive);
};
const mapStateToProps = state => {
const { viewports, servers } = state;
return {
viewports: state.viewports.viewportSpecificData,
activeViewportIndex: state.viewports.activeViewportIndex
viewports: viewports.viewportSpecificData,
activeViewportIndex: viewports.activeViewportIndex,
activeServer: getActiveServer(servers),
};
};
@@ -18,7 +25,7 @@ const mapDispatchToProps = dispatch => {
},
onMeasurementsUpdated: measurements => {
dispatch(setMeasurements(measurements));
}
},
};
};
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { commandsManager } from './../App.js';
import './ToolContextMenu.css';
import { ContextMenu } from '@ohif/ui';
const toolTypes = [
'Angle',
@@ -24,11 +24,13 @@ const ToolContextMenu = ({
}) => {
const defaultDropdownItems = [
{
label: 'Delete measurement',
actionType: 'Delete',
action: ({ nearbyToolData, eventData }) =>
onDelete(nearbyToolData, eventData),
},
{
label: 'Relabel',
actionType: 'setLabel',
action: ({ nearbyToolData, eventData }) => {
const { tool: measurementData } = nearbyToolData;
@@ -51,7 +53,10 @@ const ToolContextMenu = ({
availableToolTypes: toolTypes,
});
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
/*
* Annotate tools for touch events already have a press handle to edit it,
* has a better UX for deleting it.
*/
if (
isTouchEvent &&
nearbyToolData &&
@@ -63,21 +68,10 @@ const ToolContextMenu = ({
let dropdownItems = [];
if (nearbyToolData) {
defaultDropdownItems.forEach(item => {
item.params = {
eventData,
nearbyToolData,
};
if (item.actionType === 'Delete') {
item.text = 'Delete measurement';
}
if (item.actionType === 'setLabel') {
item.text = 'Relabel';
}
item.params = { eventData, nearbyToolData };
if (item.actionType === 'setDescription') {
item.text = `${
item.label = `${
nearbyToolData.tool.description ? 'Edit' : 'Add'
} Description`;
}
@@ -89,7 +83,7 @@ const ToolContextMenu = ({
return dropdownItems;
};
const itemOnClickHandler = (action, params, onClose) => {
const onClickHandler = ({ action, params }) => {
action(params);
if (onClose) {
onClose();
@@ -99,23 +93,9 @@ const ToolContextMenu = ({
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
return (
dropdownItems.length &&
eventData && (
<div className="ToolContextMenu">
<ul className="bounded">
{dropdownItems.map(({ params, action, text, actionType }) => (
<li key={actionType}>
<button
className="form-action"
onClick={() => itemOnClickHandler(action, params, onClose)}
>
<span key={actionType}>{text}</span>
</button>
</li>
))}
</ul>
</div>
)
<div className="ToolContextMenu">
<ContextMenu items={dropdownItems} onClick={onClickHandler} />;
</div>
);
};
@@ -123,6 +103,9 @@ ToolContextMenu.propTypes = {
isTouchEvent: PropTypes.bool.isRequired,
eventData: PropTypes.object,
onClose: PropTypes.func,
onSetDescription: PropTypes.func,
onSetLabel: PropTypes.func,
onDelete: PropTypes.func,
};
ToolContextMenu.defaultProps = {
@@ -11,6 +11,7 @@ import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js';
import { extensionManager } from './../App.js';
import DICOMSR from '../lib/DICOMSR';
// Contexts
import WhiteLabellingContext from '../context/WhiteLabellingContext.js';
@@ -57,21 +58,31 @@ class Viewer extends Component {
static propTypes = {
studies: PropTypes.array,
studyInstanceUids: PropTypes.array,
activeServer: PropTypes.shape({
type: PropTypes.string,
wadoRoot: PropTypes.string,
}),
onTimepointsUpdated: PropTypes.func,
onMeasurementsUpdated: PropTypes.func,
// window.store.getState().viewports.viewportSpecificData
viewports: PropTypes.object.isRequired,
// window.store.getState().viewports.activeViewportIndex
activeViewportIndex: PropTypes.number.isRequired,
isStudyLoaded: PropTypes.bool,
};
constructor(props) {
super(props);
const { activeServer } = this.props;
const server = Object.assign({}, activeServer);
OHIF.measurements.MeasurementApi.setConfiguration({
dataExchange: {
retrieve: this.retrieveMeasurements,
store: this.storeMeasurements,
retrieve: DICOMSR.retrieveMeasurements,
store: DICOMSR.storeMeasurements,
},
server,
});
OHIF.measurements.TimepointApi.setConfiguration({
@@ -93,18 +104,6 @@ class Viewer extends Component {
thumbnails: [],
};
retrieveMeasurements = (patientId, timepointIds) => {
OHIF.log.info('retrieveMeasurements');
// TODO: Retrieve the measurements from the latest available SR
return Promise.resolve();
};
storeMeasurements = (measurementData, timepointIds) => {
OHIF.log.info('storeMeasurements');
// TODO: Store the measurements into a new SR sent to the active server
return Promise.resolve();
};
retrieveTimepoints = filter => {
OHIF.log.info('retrieveTimepoints');
@@ -171,7 +170,7 @@ class Viewer extends Component {
};
componentDidMount() {
const { studies } = this.props;
const { studies, isStudyLoaded } = this.props;
const { TimepointApi, MeasurementApi } = OHIF.measurements;
const currentTimepointId = 'TimepointId';
@@ -191,8 +190,11 @@ class Viewer extends Component {
const patientId = studies[0] && studies[0].patientId;
timepointApi.retrieveTimepoints({ patientId });
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
if (isStudyLoaded) {
this.measurementApi.retrieveMeasurements(patientId, [
currentTimepointId,
]);
}
this.setState({
thumbnails: _mapStudiesToThumbnails(studies),
});
@@ -200,18 +202,19 @@ class Viewer extends Component {
}
componentDidUpdate(prevProps) {
if (this.props.studies !== prevProps.studies) {
const { studies } = this.props;
const patientId = studies[0] && studies[0].patientId;
const currentTimepointId = this.currentTimepointId;
this.timepointApi.retrieveTimepoints({ patientId });
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
const { studies, isStudyLoaded } = this.props;
if (studies !== prevProps.studies) {
this.setState({
thumbnails: _mapStudiesToThumbnails(studies),
});
}
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
const patientId = studies[0] && studies[0].patientId;
const { currentTimepointId } = this;
this.timepointApi.retrieveTimepoints({ patientId });
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
}
}
render() {
@@ -294,11 +297,11 @@ class Viewer extends Component {
activeIndex={this.props.activeViewportIndex}
/>
) : (
<ConnectedStudyBrowser
studies={this.state.thumbnails}
studyMetadata={this.props.studies}
/>
)}
<ConnectedStudyBrowser
studies={this.state.thumbnails}
studyMetadata={this.props.studies}
/>
)}
</SidePanel>
{/* MAIN */}
@@ -334,7 +337,7 @@ export default Viewer;
* @param {Study[]} studies
* @param {DisplaySet[]} studies[].displaySets
*/
const _mapStudiesToThumbnails = function(studies) {
const _mapStudiesToThumbnails = function (studies) {
return studies.map(study => {
const { studyInstanceUid } = study;
@@ -178,6 +178,7 @@ function ViewerRetrieveStudyData({
// hooks
const [error, setError] = useState(false);
const [studies, setStudies] = useState([]);
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
const snackbarContext = useSnackbarContext();
const { appConfig = {} } = useContext(AppContext);
const { filterQueryParam: isFilterStrategy = false } = appConfig;
@@ -218,6 +219,7 @@ function ViewerRetrieveStudyData({
);
setStudies([...studies, study]);
setIsStudyLoaded(true);
};
/**
@@ -332,7 +334,11 @@ function ViewerRetrieveStudyData({
}
return (
<ConnectedViewer studies={studies} studyInstanceUids={studyInstanceUids} />
<ConnectedViewer
studies={studies}
isStudyLoaded={isStudyLoaded}
studyInstanceUids={studyInstanceUids}
/>
);
}
@@ -0,0 +1,73 @@
import { log, studies, utils } from '@ohif/core';
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 { DICOMWeb } from '@ohif/core';
import parseDicomStructuredReport from './parseDicomStructuredReport';
import parseMeasurementsData from './parseMeasurementsData';
import getAllDisplaySets from './utils/getAllDisplaySets';
import { api } from 'dicomweb-client';
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,82 @@
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,55 @@
import * as dcmjs from 'dcmjs';
import { log, measurements } from '@ohif/core';
import cornerstone from 'cornerstone-core';
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,22 @@
/**
* 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,57 @@
/**
* 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;
+13 -13
View File
@@ -876,7 +876,7 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.2.5":
"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc"
integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw==
@@ -6441,10 +6441,10 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress@^3.3.1:
version "3.5.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.5.0.tgz#e188bc8f48782953f6865d8830a4dc342334b81c"
integrity sha512-I1iSReD2C8CTP6s4BvQky4gEqHBnKLmhBIqFyCUZdj6BQ6ZDxGnmIbQPM5g79E2iP60KTIbTK99ZPSDVtsNUUg==
cypress@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.8.0.tgz#7d4cd08f81f9048ee36760cc9ee3b9014f9e84ab"
integrity sha512-gtEbqCgKETRc3pQFMsELRgIBNgiQg7vbOWTrCi7WE7bgOwNCaW9PEX8Jb3UN8z/maIp9WwzoFfeySfelYY7nRA==
dependencies:
"@cypress/listr-verbose-renderer" "0.4.1"
"@cypress/xvfb" "1.2.4"
@@ -6535,14 +6535,14 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dcmjs@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.6.1.tgz#453f764bca2cd8e3430126ba8385ef4c77071bcd"
integrity sha512-sJ0cRVZY02GSaAW5S9JPcv8+7iDtgrM0L/niYWAvu2piSa4bFSXwKi+t4XevAfkm+mF0sXZtaD15bw+/oiCYkw==
dcmjs@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.8.2.tgz#e1107273764de0a3052244b79d999fd870e58650"
integrity sha512-HEd1Rq12qxZXVXr5B4xgl/foCWaXxioO5v42FLgiUjKrbQR6iKIVcZsmE+3/AvAurcHiQFfW5vk5K4AY7eAp5A==
dependencies:
"@babel/polyfill" "^7.2.5"
"@babel/runtime" "^7.3.1"
loglevelnext "^3.0.0"
"@babel/polyfill" "^7.6.0"
"@babel/runtime" "^7.6.3"
loglevelnext "^3.0.1"
ndarray "^1.0.18"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
@@ -12373,7 +12373,7 @@ loglevelnext@^1.0.1, loglevelnext@^1.0.2:
es6-symbol "^3.1.1"
object.assign "^4.1.0"
loglevelnext@^3.0.0:
loglevelnext@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04"
integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA==