Compare commits

..
Author SHA1 Message Date
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
53 changed files with 1360 additions and 456 deletions

No files matched your search

+46
View File
@@ -367,6 +367,31 @@ workflows:
requires: requires:
- UNIT_TESTS - 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
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
requires:
- AWAIT_APPROVAL
PR_OPTIONAL_DOCKER_PUBLISH: PR_OPTIONAL_DOCKER_PUBLISH:
jobs: jobs:
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval # https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
@@ -419,6 +444,27 @@ workflows:
filters: filters:
branches: branches:
only: master only: master
# Update base branch snapshots
- cypress/run:
name: 'Generate Percy Snapshots'
executor: cypress/browsers-chrome76
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
filters:
branches:
only: master
- DOCKER_MASTER_PUBLISH: - DOCKER_MASTER_PUBLISH:
requires: requires:
- NPM_PUBLISH - NPM_PUBLISH
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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) ## [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 **Note:** Version bump only for package @ohif/extension-cornerstone
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone", "name": "@ohif/extension-cornerstone",
"version": "2.0.1", "version": "2.1.0",
"description": "OHIF extension for Cornerstone", "description": "OHIF extension for Cornerstone",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -34,7 +34,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
+20 -14
View File
@@ -102,7 +102,7 @@ export default function init({ servicesManager, configuration }) {
]; ];
/* Add extension tools configuration here. */ /* Add extension tools configuration here. */
const extensionToolsConfiguration = { const internalToolsConfig = {
ArrowAnnotate: { ArrowAnnotate: {
configuration: { configuration: {
getTextCallback: (callback, eventDetails) => getTextCallback: (callback, eventDetails) =>
@@ -113,20 +113,26 @@ export default function init({ servicesManager, configuration }) {
}, },
}; };
const isEmpty = obj => Object.keys(obj).length < 1; /* Add tools with its custom props through extension configuration. */
if (!isEmpty(configuration.tools) || !isEmpty(extensionToolsConfiguration)) { tools.forEach(tool => {
/* Add tools with its custom props through extension configuration. */ const toolName = tool.name.replace('Tool', '');
tools.forEach(tool => { const externalToolsConfig = configuration.tools || {};
const toolName = tool.name.replace('Tool', ''); const externalToolProps = externalToolsConfig[toolName] || {};
const configurationToolProps = configuration.tools[toolName] || {}; const internalToolProps = internalToolsConfig[toolName] || {};
const extensionToolProps = extensionToolsConfiguration[toolName]; const props = merge(internalToolProps, externalToolProps);
let props = merge(extensionToolProps, configurationToolProps); csTools.addTool(tool, props);
csTools.addTool(tool, props); });
});
} else {
tools.forEach(tool => csTools.addTool(tool));
}
// TODO -> We need a better way to do this with maybe global tool state setting all tools passive.
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
tools.forEach(tool => {
if (tool.prototype instanceof BaseAnnotationTool) {
// BaseAnnotationTool would likely come from csTools lib exports
const toolName = new tool().name;
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
csTools.setToolActive('Pan', { mouseButtonMask: 4 }); csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 }); csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 }); csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02) ## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-html **Note:** Version bump only for package @ohif/extension-dicom-html
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-dicom-html", "name": "@ohif/extension-dicom-html",
"version": "1.0.2", "version": "1.1.0",
"description": "OHIF extension for rendering structured reports to HTML", "description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -29,7 +29,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6" "react-dom": "^16.8.6"
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.1...@ohif/extension-vtk@1.1.2) (2019-12-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.0...@ohif/extension-vtk@1.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.2...@ohif/extension-vtk@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11) ## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk **Note:** Version bump only for package @ohif/extension-vtk
+4 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-vtk", "name": "@ohif/extension-vtk",
"version": "1.0.2", "version": "1.1.2",
"description": "OHIF extension for VTK.js", "description": "OHIF extension for VTK.js",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -34,7 +34,7 @@
"@ohif/ui": "^0.50.0", "@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"i18next": "^17.0.3", "i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
@@ -52,11 +52,10 @@
"react-vtkjs-viewport": "^0.3.9" "react-vtkjs-viewport": "^0.3.9"
}, },
"devDependencies": { "devDependencies": {
"@ohif/core": "^2.0.2", "@ohif/core": "^2.1.0",
"@ohif/ui": "^1.0.1", "@ohif/ui": "^1.1.2",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1", "gh-pages": "^2.0.1",
"i18next": "^17.0.3", "i18next": "^17.0.3",
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.2...@ohif/core@2.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11) ## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/core **Note:** Version bump only for package @ohif/core
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/core", "name": "@ohif/core",
"version": "2.0.2", "version": "2.1.0",
"description": "Generic business logic for web-based medical imaging applications", "description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team", "author": "OHIF Core Team",
"license": "MIT", "license": "MIT",
@@ -1,4 +1,5 @@
import cornerstoneTools from 'cornerstone-tools'; import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import log from '../../log'; import log from '../../log';
import getLabel from '../lib/getLabel'; import getLabel from '../lib/getLabel';
import getDescription from '../lib/getDescription'; import getDescription from '../lib/getDescription';
@@ -7,13 +8,32 @@ import guid from '../../utils/guid';
import studyMetadataManager from '../../utils/studyMetadataManager'; import studyMetadataManager from '../../utils/studyMetadataManager';
import { measurementApiDefaultConfig } from './../configuration.js'; import { measurementApiDefaultConfig } from './../configuration.js';
const configuration = { const configuration = {
...measurementApiDefaultConfig, ...measurementApiDefaultConfig
}; };
export default class MeasurementApi { export default class MeasurementApi {
static Instance; static Instance;
/**
* Set configuration: It should merge default configuration with any new one
*
* @static
* @param {Object} config
* @param {Object} config.server
* @param {string} config.server.type - The server type
* @param {string} config.server.wadoRoot - The server wado URL root
* @param {Array} config.measurementTools
* @param {string} config.measurementTools[].id - The tool group id
* @param {string} config.measurementTools[].name - The tool group name
* @param {Array} config.measurementTools[].childTools - The child tool's configuration
* @param {Object} config.dataExchange
* @param {Function} config.dataExchange.store - Function that store measurement data
* @param {Function} config.dataExchange.retrieve - Function that retrieves measurement data
*
* @memberof MeasurementApi
*/
static setConfiguration(config) { static setConfiguration(config) {
Object.assign(configuration, config); Object.assign(configuration, config);
} }
@@ -203,13 +223,14 @@ export default class MeasurementApi {
retrieveMeasurements(patientId, timepointIds) { retrieveMeasurements(patientId, timepointIds) {
const retrievalFn = configuration.dataExchange.retrieve; const retrievalFn = configuration.dataExchange.retrieve;
const { server } = configuration;
if (typeof retrievalFn !== 'function') { if (typeof retrievalFn !== 'function') {
log.error('Measurement retrieval function has not been configured.'); log.error('Measurement retrieval function has not been configured.');
return; return;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
retrievalFn(patientId, timepointIds).then(measurementData => { retrievalFn(server).then(measurementData => {
if (measurementData) { if (measurementData) {
log.info('Measurement data retrieval'); log.info('Measurement data retrieval');
log.info(measurementData); log.info(measurementData);
@@ -230,6 +251,10 @@ export default class MeasurementApi {
// Synchronize the new tool data // Synchronize the new tool data
this.syncMeasurementsAndToolData(); this.syncMeasurementsAndToolData();
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
// Let others know that the measurements are updated // Let others know that the measurements are updated
this.onMeasurementsUpdated(); this.onMeasurementsUpdated();
}, reject); }, reject);
@@ -237,6 +262,7 @@ export default class MeasurementApi {
} }
storeMeasurements(timepointId) { storeMeasurements(timepointId) {
const { server } = configuration;
const storeFn = configuration.dataExchange.store; const storeFn = configuration.dataExchange.store;
if (typeof storeFn !== 'function') { if (typeof storeFn !== 'function') {
log.error('Measurement store function has not been configured.'); log.error('Measurement store function has not been configured.');
@@ -278,8 +304,9 @@ export default class MeasurementApi {
}; };
log.info('Saving Measurements for timepoints:', timepoints); log.info('Saving Measurements for timepoints:', timepoints);
return storeFn(measurementData, filter).then(() => { return storeFn(measurementData, filter, server).then(result => {
log.info('Measurement storage completed'); log.info('Measurement storage completed');
return result;
}); });
} }
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.2...@ohif/i18n@0.52.3) (2019-12-12)
### Bug Fixes
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28) ## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/i18n", "name": "@ohif/i18n",
"version": "0.52.2", "version": "0.52.3",
"description": "Internationalization library for The OHIF Viewer", "description": "Internationalization library for The OHIF Viewer",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -10,6 +10,7 @@
"Clear": "Clear", "Clear": "Clear",
"Coronal": "Coronal", "Coronal": "Coronal",
"Crosshairs": "Crosshairs", "Crosshairs": "Crosshairs",
"Download": "Download",
"Ellipse": "Ellipse", "Ellipse": "Ellipse",
"Elliptical": "Elliptical", "Elliptical": "Elliptical",
"Flip H": "Flip H", "Flip H": "Flip H",
+10 -9
View File
@@ -10,18 +10,19 @@
"Clear": "Limpiar", "Clear": "Limpiar",
"Coronal": "Coronal", "Coronal": "Coronal",
"Crosshairs": "Punto de mira", "Crosshairs": "Punto de mira",
"Download": "Descargar",
"Ellipse": "Elipse", "Ellipse": "Elipse",
"Elliptical": "Elíptico", "Elliptical": "Elíptico",
"Flip H": "Espejo H", "Flip H": "Voltear H",
"Flip V": "Espejo V", "Flip V": "Voltear V",
"Freehand": "Mano alzada", "Freehand": "Mano alzada",
"Invert": "Invertir", "Invert": "Negativo",
"Layout": "$t(Common:Layout)", "Layout": "$t(Common:Layout)",
"Length": "Longitud", "Length": "Longitud",
"Levels": "Niveles", "Levels": "W/L",
"Magnify": "Aumentar", "Magnify": "Lupa",
"Manual": "Manual", "Manual": "Manual",
"Measurements": "Mediciones", "Measurements": "Medidas",
"More": "$t(Common:More)", "More": "$t(Common:More)",
"Next": "$t(Common:Next)", "Next": "$t(Common:Next)",
"Pan": "Mover", "Pan": "Mover",
@@ -32,11 +33,11 @@
"Rectangle": "Rectángulo", "Rectangle": "Rectángulo",
"Reset": "$t(Common:Reset)", "Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:Reset) por defecto", "Reset to Defaults": "$t(Common:Reset) por defecto",
"Rotate Right": "Rotar ->", "Rotate Right": "Girar ->",
"Sagittal": "Sagital", "Sagittal": "Sagital",
"Save": "Guardar", "Save": "Guardar",
"Stack Scroll": "Scroll en pila", "Stack Scroll": "Scroll",
"Stop": "$t(Common:Stop)", "Stop": "$t(Common:Stop)",
"Themes": "Temas", "Themes": "Temas",
"Zoom": "Zoom" "Zoom": "Ampliar"
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"Image": "Imagen", "Image": "Imagen",
"Layout": "Disposición", "Layout": "Formato",
"Measurements": "Medidas", "Measurements": "Medidas",
"More": "Más", "More": "Más",
"Next": "Siguiente", "Next": "Siguiente",
"Play": "Play", "Play": "Play",
"Previous": "Anterior", "Previous": "Anterior",
"Reset": "Resetear", "Reset": "Restaurar",
"RowsPerPage": "filas por página", "RowsPerPage": "filas por página",
"Series": "Secuencia", "Series": "Secuencia",
"Show": "Mostrar", "Show": "Mostrar",
+3 -2
View File
@@ -10,6 +10,7 @@
"Clear": "Limpar", "Clear": "Limpar",
"Coronal": "Coronal", "Coronal": "Coronal",
"Crosshairs": "Localizador", "Crosshairs": "Localizador",
"Download": "Baixar",
"Ellipse": "Elipse", "Ellipse": "Elipse",
"Elliptical": "Elíptico", "Elliptical": "Elíptico",
"Flip H": "Inverter H", "Flip H": "Inverter H",
@@ -30,8 +31,8 @@
"Probe": "Prova", "Probe": "Prova",
"ROI Window": "Janela ROI", "ROI Window": "Janela ROI",
"Rectangle": "Retângulo", "Rectangle": "Retângulo",
"Reset": "Resetar", "Reset": "$t(Common:Reset)",
"Reset to Defaults": "Restaurar para o Padrão", "Reset to Defaults": "$t(Common:Reset) para o Padrão",
"Rotate Right": "Girar à direita", "Rotate Right": "Girar à direita",
"Sagittal": "Sagital", "Sagittal": "Sagital",
"Save": "Salvar", "Save": "Salvar",
+1 -1
View File
@@ -6,6 +6,6 @@
"Next": "Próximo", "Next": "Próximo",
"Play": "Play", "Play": "Play",
"Previous": "Anterior", "Previous": "Anterior",
"Reset": "Reset", "Reset": "Restaurar",
"Stop": "Stop" "Stop": "Stop"
} }
@@ -1,6 +1,6 @@
{ {
"Cancel": "Cancelar", "Cancel": "Cancelar",
"Reset to Defaults": "Restaurar Default", "Reset to Defaults": "$t(Common:Reset) para Padrão",
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.", "ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
"Save": "Salvar", "Save": "Salvar",
"SaveMessage": "Preferências salvas", "SaveMessage": "Preferências salvas",
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.1...@ohif/ui@1.1.2) (2019-12-12)
### Bug Fixes
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.0...@ohif/ui@1.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/ui
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.1...@ohif/ui@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.0...@ohif/ui@1.0.1) (2019-12-09) ## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.0...@ohif/ui@1.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/ui **Note:** Version bump only for package @ohif/ui
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/ui", "name": "@ohif/ui",
"version": "1.0.1", "version": "1.1.2",
"description": "A set of React components for Medical Imaging Viewers", "description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors", "author": "OHIF Contributors",
"license": "MIT", "license": "MIT",
@@ -32,7 +32,7 @@ const ViewportDownloadForm = ({
maximumSize, maximumSize,
canvasClass, canvasClass,
}) => { }) => {
const [t] = useTranslation('ViewportDownloadForm'); const [t] = useTranslation('Buttons');
const [filename, setFilename] = useState(DEFAULT_FILENAME); const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg'); const [fileType, setFileType] = useState('jpg');
@@ -196,11 +196,12 @@ const ViewportDownloadForm = ({
)} )}
</div> </div>
<div className="file-info-container"> <div className="file-info-container" data-cy="file-info-container">
<div className="col"> <div className="col">
<div className="width"> <div className="width">
<TextInput <TextInput
type="number" type="number"
data-cy="image-width"
min={minimumSize} min={minimumSize}
max={maximumSize} max={maximumSize}
value={width} value={width}
@@ -211,6 +212,7 @@ const ViewportDownloadForm = ({
<div className="height"> <div className="height">
<TextInput <TextInput
type="number" type="number"
data-cy="image-height"
min={minimumSize} min={minimumSize}
max={maximumSize} max={maximumSize}
value={height} value={height}
@@ -224,6 +226,7 @@ const ViewportDownloadForm = ({
<div className="file-name"> <div className="file-name">
<TextInput <TextInput
type="text" type="text"
data-cy="file-name"
value={filename} value={filename}
onChange={event => setFilename(event.target.value)} onChange={event => setFilename(event.target.value)}
label={t('File name')} label={t('File name')}
@@ -233,6 +236,7 @@ const ViewportDownloadForm = ({
<div className="file-type"> <div className="file-type">
<Select <Select
value={fileType} value={fileType}
data-cy="file-type"
onChange={event => setFileType(event.target.value)} onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS} options={FILE_TYPE_OPTIONS}
label={t('File type')} label={t('File type')}
@@ -245,6 +249,7 @@ const ViewportDownloadForm = ({
<label htmlFor="show-annotations" className="form-check-label"> <label htmlFor="show-annotations" className="form-check-label">
<input <input
id="show-annotations" id="show-annotations"
data-cy="show-annotations"
type="checkbox" type="checkbox"
className="form-check-input" className="form-check-input"
checked={showAnnotations} checked={showAnnotations}
@@ -278,23 +283,33 @@ const ViewportDownloadForm = ({
></canvas> ></canvas>
</div> </div>
<div className="preview"> <div className="preview" data-cy="image-preview">
<h4> {t('Image Preview')}</h4> <h4> {t('Image Preview')}</h4>
<img <img
className="viewport-preview" className="viewport-preview"
src={viewportPreview.src} src={viewportPreview.src}
alt="Viewport Preview" alt="Viewport Preview"
data-cy="viewport-preview-img"
/> />
</div> </div>
<div className="actions"> <div className="actions">
<div className="action-cancel"> <div className="action-cancel">
<button type="button" className="btn btn-danger" onClick={onClose}> <button
type="button"
data-cy="cancel-btn"
className="btn btn-danger"
onClick={onClose}
>
{t('Cancel')} {t('Cancel')}
</button> </button>
</div> </div>
<div className="action-save"> <div className="action-save">
<button onClick={downloadImage} className="btn btn-primary"> <button
onClick={downloadImage}
className="btn btn-primary"
data-cy="download-btn"
>
{t('Download')} {t('Download')}
</button> </button>
</div> </div>
@@ -22,8 +22,9 @@ class MeasurementTable extends Component {
onDeleteClick: PropTypes.func, onDeleteClick: PropTypes.func,
onEditDescriptionClick: PropTypes.func, onEditDescriptionClick: PropTypes.func,
selectedMeasurementNumber: PropTypes.number, selectedMeasurementNumber: PropTypes.number,
overwallWarnings: PropTypes.object,
t: PropTypes.func, t: PropTypes.func,
saveFunction: PropTypes.func,
onSaveComplete: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
@@ -38,8 +39,9 @@ class MeasurementTable extends Component {
}; };
render() { render() {
const hasOverallWarnings = const { overallWarnings, saveFunction, t } = this.props;
this.props.overallWarnings.warningList.length > 0; const hasOverallWarnings = overallWarnings.warningList.length > 0;
return ( return (
<div className="measurementTable"> <div className="measurementTable">
<div className="measurementTableHeader"> <div className="measurementTableHeader">
@@ -55,7 +57,7 @@ class MeasurementTable extends Component {
style={{}} style={{}}
> >
<div className="warningTitle"> <div className="warningTitle">
{this.props.t('Criteria nonconformities')} {t('Criteria nonconformities')}
</div> </div>
<div className="warningContent"> <div className="warningContent">
{this.getWarningContent()} {this.getWarningContent()}
@@ -75,10 +77,42 @@ class MeasurementTable extends Component {
<ScrollableArea> <ScrollableArea>
<div>{this.getMeasurementsGroups()}</div> <div>{this.getMeasurementsGroups()}</div>
</ScrollableArea> </ScrollableArea>
<div className="measurementTableFooter">
{saveFunction && (
<button onClick={this.saveFunction} className="saveBtn">
<Icon name="save" width="14px" height="14px" />
Save measurements
</button>
)}
</div>
</div> </div>
); );
} }
saveFunction = async event => {
const { saveFunction, onSaveComplete } = this.props;
if (saveFunction) {
try {
const result = await saveFunction();
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: result.message,
type: 'success',
});
}
} catch (error) {
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: error.message,
type: 'error',
});
}
}
}
};
getMeasurementsGroups = () => { getMeasurementsGroups = () => {
return this.props.measurementCollection.map((measureGroup, index) => { return this.props.measurementCollection.map((measureGroup, index) => {
return ( return (
@@ -157,7 +191,7 @@ class MeasurementTable extends Component {
}; };
getWarningContent = () => { getWarningContent = () => {
const { warningList = '' } = this.props.overwallWarnings; const { warningList = '' } = this.props.overallWarnings;
if (Array.isArray(warningList)) { if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => { const listedWarnings = warningList.map((warn, index) => {
@@ -1,92 +1,117 @@
.measurementTable .measurementTable
background-color: var(--primary-background-color) background-color: var(--primary-background-color)
height: 100%; height: 100%
width: 100%; width: 100%
display: flex; display: flex
flex-direction: column; flex-direction: column
.maxMeasurements .maxMeasurements
border-radius: 3px; border-radius: 3px
color: var(--primary-background-color); color: var(--primary-background-color)
font-size: 12px; font-size: 12px
font-weight: 500; font-weight: 500
height: 19px; height: 19px
line-height: 17px; line-height: 17px
margin-top: 22px; margin-top: 22px
margin-right: 15px; margin-right: 15px
padding: 2px 6px 0; padding: 2px 6px 0
text-transform: uppercase; text-transform: uppercase
transition: all 0.3s ease; transition: all 0.3s ease
background-color: var(--default-color); background-color: var(--default-color)
.measurementTableHeader .measurementTableHeader
padding-left: 45px; padding-left: 45px
.measurementTableHeaderItem .measurementTableHeaderItem
display: inline-block; display: inline-block
margin-top: 9px; margin-top: 9px
margin-left: 9px; margin-left: 9px
padding-left: 9px; padding-left: 9px
width: 90px; width: 90px
font-size: 14px; font-size: 14px
line-height: 20px; line-height: 20px
border-left: 1px solid var(--text-secondary-color); border-left: 1px solid var(--text-secondary-color)
color: var(--text-primary-color); color: var(--text-primary-color)
.timepointLabel .timepointLabel
color: var(--text-secondary-color); color: var(--text-secondary-color)
font-size: 12px; font-size: 12px
.warning-status .warning-status
display: block; display: block
position: absolute; position: absolute
width: 50px; width: 50px
height: 40px; height: 40px
padding-left: 7px; padding-left: 7px
left: 15px; left: 15px
top: 7px; top: 7px
.warning-border .warning-border
display: block; display: block
border: 2px solid #e29e4a; border: 2px solid #e29e4a
border-radius: 36px; border-radius: 36px
padding: 6px 7px; padding: 6px 7px
width: 40px; width: 40px
height: 40px; height: 40px
svg svg
color: #e29e4a; color: #e29e4a
width: 22px; width: 22px
height: 20px; height: 20px
pointer-events: inherit; pointer-events: inherit
.measurementTableFooter
padding: 10px
color: #fff
text-align: center
z-index: 1
.saveBtn
border: 1px solid var(--ui-border-color-active)
border-radius: 25px
background-color: var(--active-color)
color: var(--ui-gray-dark)
line-height: 25px
padding: 0 20px
outline: none
cursor: pointer
&:hover, &:active
background-color: var(--ui-sky-blue)
svg
margin-right: 4px
position: relative
top: 2px
.tooltip-warning .tooltip-warning
&.in &.in
opacity 1; opacity 1
.tooltip-inner .tooltip-inner
padding: 0 padding: 0
.warningTitle .warningTitle
text-align: left text-align: left
background-color: var(--ui-gray-darkest); background-color: var(--ui-gray-darkest)
padding: 7px 15px; padding: 7px 15px
border-top-left-radius: 7px; border-top-left-radius: 7px
border-top-right-radius: 7px; border-top-right-radius: 7px
.warningContent .warningContent
text-align center text-align center
background-color: var(--ui-gray); background-color: var(--ui-gray)
border-bottom-left-radius: 7px; border-bottom-left-radius: 7px
border-bottom-right-radius: 7px; border-bottom-right-radius: 7px
ol ol
margin-top: auto; margin-top: auto
padding-inline-start: 20px; padding-inline-start: 20px
padding-top: 10px; padding-top: 10px
padding-bottom: 10px; padding-bottom: 10px
padding-left: 25px; padding-left: 25px
text-align: left; text-align: left
li li
padding-right: 10px; padding-right: 10px
@@ -29,7 +29,7 @@ const OHIFModal = ({
const renderHeader = () => { const renderHeader = () => {
return ( return (
title && ( title && (
<div className="OHIFModal__header"> <div className="OHIFModal__header" data-cy="modal-header">
<h4>{title}</h4> <h4>{title}</h4>
{closeButton && ( {closeButton && (
<button data-cy="close-button" onClick={onClose}> <button data-cy="close-button" onClick={onClose}>
@@ -44,6 +44,7 @@ const OHIFModal = ({
return ( return (
<Modal <Modal
className={classNames('OHIFModal', className)} className={classNames('OHIFModal', className)}
data-cy="modal"
shouldCloseOnEsc={shouldCloseOnEsc} shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen} isOpen={isOpen}
title={title} title={title}
@@ -51,7 +52,9 @@ const OHIFModal = ({
> >
<> <>
{renderHeader()} {renderHeader()}
<div className="OHIFModal__content">{children}</div> <div className="OHIFModal__content" data-cy="modal-content">
{children}
</div>
</> </>
</Modal> </Modal>
); );
+2
View File
@@ -62,6 +62,7 @@ import powerOff from './icons/power-off.svg';
import reset from './icons/reset.svg'; import reset from './icons/reset.svg';
import rotate from './icons/rotate.svg'; import rotate from './icons/rotate.svg';
import rotateRight from './icons/rotate-right.svg'; import rotateRight from './icons/rotate-right.svg';
import saveRegular from './icons/save-regular.svg';
import search from './icons/search.svg'; import search from './icons/search.svg';
import searchPlus from './icons/search-plus.svg'; import searchPlus from './icons/search-plus.svg';
import softTissue from './icons/soft-tissue.svg'; import softTissue from './icons/soft-tissue.svg';
@@ -164,6 +165,7 @@ const ICONS = {
'soft-tissue': softTissue, 'soft-tissue': softTissue,
lung, lung,
liver, liver,
save: saveRegular,
}; };
/** /**
@@ -0,0 +1,11 @@
<svg
aria-hidden="true"
focusable="false"
data-icon="save"
role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<path
fill="currentColor"
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM272 80v80H144V80h128zm122 352H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h42v104c0 13.255 10.745 24 24 24h176c13.255 0 24-10.745 24-24V83.882l78.243 78.243a6 6 0 0 1 1.757 4.243V426a6 6 0 0 1-6 6zM224 232c-48.523 0-88 39.477-88 88s39.477 88 88 88 88-39.477 88-88-39.477-88-88-88zm0 128c-22.056 0-40-17.944-40-40s17.944-40 40-40 40 17.944 40 40-17.944 40-40 40z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 738 B

+35
View File
@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.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) ## [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 **Note:** Version bump only for package @ohif/viewer
@@ -81,10 +81,8 @@ describe('OHIF Cornerstone Hotkeys', () => {
}); });
it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => { it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => {
//Click on Layout button //Select viewport layout (3,1)
cy.get('@layoutBtn').click(); cy.setLayout(3, 1);
//Select 3 viewports
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
cy.waitViewportImageLoading(); cy.waitViewportImageLoading();
// Press multiples hotkeys on viewport #1 // Press multiples hotkeys on viewport #1
@@ -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');
});
});
@@ -11,6 +11,7 @@ export function initCornerstoneToolsAliases() {
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn'); cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
cy.get('.expandableToolMenu').as('moreBtn'); cy.get('.expandableToolMenu').as('moreBtn');
cy.get('.btn-group > .toolbar-button').as('layoutBtn'); cy.get('.btn-group > .toolbar-button').as('layoutBtn');
cy.get('.viewport-element').as('viewport');
} }
//Creating aliases for Common page elements //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) => { Cypress.Commands.add('setLanguage', (language, save = true) => {
cy.openPreferences(); cy.openPreferences();
+8 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/viewer", "name": "@ohif/viewer",
"version": "3.0.2", "version": "3.1.2",
"description": "OHIF Viewer", "description": "OHIF Viewer",
"author": "OHIF Contributors", "author": "OHIF Contributors",
"license": "MIT", "license": "MIT",
@@ -45,14 +45,14 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"@ohif/core": "^2.0.2", "@ohif/core": "^2.1.0",
"@ohif/extension-cornerstone": "^2.0.1", "@ohif/extension-cornerstone": "^2.1.0",
"@ohif/extension-dicom-html": "^1.0.2", "@ohif/extension-dicom-html": "^1.1.0",
"@ohif/extension-dicom-microscopy": "^0.50.6", "@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.0", "@ohif/extension-dicom-pdf": "^1.0.0",
"@ohif/extension-vtk": "^1.0.2", "@ohif/extension-vtk": "^1.1.2",
"@ohif/i18n": "^0.52.2", "@ohif/i18n": "^0.52.3",
"@ohif/ui": "^1.0.1", "@ohif/ui": "^1.1.2",
"@tanem/react-nprogress": "^1.1.25", "@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"core-js": "^3.2.1", "core-js": "^3.2.1",
@@ -60,7 +60,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4", "dicomweb-client": "^0.4.4",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
+1 -1
View File
@@ -70,5 +70,5 @@ window.config = {
// ~ Cornerstone Tools // ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
], ],
cornerstoneExtensionConfig: { tools: {} }, cornerstoneExtensionConfig: {},
}; };
+3 -3
View File
@@ -9,7 +9,7 @@ import { hot } from 'react-hot-loader/root';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone'; import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import ToolContextMenu from './connectedComponents/ToolContextMenu'; import ToolContextMenu from './connectedComponents/ToolContextMenu';
import LabellingManager from './components/Labelling/LabellingManager'; import LabellingFlow from './components/Labelling/LabellingFlow';
import { import {
SnackbarProvider, SnackbarProvider,
@@ -183,7 +183,7 @@ class App extends Component {
> >
<LabellingFlowProvider <LabellingFlowProvider
service={UILabellingFlowService} service={UILabellingFlowService}
labellingComponent={LabellingManager} labellingComponent={LabellingFlow}
commandsManager={commandsManager} commandsManager={commandsManager}
> >
<ContextMenuProvider <ContextMenuProvider
@@ -220,7 +220,7 @@ class App extends Component {
<ModalProvider modal={OHIFModal} service={UIModalService}> <ModalProvider modal={OHIFModal} service={UIModalService}>
<LabellingFlowProvider <LabellingFlowProvider
service={UILabellingFlowService} service={UILabellingFlowService}
labellingComponent={LabellingManager} labellingComponent={LabellingFlow}
commandsManager={commandsManager} commandsManager={commandsManager}
> >
<ContextMenuProvider <ContextMenuProvider
@@ -3,12 +3,20 @@ import { MeasurementTable } from '@ohif/ui';
import OHIF from '@ohif/core'; import OHIF from '@ohif/core';
import moment from 'moment'; import moment from 'moment';
import cornerstone from 'cornerstone-core'; import cornerstone from 'cornerstone-core';
import DICOMSR from './../../lib/DICOMSR';
import jumpToRowItem from './jumpToRowItem.js'; import jumpToRowItem from './jumpToRowItem.js';
const { setViewportSpecificData } = OHIF.redux.actions; const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements; 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) { function groupBy(list, props) {
return list.reduce((a, b) => { return list.reduce((a, b) => {
(a[b[props]] = a[b[props]] || []).push(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 = []; let tools = [];
config.measurementTools.forEach( toolGroups.forEach(
toolGroup => (tools = tools.concat(toolGroup.childTools)) toolGroup => (tools = tools.concat(toolGroup.childTools))
); );
return tools; 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 = {}) { function getMeasurementText(measurementData = {}) {
const defaultText = '...'; const defaultText = '...';
const { location = '', description = '' } = measurementData; const { location = '', description = '' } = measurementData;
@@ -34,6 +55,14 @@ function getMeasurementText(measurementData = {}) {
return result || defaultText; 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( function getDataForEachMeasurementNumber(
measurementNumberList, measurementNumberList,
timepoints, timepoints,
@@ -56,10 +85,35 @@ function getDataForEachMeasurementNumber(
return data; 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) { function convertMeasurementsToTableData(toolCollections, timepoints) {
const config = OHIF.measurements.MeasurementApi.getConfiguration(); const config = OHIF.measurements.MeasurementApi.getConfiguration();
const toolGroups = config.measurementTools; const toolGroups = config.measurementTools;
const tools = getAllTools(); const tools = getAllTools(toolGroups);
const tableMeasurements = toolGroups.map(toolGroup => { const tableMeasurements = toolGroups.map(toolGroup => {
return { return {
@@ -88,6 +142,12 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
} = measurementData; } = measurementData;
const measurementId = measurementData._id; const measurementId = measurementData._id;
const {
hasWarnings,
warningTitle,
warningList,
} = getWarningsForMeasurement(toolType);
//check if all measurements with same measurementNumber will have same LABEL //check if all measurements with same measurementNumber will have same LABEL
const tableMeasurement = { const tableMeasurement = {
itemNumber: lesionNamingNumber, itemNumber: lesionNamingNumber,
@@ -96,10 +156,10 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
measurementNumber, measurementNumber,
lesionNamingNumber, lesionNamingNumber,
toolType, toolType,
hasWarnings: false, //TODO hasWarnings,
warningTitle: '', //TODO warningTitle,
warningList,
isSplitLesion: false, //TODO isSplitLesion: false, //TODO
warningList: [], //TODO
data: getDataForEachMeasurementNumber( data: getDataForEachMeasurementNumber(
measurementNumberList, measurementNumberList,
timepoints, timepoints,
@@ -126,6 +186,13 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
return tableMeasurements; 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) { function convertTimepointsToTableData(timepoints) {
if (!timepoints || !timepoints.length) { if (!timepoints || !timepoints.length) {
return []; 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 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 { return {
timepoints: convertTimepointsToTableData(timepoints), timepoints: convertTimepointsToTableData(timepoints),
measurementCollection: convertMeasurementsToTableData( measurementCollection: convertMeasurementsToTableData(
@@ -150,6 +237,7 @@ const mapStateToProps = state => {
), ),
timepointManager: state.timepointManager, timepointManager: state.timepointManager,
viewports: state.viewports, viewports: state.viewports,
saveFunction,
}; };
}; };
@@ -265,10 +353,15 @@ const mapDispatchToProps = (dispatch, ownProps) => {
}; };
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => { const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
const { timepoints, saveFunction, measurementCollection } = propsFromState;
const { onSaveComplete, selectedMeasurementNumber } = ownProps;
return { return {
timepoints: propsFromState.timepoints, timepoints,
measurementCollection: propsFromState.measurementCollection, saveFunction,
selectedMeasurementNumber: ownProps.selectedMeasurementNumber, measurementCollection,
onSaveComplete,
selectedMeasurementNumber,
...propsFromDispatch, ...propsFromDispatch,
onItemClick: (event, measurementData) => { onItemClick: (event, measurementData) => {
// TODO: Add timepointId to .data for measurementData? // TODO: Add timepointId to .data for measurementData?
@@ -11,8 +11,9 @@ export default {
preRegistration({ servicesManager, commandsManager, configuration = {} }) { preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration }); init({ servicesManager, commandsManager, configuration });
}, },
getPanelModule({ servicesManager, commandsManager }) { getPanelModule({ servicesManager, commandsManager }) {
const { UILabellingFlowService } = servicesManager.services; const { UILabellingFlowService, UINotificationService } = servicesManager.services;
const ExtendedConnectedMeasurementTable = () => ( const ExtendedConnectedMeasurementTable = () => (
<ConnectedMeasurementTable <ConnectedMeasurementTable
onRelabel={tool => { onRelabel={tool => {
@@ -38,6 +39,11 @@ export default {
}); });
} }
}} }}
onSaveComplete={message => {
if (UINotificationService) {
UINotificationService.show(message);
}
}}
/> />
); );
return { return {
@@ -6,8 +6,7 @@
max-height: 500px; max-height: 500px;
} }
.labellingComponent .selectedLabel, .labellingComponent .selectedLabel, .labellingComponent .selectedDescription {
.labellingComponent .selectedDescription {
padding: 5px; padding: 5px;
background-color: white; background-color: white;
width: 150px; width: 150px;
@@ -86,14 +85,12 @@
color: #337ab7; color: #337ab7;
} }
.labellingComponent .commonButtons, .labellingComponent .commonButtons, .labellingComponent.editDescription .editDescriptionButtons {
.labellingComponent.editDescription .editDescriptionButtons {
display: block; display: block;
margin-left: 55px; margin-left: 55px;
} }
.labellingComponent.editDescription .commonButtons, .labellingComponent.editDescription .commonButtons, .labellingComponent .editDescriptionButtons {
.labellingComponent .editDescriptionButtons {
display: none; display: none;
} }
@@ -1,112 +1,165 @@
import { Icon, SelectTree } from '@ohif/ui'; import { Icon, SelectTree } from '@ohif/ui';
import React, { Component } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';
import LabellingTransition from './LabellingTransition.js'; import LabellingTransition from './LabellingTransition.js';
import OHIFLabellingData from './OHIFLabellingData.js'; import OHIFLabellingData from './OHIFLabellingData.js';
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
import './LabellingFlow.css';
export default class LabellingFlow extends Component { const LabellingFlow = ({
static propTypes = { measurementData,
measurementData: PropTypes.object.isRequired, editLocation,
labellingDoneCallback: PropTypes.func.isRequired, editDescription,
updateLabelling: PropTypes.func.isRequired, skipAddLabelButton,
initialTopDistance: PropTypes.number, updateLabelling,
skipAddLabelButton: PropTypes.bool, labellingDoneCallback,
editLocation: PropTypes.bool, editDescriptionOnDialog,
editDescription: PropTypes.bool, }) => {
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) { const descriptionCancel = () => {
super(props); const { description = '' } = cloneDeep(state);
descriptionInput.current.value = description;
setState(state => ({ ...state, editDescription: false }));
};
const { location, locationLabel, description } = props.measurementData; const handleKeyPress = event => {
if (event.key === 'Enter') {
this.state = { descriptionSave();
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();
} }
}; };
render() { const descriptionSave = () => {
let mainElementClassName = 'labellingComponent'; const description = descriptionInput.current.value;
if (this.state.editDescription) { updateLabelling({ description });
mainElementClassName += ' editDescription';
}
return ( setState(state => ({
<LabellingTransition ...state,
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,
description, description,
locationLabel, editDescription: false,
} = this.state; }));
};
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) { if (!skipAddLabelButton) {
return ( return (
<> <button
<button type="button"
type="button" className="addLabelButton"
className="addLabelButton" onClick={showLabelling}
onClick={this.showLabelling} >
> {location ? 'Edit' : 'Add'} Label
{this.state.location ? 'Edit' : 'Add'} Label </button>
</button>
</>
); );
} else { } else {
if (editLocation) { if (editLocation) {
return ( return (
<SelectTree <SelectTree
items={this.currentItems} items={OHIFLabellingData}
columns={1} columns={1}
onSelected={this.selectTreeSelectCallback} onSelected={selectTreeSelectCallback}
selectTreeFirstTitle="Assign Label" selectTreeFirstTitle="Assign Label"
/> />
); );
} else { } else {
return ( return (
<> <>
<div <div className="checkIconWrapper" onClick={fadeOutAndLeaveFast}>
className="checkIconWrapper"
onClick={this.fadeOutAndLeaveFast}
>
<Icon name="check" className="checkIcon" /> <Icon name="check" className="checkIcon" />
</div> </div>
<div className="locationDescriptionWrapper"> <div className="locationDescriptionWrapper">
@@ -114,10 +167,10 @@ export default class LabellingFlow extends Component {
<div className="description"> <div className="description">
<input <input
id="descriptionInput" id="descriptionInput"
ref={this.descriptionInput} ref={descriptionInput}
defaultValue={description || ''} defaultValue={description || ''}
autoComplete="off" autoComplete="off"
onKeyPress={this.handleKeyPress} onKeyPress={handleKeyPress}
/> />
</div> </div>
</div> </div>
@@ -125,14 +178,14 @@ export default class LabellingFlow extends Component {
<button <button
type="button" type="button"
className="commonButton left" className="commonButton left"
onClick={this.relabel} onClick={relabel}
> >
Relabel Relabel
</button> </button>
<button <button
type="button" type="button"
className="commonButton right" className="commonButton right"
onClick={this.setDescriptionUpdateMode} onClick={setDescriptionUpdateMode}
> >
{description ? 'Edit ' : 'Add '} {description ? 'Edit ' : 'Add '}
Description Description
@@ -142,14 +195,14 @@ export default class LabellingFlow extends Component {
<button <button
type="button" type="button"
className="commonButton left" className="commonButton left"
onClick={this.descriptionCancel} onClick={descriptionCancel}
> >
Cancel Cancel
</button> </button>
<button <button
type="button" type="button"
className="commonButton right" className="commonButton right"
onClick={this.descriptionSave} onClick={descriptionSave}
> >
Save Save
</button> </button>
@@ -160,77 +213,51 @@ export default class LabellingFlow extends Component {
} }
}; };
relabel = event => this.setState({ editLocation: true }); if (editDescriptionOnDialog) {
return (
setDescriptionUpdateMode = () => { <EditDescriptionDialog
this.descriptionInput.current.focus(); onCancel={labellingDoneCallback}
this.setState({ editDescription: true }); onUpdate={descriptionDialogUpdate}
}; measurementData={state.measurementData}
/>
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
); );
}; }
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 = () => { LabellingFlow.propTypes = {
if (!this.fadeOutTimer) { measurementData: PropTypes.object.isRequired,
return; 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 { connect } from 'react-redux';
import Viewer from "./Viewer.js"; import Viewer from './Viewer.js';
import OHIF from "@ohif/core"; import OHIF from '@ohif/core';
const { setTimepoints, setMeasurements } = OHIF.redux.actions; 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 { return {
viewports: state.viewports.viewportSpecificData, viewports: viewports.viewportSpecificData,
activeViewportIndex: state.viewports.activeViewportIndex activeViewportIndex: viewports.activeViewportIndex,
activeServer: getActiveServer(servers),
}; };
}; };
@@ -18,7 +25,7 @@ const mapDispatchToProps = dispatch => {
}, },
onMeasurementsUpdated: measurements => { onMeasurementsUpdated: measurements => {
dispatch(setMeasurements(measurements)); dispatch(setMeasurements(measurements));
} },
}; };
}; };
@@ -11,6 +11,7 @@ import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js'; import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js'; import SidePanel from './../components/SidePanel.js';
import { extensionManager } from './../App.js'; import { extensionManager } from './../App.js';
import DICOMSR from '../lib/DICOMSR';
// Contexts // Contexts
import WhiteLabellingContext from '../context/WhiteLabellingContext.js'; import WhiteLabellingContext from '../context/WhiteLabellingContext.js';
@@ -57,21 +58,31 @@ class Viewer extends Component {
static propTypes = { static propTypes = {
studies: PropTypes.array, studies: PropTypes.array,
studyInstanceUids: PropTypes.array, studyInstanceUids: PropTypes.array,
activeServer: PropTypes.shape({
type: PropTypes.string,
wadoRoot: PropTypes.string,
}),
onTimepointsUpdated: PropTypes.func, onTimepointsUpdated: PropTypes.func,
onMeasurementsUpdated: PropTypes.func, onMeasurementsUpdated: PropTypes.func,
// window.store.getState().viewports.viewportSpecificData // window.store.getState().viewports.viewportSpecificData
viewports: PropTypes.object.isRequired, viewports: PropTypes.object.isRequired,
// window.store.getState().viewports.activeViewportIndex // window.store.getState().viewports.activeViewportIndex
activeViewportIndex: PropTypes.number.isRequired, activeViewportIndex: PropTypes.number.isRequired,
isStudyLoaded: PropTypes.bool,
}; };
constructor(props) { constructor(props) {
super(props); super(props);
const { activeServer } = this.props;
const server = Object.assign({}, activeServer);
OHIF.measurements.MeasurementApi.setConfiguration({ OHIF.measurements.MeasurementApi.setConfiguration({
dataExchange: { dataExchange: {
retrieve: this.retrieveMeasurements, retrieve: DICOMSR.retrieveMeasurements,
store: this.storeMeasurements, store: DICOMSR.storeMeasurements,
}, },
server,
}); });
OHIF.measurements.TimepointApi.setConfiguration({ OHIF.measurements.TimepointApi.setConfiguration({
@@ -93,18 +104,6 @@ class Viewer extends Component {
thumbnails: [], 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 => { retrieveTimepoints = filter => {
OHIF.log.info('retrieveTimepoints'); OHIF.log.info('retrieveTimepoints');
@@ -171,7 +170,7 @@ class Viewer extends Component {
}; };
componentDidMount() { componentDidMount() {
const { studies } = this.props; const { studies, isStudyLoaded } = this.props;
const { TimepointApi, MeasurementApi } = OHIF.measurements; const { TimepointApi, MeasurementApi } = OHIF.measurements;
const currentTimepointId = 'TimepointId'; const currentTimepointId = 'TimepointId';
@@ -191,8 +190,11 @@ class Viewer extends Component {
const patientId = studies[0] && studies[0].patientId; const patientId = studies[0] && studies[0].patientId;
timepointApi.retrieveTimepoints({ patientId }); timepointApi.retrieveTimepoints({ patientId });
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]); if (isStudyLoaded) {
this.measurementApi.retrieveMeasurements(patientId, [
currentTimepointId,
]);
}
this.setState({ this.setState({
thumbnails: _mapStudiesToThumbnails(studies), thumbnails: _mapStudiesToThumbnails(studies),
}); });
@@ -200,18 +202,19 @@ class Viewer extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.studies !== prevProps.studies) { const { studies, isStudyLoaded } = this.props;
const { studies } = this.props; if (studies !== prevProps.studies) {
const patientId = studies[0] && studies[0].patientId;
const currentTimepointId = this.currentTimepointId;
this.timepointApi.retrieveTimepoints({ patientId });
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
this.setState({ this.setState({
thumbnails: _mapStudiesToThumbnails(studies), 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() { render() {
@@ -294,11 +297,11 @@ class Viewer extends Component {
activeIndex={this.props.activeViewportIndex} activeIndex={this.props.activeViewportIndex}
/> />
) : ( ) : (
<ConnectedStudyBrowser <ConnectedStudyBrowser
studies={this.state.thumbnails} studies={this.state.thumbnails}
studyMetadata={this.props.studies} studyMetadata={this.props.studies}
/> />
)} )}
</SidePanel> </SidePanel>
{/* MAIN */} {/* MAIN */}
@@ -334,7 +337,7 @@ export default Viewer;
* @param {Study[]} studies * @param {Study[]} studies
* @param {DisplaySet[]} studies[].displaySets * @param {DisplaySet[]} studies[].displaySets
*/ */
const _mapStudiesToThumbnails = function(studies) { const _mapStudiesToThumbnails = function (studies) {
return studies.map(study => { return studies.map(study => {
const { studyInstanceUid } = study; const { studyInstanceUid } = study;
@@ -178,6 +178,7 @@ function ViewerRetrieveStudyData({
// hooks // hooks
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [studies, setStudies] = useState([]); const [studies, setStudies] = useState([]);
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
const snackbarContext = useSnackbarContext(); const snackbarContext = useSnackbarContext();
const { appConfig = {} } = useContext(AppContext); const { appConfig = {} } = useContext(AppContext);
const { filterQueryParam: isFilterStrategy = false } = appConfig; const { filterQueryParam: isFilterStrategy = false } = appConfig;
@@ -218,6 +219,7 @@ function ViewerRetrieveStudyData({
); );
setStudies([...studies, study]); setStudies([...studies, study]);
setIsStudyLoaded(true);
}; };
/** /**
@@ -332,7 +334,11 @@ function ViewerRetrieveStudyData({
} }
return ( 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;
+9 -9
View File
@@ -876,7 +876,7 @@
core-js "^2.6.5" core-js "^2.6.5"
regenerator-runtime "^0.13.2" 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" version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc" resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc"
integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw== integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw==
@@ -6535,14 +6535,14 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dcmjs@^0.6.1: dcmjs@^0.8.2:
version "0.6.1" version "0.8.2"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.6.1.tgz#453f764bca2cd8e3430126ba8385ef4c77071bcd" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.8.2.tgz#e1107273764de0a3052244b79d999fd870e58650"
integrity sha512-sJ0cRVZY02GSaAW5S9JPcv8+7iDtgrM0L/niYWAvu2piSa4bFSXwKi+t4XevAfkm+mF0sXZtaD15bw+/oiCYkw== integrity sha512-HEd1Rq12qxZXVXr5B4xgl/foCWaXxioO5v42FLgiUjKrbQR6iKIVcZsmE+3/AvAurcHiQFfW5vk5K4AY7eAp5A==
dependencies: dependencies:
"@babel/polyfill" "^7.2.5" "@babel/polyfill" "^7.6.0"
"@babel/runtime" "^7.3.1" "@babel/runtime" "^7.6.3"
loglevelnext "^3.0.0" loglevelnext "^3.0.1"
ndarray "^1.0.18" 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: 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" es6-symbol "^3.1.1"
object.assign "^4.1.0" object.assign "^4.1.0"
loglevelnext@^3.0.0: loglevelnext@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04" resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04"
integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA== integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA==