Compare commits

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

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

* chore: revert to remote JSON file

* chore: delete PTCTStudy.json file

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

* CI fix

* Fixes for PR comments

* Small change just to check CI behavior

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

* Fixed Download button test and blocked Download Dialog event

* Deleting file ConnectedViewportDownloadForm.js

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

* Removing wrong import line

* Fixes based on PR comments

* Fixes based on PR comments

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

Closes: #758

* feat: 🎸 Adding the dataExchance from DICOM SR

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

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

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

* feat: 🎸Small fixes into the code

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

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

This reverts commit 4b3a1efb5810487d2319826fd59c03d779882b5e.

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

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

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

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

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

* Update DCMJS into latest dev version

* Update dcmjs with latest fixes

* Fix PR callouts

* Logging error on MeasurementReport creation

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

* Small error message update

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

* Renaming variable

* Expose dcmjs tool support check

* Change the way we warn the unsupportedTools

* Adding style to the button

* Small change on button style

* Fix git conflicts

* Fix after merge with latest master

* Fix after merge with master, btn was not clickable

* Fixing callouts

* Adding util functions into files and refactor with JSDocs

* Refactoring code, adding some basic JSDocs

* Bump dcmjs with latest version and fixes

* JSDocs and small refactors

* yarn lock file after dcmjs update

* Remove unused parameters

* Registering a plain copy of server into measurementAPI

* Small refactor

* Start changing from toolType to toolName

* Rename a function to get instanceMetadata

* Adding error if server is not DICOMWeb on retrieve

* Move snackbar outside of UI component and use UINotificationService

* Refactor and JSDocs

* Small fixes on JSDocs (style)

* Remove unused import

* Improve JSDocs on MeasurementApi

* Remove refactor leftover

* Improve JSDocs and propTypes

* JSDocs and small refactors
2019-12-11 11:14:01 -05:00
ohif-bot 5cda5159da chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.2
 - @ohif/viewer@3.0.3
2019-12-11 15:41:29 +00:00
Danny Brown 115a3ec0b0 refactor: remove-labelling-manager
Igor/refactor/1267 remove labelling manager
2019-12-11 10:39:36 -05:00
Danny Brown ac82a111e7 Merge branch 'master' into igor/refactor/1267-remove-labelling-manager 2019-12-11 09:16:18 -05:00
Igor a22fd67e13 Fix config check in extension init 2019-12-11 06:54:41 -03:00
Danny Brown 91f0af058f Merge pull request #1278 from OHIF/dannyrb/ci/visual-test
ci: make sure we're updating the base branch for comparisons (percy)
2019-12-10 23:58:53 -05:00
dannyrb 5a1186388d Merge branch 'dannyrb/ci/visual-test' of github.com:OHIF/Viewers into dannyrb/ci/visual-test 2019-12-10 23:52:54 -05:00
dannyrb 2f0f92c095 ci: make sure we're updating the base branch for comparisons (percy) 2019-12-10 23:45:29 -05:00
Danny Brown 5c686268ab Merge pull request #1274 from OHIF/refactor/igor/1267-remove-labelling-manager
refactor: 💡 Remove LabellingManager and Cleanup LabellingFlow #1267
2019-12-10 23:24:14 -05:00
Danny Brown 974aabc9b2 Merge pull request #1276 from OHIF/dannyrb/ci/visual-test
ci: optional visual test step
2019-12-10 23:12:30 -05:00
Danny Brown 17e3b49b06 Merge branch 'master' into dannyrb/ci/visual-test 2019-12-10 23:08:09 -05:00
dannyrb e3c119489a cI: Better job name for percy 2019-12-10 23:07:54 -05:00
ohif-bot 17447baf44 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.0.2
 - @ohif/core@2.0.2
 - @ohif/viewer@3.0.2
2019-12-11 03:31:18 +00:00
Igor Octaviano daa3ddccdb refactor: 💡 ServicesManager factory functions vs instances (#1272)
* refactor: 💡 ServicesManager factory functions vs instances

Refactor servicesManager to take factory functions instead of instances

Closes: #1265

* Remove testing code
2019-12-10 22:28:23 -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
Danny Brown 29bac8df7a chore: don't mark CI/Tooling tasks as stale 2019-12-10 11:37:39 -05:00
Danny Brown 319c67c389 docs: swap out "always red" badge for viewer-dev.ohif.org badge 2019-12-10 08:57:03 -05:00
Danny Brown b9836f6c71 docs: templates in P_R_T directory can only be set w/ query string; removing 2019-12-09 22:06:01 -05:00
Danny Brown 6a6e2d7df3 docs: shift pr template up a dir 2019-12-09 22:05:05 -05:00
Danny Brown 2f8c408ed2 docs: contributing guidelines 2019-12-09 22:02:29 -05:00
Danny Brown 27910239e0 docs: code of conduct 2019-12-09 22:00:17 -05:00
ohif-bot 719bfd02eb chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.1
 - @ohif/extension-vtk@1.0.1
 - @ohif/core@2.0.1
 - @ohif/ui@1.0.1
 - @ohif/viewer@3.0.1
2019-12-09 18:58:37 +00:00
dannyrb 994818faeb Merge branch 'master' of github.com:OHIF/Viewers into ups-master 2019-12-09 13:55:04 -05:00
dannyrb f06ef53e83 docs: beginnings of SOPClassHandlerModule docs 2019-12-09 13:53:35 -05:00
73 changed files with 1831 additions and 1710 deletions

No files matched your search

+46
View File
@@ -367,6 +367,31 @@ workflows:
requires:
- UNIT_TESTS
PR_OPTIONAL_VISUAL_TESTS:
jobs:
- AWAIT_APPROVAL:
type: approval
# Update hub.docker.org
- cypress/run:
name: 'Generate Percy Snapshots'
executor: cypress/browsers-chrome76
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:
jobs:
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
@@ -419,6 +444,27 @@ workflows:
filters:
branches:
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:
requires:
- NPM_PUBLISH
+1 -1
View File
@@ -9,7 +9,7 @@ daysUntilClose: 7
exemptLabels:
- 'Story :raised_hands:'
- 'Bug: Verified :bug:'
- 'Task: CI/Tooling 🤖'
- 'Task: CI/Tooling :robot:'
- 'Task: Docs 📖'
- 'Task: Docs :book:'
- 'Task: Refactor 🛠'
+76
View File
@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at danny.ri.brown+OHIFcoc@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+1
View File
@@ -0,0 +1 @@
See our contributing guidelines at [`https://docs.ohif.org`](https://docs.ohif.org/development/contributing.html)
+2 -2
View File
@@ -271,8 +271,8 @@ MIT © [OHIF](https://github.com/OHIF)
<!-- Badges -->
[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
[lerna-url]: https://lerna.js.org/
[netlify-image]: https://api.netlify.com/api/v1/badges/a5d369ab-18a6-41c3-bcde-83805205ac7f/deploy-status
[netlify-url]: https://app.netlify.com/sites/ohif/deploys
[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status
[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys
[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square
[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg
[circleci-url]: https://circleci.com/gh/OHIF/Viewers
@@ -1,6 +1,37 @@
# Module: SOP Class Handler
...
An extension can register a SOP Class Handler Module by defining a
`getSopClassHandlerModule` method. The SOP Class Handler is a bit different from
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
own components. It instead defines:
- `sopClassUids`: an array of string SOP Class UIDs that the
`getDisplaySetFromSeries` method should be applied to.
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
display set
A `displaySet` has the following shape:
```js
return {
plugin: 'html',
modality: 'SR',
displaySetInstanceUid: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders,
};
```
Where the `plugin` key is used to influence the default `ViewportComponent` for
rendering the `displaySet`. Additional properties are passed to the
`ViewportComponent` and used by the default `StudyBrowser` to render
"thumbnails" for each `displaySet`
## Example SOP Class Handler Module
```js
const SOP_CLASS_UIDS = {
@@ -8,29 +39,52 @@ const SOP_CLASS_UIDS = {
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
};
const sopClassHandlerModule = {
id: 'OHIFDicomHtmlSopClassHandler',
sopClassUids: Object.values(SOP_CLASS_UIDS),
export default {
id: 'example-sop-class-handler-module',
/**
* @param {object} series -
* @param {object} study -
* @param {object} dicomWebClient -
* @param {object} authorizationHeaders -
*/
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getSopClassHandlerModule({ servicesManager, commandsManager }) {
return {
id: 'OHIFDicomHtmlSopClassHandler',
sopClassUids: Object.values(SOP_CLASS_UIDS),
return {
plugin: 'html',
displaySetInstanceUid: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders,
};
},
/**
* @param {object} series -
* @param {object} study -
* @param {object} dicomWebClient -
* @param {object} authorizationHeaders -
*/
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
return {
plugin: 'html',
displaySetInstanceUid: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders,
};
},
}
};
```
## `@ohif/viewer` usage
We use the `sopClassHandlerModule`s in three different places:
- `ViewerLocalFileData.js`
- `ViewerRetrieveStudyData.js`
- `StandaloneRouting.js`
Each time, it is used to map study and series data to `displaySets`. It does
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
manager has the method `createDisplaySets` that takes an array of
`sopClassHandlerModules`.
+1 -1
View File
@@ -43,7 +43,7 @@ translate to in practice?
```js
// In the application
const UINotificationService = createUINotificationService();
import UINotificationService from '@ohif/core';
const servicesManager = new ServicesManager();
servicesManager.registerService(UINotificationService);
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.0.0",
"version": "2.1.0",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -34,7 +34,7 @@
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
+20 -14
View File
@@ -102,7 +102,7 @@ export default function init({ servicesManager, configuration }) {
];
/* Add extension tools configuration here. */
const extensionToolsConfiguration = {
const internalToolsConfig = {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
@@ -113,20 +113,26 @@ export default function init({ servicesManager, configuration }) {
},
};
const isEmpty = obj => Object.keys(obj).length < 1;
if (!isEmpty(configuration.tools) || !isEmpty(extensionToolsConfiguration)) {
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const configurationToolProps = configuration.tools[toolName] || {};
const extensionToolProps = extensionToolsConfiguration[toolName];
let props = merge(extensionToolProps, configurationToolProps);
csTools.addTool(tool, props);
});
} else {
tools.forEach(tool => csTools.addTool(tool));
}
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const externalToolsConfig = configuration.tools || {};
const externalToolProps = externalToolsConfig[toolName] || {};
const internalToolProps = internalToolsConfig[toolName] || {};
const props = merge(internalToolProps, externalToolProps);
csTools.addTool(tool, props);
});
// TODO -> We need a better way to do this with maybe global tool state setting all tools passive.
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
tools.forEach(tool => {
if (tool.prototype instanceof BaseAnnotationTool) {
// BaseAnnotationTool would likely come from csTools lib exports
const toolName = new tool().name;
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-html
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-html",
"version": "1.0.2",
"version": "1.1.0",
"description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.1...@ohif/extension-vtk@1.1.2) (2019-12-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.0...@ohif/extension-vtk@1.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.2...@ohif/extension-vtk@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.0...@ohif/extension-vtk@1.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.6...@ohif/extension-vtk@1.0.0) (2019-12-09)
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.0.0",
"version": "1.1.2",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -34,7 +34,7 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
@@ -52,11 +52,10 @@
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^2.0.0",
"@ohif/ui": "^1.0.0",
"@ohif/core": "^2.1.0",
"@ohif/ui": "^1.1.2",
"cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1",
"i18next": "^17.0.3",
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.2...@ohif/core@2.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/core
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.0...@ohif/core@2.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/core
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.3...@ohif/core@2.0.0) (2019-12-09)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.0.0",
"version": "2.1.0",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
+15 -15
View File
@@ -20,11 +20,11 @@ import user from './user.js';
import utils from './utils/';
import {
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
} from './services';
const OHIF = {
@@ -52,11 +52,11 @@ const OHIF = {
measurements,
hangingProtocols,
//
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
};
export {
@@ -83,11 +83,11 @@ export {
measurements,
hangingProtocols,
//
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
};
export { OHIF };
+5 -5
View File
@@ -10,11 +10,11 @@ describe('Top level exports', () => {
'HotkeysManager',
'ServicesManager',
//
'createUINotificationService',
'createUIModalService',
'createUIDialogService',
'createUIContextMenuService',
'createUILabellingFlowService',
'UINotificationService',
'UIModalService',
'UIDialogService',
'UIContextMenuService',
'UILabellingFlowService',
//
'utils',
'studies',
@@ -1,4 +1,5 @@
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import log from '../../log';
import getLabel from '../lib/getLabel';
import getDescription from '../lib/getDescription';
@@ -7,13 +8,32 @@ import guid from '../../utils/guid';
import studyMetadataManager from '../../utils/studyMetadataManager';
import { measurementApiDefaultConfig } from './../configuration.js';
const configuration = {
...measurementApiDefaultConfig,
...measurementApiDefaultConfig
};
export default class MeasurementApi {
static Instance;
/**
* Set configuration: It should merge default configuration with any new one
*
* @static
* @param {Object} config
* @param {Object} config.server
* @param {string} config.server.type - The server type
* @param {string} config.server.wadoRoot - The server wado URL root
* @param {Array} config.measurementTools
* @param {string} config.measurementTools[].id - The tool group id
* @param {string} config.measurementTools[].name - The tool group name
* @param {Array} config.measurementTools[].childTools - The child tool's configuration
* @param {Object} config.dataExchange
* @param {Function} config.dataExchange.store - Function that store measurement data
* @param {Function} config.dataExchange.retrieve - Function that retrieves measurement data
*
* @memberof MeasurementApi
*/
static setConfiguration(config) {
Object.assign(configuration, config);
}
@@ -203,13 +223,14 @@ export default class MeasurementApi {
retrieveMeasurements(patientId, timepointIds) {
const retrievalFn = configuration.dataExchange.retrieve;
const { server } = configuration;
if (typeof retrievalFn !== 'function') {
log.error('Measurement retrieval function has not been configured.');
return;
}
return new Promise((resolve, reject) => {
retrievalFn(patientId, timepointIds).then(measurementData => {
retrievalFn(server).then(measurementData => {
if (measurementData) {
log.info('Measurement data retrieval');
log.info(measurementData);
@@ -230,6 +251,10 @@ export default class MeasurementApi {
// Synchronize the new tool data
this.syncMeasurementsAndToolData();
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
// Let others know that the measurements are updated
this.onMeasurementsUpdated();
}, reject);
@@ -237,6 +262,7 @@ export default class MeasurementApi {
}
storeMeasurements(timepointId) {
const { server } = configuration;
const storeFn = configuration.dataExchange.store;
if (typeof storeFn !== 'function') {
log.error('Measurement store function has not been configured.');
@@ -278,8 +304,9 @@ export default class MeasurementApi {
};
log.info('Saving Measurements for timepoints:', timepoints);
return storeFn(measurementData, filter).then(() => {
return storeFn(measurementData, filter, server).then(result => {
log.info('Measurement storage completed');
return result;
});
}
+26 -11
View File
@@ -7,10 +7,12 @@ export default class ServicesManager {
}
/**
* Registers a new service.
*
* @param {Object} service
* @param {Object} configuration
*/
registerService(service) {
registerService(service, configuration = {}) {
if (!service) {
log.warn(
'Attempting to register a null/undefined service. Exiting early.'
@@ -18,32 +20,45 @@ export default class ServicesManager {
return;
}
let serviceName = service.name;
if (!serviceName) {
if (!service.name) {
log.warn(`Service name not set. Exiting early.`);
return;
}
if (this.registeredServiceNames.includes(serviceName)) {
if (this.registeredServiceNames.includes(service.name)) {
log.warn(
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
`Service name ${service.name} has already been registered. Exiting before duplicating services.`
);
return;
}
this.services[service.name] = service;
if (service.create) {
this.services[service.name] = service.create({ configuration });
} else {
log.warn(`Service create factory function not defined. Exiting early.`);
return;
}
// Track service registration
this.registeredServiceNames.push(serviceName);
/* Track service registration */
this.registeredServiceNames.push(service.name);
}
/**
* An array of services.
* An array of services, or an array of arrays that contains service
* configuration pairs.
*
* @param {Object[]} services - Array of services
*/
registerServices(services) {
services.forEach(service => this.registerService(service));
services.forEach(service => {
const hasConfiguration = Array.isArray(service);
if (hasConfiguration) {
const [ohifService, configuration] = service;
this.registerService(ohifService, configuration);
} else {
this.registerService(service);
}
});
}
}
@@ -16,18 +16,32 @@ describe('ServicesManager.js', () => {
it('calls registerService() for each service', () => {
servicesManager.registerService = jest.fn();
const fakeServices = [
{ name: 'UINotificationTestService', hide: jest.fn() },
{ name: 'UIModalTestService', hide: jest.fn() },
];
servicesManager.registerServices(fakeServices);
servicesManager.registerServices([
{ name: 'UINotificationTestService', create: jest.fn() },
{ name: 'UIModalTestService', create: jest.fn() },
]);
expect(servicesManager.registerService.mock.calls.length).toBe(2);
});
it('calls registerService() for each service passing its configuration if tuple', () => {
servicesManager.registerService = jest.fn();
const fakeConfiguration = { testing: true };
servicesManager.registerServices([
{ name: 'UINotificationTestService', create: jest.fn() },
[{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration],
]);
expect(servicesManager.registerService.mock.calls[1]).toContain(
fakeConfiguration
);
});
});
describe('registerService()', () => {
const fakeService = { name: 'UINotificationService', create: jest.fn() };
it('logs a warning if the service is null or undefined', () => {
const undefinedService = undefined;
const nullService = null;
@@ -39,8 +53,8 @@ describe('ServicesManager.js', () => {
});
it('logs a warning if the service does not have a name', () => {
const serviceWithEmptyName = { name: '', hide: jest.fn() };
const serviceWithoutName = { hide: jest.fn() };
const serviceWithEmptyName = { name: '', create: jest.fn() };
const serviceWithoutName = { create: jest.fn() };
servicesManager.registerService(serviceWithEmptyName);
servicesManager.registerService(serviceWithoutName);
@@ -48,23 +62,37 @@ describe('ServicesManager.js', () => {
expect(log.warn.mock.calls.length).toBe(2);
});
it('tracks which services have been registered', () => {
const service = {
name: 'UINotificationService',
};
it('logs a warning if the service does not have a create factory function', () => {
const serviceWithoutCreate = { name: 'UINotificationService' };
servicesManager.registerService(service);
expect(servicesManager.registeredServiceNames).toContain(service.name);
});
it('logs a warning if the service has an name that has already been registered', () => {
const service = { name: 'UINotificationService' };
servicesManager.registerService(service);
servicesManager.registerService(service);
servicesManager.registerService(serviceWithoutCreate);
expect(log.warn.mock.calls.length).toBe(1);
});
it('tracks which services have been registered', () => {
servicesManager.registerService(fakeService);
expect(servicesManager.registeredServiceNames).toContain(
fakeService.name
);
});
it('logs a warning if the service has an name that has already been registered', () => {
servicesManager.registerService(fakeService);
servicesManager.registerService(fakeService);
expect(log.warn.mock.calls.length).toBe(1);
});
it('pass configuration to service create factory function', () => {
const configuration = { config: 'Some configuration' };
servicesManager.registerService(fakeService, configuration);
expect(fakeService.create.mock.calls[0][0]).toEqual({
configuration,
});
});
});
});
@@ -5,29 +5,27 @@
* @property {Event} event The event with tool information.
*/
const uiContextMenuServicePublicAPI = {
name: 'UIContextMenuService',
hide,
show,
const name = 'UIContextMenuService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const uiContextMenuServiceImplementation = {
const serviceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
};
function createUIContextMenuService() {
return uiContextMenuServicePublicAPI;
}
/**
* Show a new UI ContextMenu dialog;
*
* @param {ContextMenuProps} props { event }
*/
function show({ event }) {
return uiContextMenuServiceImplementation._show({
function _show({ event }) {
return serviceImplementation._show({
event,
});
}
@@ -36,8 +34,8 @@ function show({ event }) {
* Hide a UI ContextMenu dialog;
*
*/
function hide() {
return uiContextMenuServiceImplementation._hide();
function _hide() {
return serviceImplementation._hide();
}
/**
@@ -53,11 +51,16 @@ function setServiceImplementation({
hide: hideImplementation,
}) {
if (showImplementation) {
uiContextMenuServiceImplementation._show = showImplementation;
serviceImplementation._show = showImplementation;
}
if (hideImplementation) {
uiContextMenuServiceImplementation._hide = hideImplementation;
serviceImplementation._hide = hideImplementation;
}
}
export default createUIContextMenuService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -25,30 +25,28 @@
* @property {Function} onDrag Called while dragging.
*/
const uiDialogServicePublicAPI = {
name: 'UIDialogService',
dismiss,
dismissAll,
create,
const name = 'UIDialogService';
const publicAPI = {
name,
dismiss: _dismiss,
dismissAll: _dismissAll,
create: _create,
setServiceImplementation,
};
const uiDialogServiceImplementation = {
const serviceImplementation = {
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
_create: () => console.warn('create() NOT IMPLEMENTED'),
};
function createUIDialogService() {
return uiDialogServicePublicAPI;
}
/**
* Show a new UI dialog;
*
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, centralize, isDraggable, showOverlay, preservePosition, defaultPosition }
*/
function create({
function _create({
id,
content,
contentProps,
@@ -61,7 +59,7 @@ function create({
showOverlay = false,
defaultPosition,
}) {
return uiDialogServiceImplementation._create({
return serviceImplementation._create({
id,
content,
contentProps,
@@ -81,8 +79,8 @@ function create({
*
* @returns void
*/
function dismissAll() {
return uiDialogServiceImplementation._dismissAll();
function _dismissAll() {
return serviceImplementation._dismissAll();
}
/**
@@ -90,8 +88,8 @@ function dismissAll() {
*
* @returns void
*/
function dismiss({ id }) {
return uiDialogServiceImplementation._dismiss({ id });
function _dismiss({ id }) {
return serviceImplementation._dismiss({ id });
}
/**
@@ -109,14 +107,19 @@ function setServiceImplementation({
create: createImplementation,
}) {
if (dismissImplementation) {
uiDialogServiceImplementation._dismiss = dismissImplementation;
serviceImplementation._dismiss = dismissImplementation;
}
if (dismissAllImplementation) {
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
serviceImplementation._dismissAll = dismissAllImplementation;
}
if (createImplementation) {
uiDialogServiceImplementation._create = createImplementation;
serviceImplementation._create = createImplementation;
}
}
export default createUIDialogService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -8,28 +8,26 @@
*
*/
const uiLabellingFlowServicePublicAPI = {
name: 'UILabellingFlowService',
show,
hide,
const name = 'UILabellingFlowService';
const publicAPI = {
name,
show: _show,
hide: _hide,
setServiceImplementation,
};
const uiLabellingFlowServiceImplementation = {
const serviceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
};
function createUILabellingFlowService() {
return uiLabellingFlowServicePublicAPI;
}
/**
* Hide a UI LabellingFlow dialog;
*
*/
function hide() {
return uiLabellingFlowServiceImplementation._hide();
function _hide() {
return serviceImplementation._hide();
}
/**
@@ -37,8 +35,8 @@ function hide() {
*
* @param {LabellingFlowProps} props { defaultPosition, centralize, props }
*/
function show({ defaultPosition, centralize, props }) {
return uiLabellingFlowServiceImplementation._show({
function _show({ defaultPosition, centralize, props }) {
return serviceImplementation._show({
defaultPosition,
centralize,
props,
@@ -58,11 +56,16 @@ function setServiceImplementation({
hide: hideImplementation,
}) {
if (showImplementation) {
uiLabellingFlowServiceImplementation._show = showImplementation;
serviceImplementation._show = showImplementation;
}
if (hideImplementation) {
uiLabellingFlowServiceImplementation._hide = hideImplementation;
serviceImplementation._hide = hideImplementation;
}
}
export default createUILabellingFlowService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -11,28 +11,26 @@
* @property {string} [customClassName=null] The custom class to style the modal.
*/
const uiModalServicePublicAPI = {
name: 'UIModalService',
hide,
show,
const name = 'UIModalService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const uiModalServiceImplementation = {
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUIModalService() {
return uiModalServicePublicAPI;
}
/**
* Show a new UI modal;
*
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
*/
function show({
function _show({
content = null,
contentProps = null,
shouldCloseOnEsc = false,
@@ -41,7 +39,7 @@ function show({
title = null,
customClassName = null,
}) {
return uiModalServiceImplementation._show({
return serviceImplementation._show({
content,
contentProps,
shouldCloseOnEsc,
@@ -57,8 +55,8 @@ function show({
*
* @returns void
*/
function hide() {
return uiModalServiceImplementation._hide();
function _hide() {
return serviceImplementation._hide();
}
/**
@@ -74,11 +72,16 @@ function setServiceImplementation({
show: showImplementation,
}) {
if (hideImplementation) {
uiModalServiceImplementation._hide = hideImplementation;
serviceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiModalServiceImplementation._show = showImplementation;
serviceImplementation._show = showImplementation;
}
}
export default createUIModalService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -10,22 +10,20 @@
* @property {boolean} [autoClose=true]
*/
const uiNotificationServicePublicAPI = {
name: 'UINotificationService',
hide,
show,
const name = 'UINotificationService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const uiNotificationServiceImplementation = {
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUINotificationService() {
return uiNotificationServicePublicAPI;
}
/**
* Create and show a new UI notification; returns the
* ID of the created notification.
@@ -33,7 +31,7 @@ function createUINotificationService() {
* @param {Notification} notification { title, message, duration, position, type, autoClose}
* @returns {number} id
*/
function show({
function _show({
title,
message,
duration = 5000,
@@ -41,7 +39,7 @@ function show({
type = 'info',
autoClose = true,
}) {
return uiNotificationServiceImplementation._show({
return serviceImplementation._show({
title,
message,
duration,
@@ -57,8 +55,8 @@ function show({
* @param {number} id - id of the notification to hide/dismiss
* @returns undefined
*/
function hide(id) {
return uiNotificationServiceImplementation._hide({ id });
function _hide(id) {
return serviceImplementation._hide({ id });
}
/**
@@ -74,11 +72,16 @@ function setServiceImplementation({
show: showImplementation,
}) {
if (hideImplementation) {
uiNotificationServiceImplementation._hide = hideImplementation;
serviceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiNotificationServiceImplementation._show = showImplementation;
serviceImplementation._show = showImplementation;
}
}
export default createUINotificationService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
+10 -10
View File
@@ -1,15 +1,15 @@
import ServicesManager from './ServicesManager.js';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
import createUIDialogService from './UIDialogService';
import createUIContextMenuService from './UIContextMenuService';
import createUILabellingFlowService from './UILabellingFlowService';
import UINotificationService from './UINotificationService';
import UIModalService from './UIModalService';
import UIDialogService from './UIDialogService';
import UIContextMenuService from './UIContextMenuService';
import UILabellingFlowService from './UILabellingFlowService';
export {
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
ServicesManager,
};
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.2...@ohif/i18n@0.52.3) (2019-12-12)
### Bug Fixes
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.52.2",
"version": "0.52.3",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
@@ -10,6 +10,7 @@
"Clear": "Clear",
"Coronal": "Coronal",
"Crosshairs": "Crosshairs",
"Download": "Download",
"Ellipse": "Ellipse",
"Elliptical": "Elliptical",
"Flip H": "Flip H",
+10 -9
View File
@@ -10,18 +10,19 @@
"Clear": "Limpiar",
"Coronal": "Coronal",
"Crosshairs": "Punto de mira",
"Download": "Descargar",
"Ellipse": "Elipse",
"Elliptical": "Elíptico",
"Flip H": "Espejo H",
"Flip V": "Espejo V",
"Flip H": "Voltear H",
"Flip V": "Voltear V",
"Freehand": "Mano alzada",
"Invert": "Invertir",
"Invert": "Negativo",
"Layout": "$t(Common:Layout)",
"Length": "Longitud",
"Levels": "Niveles",
"Magnify": "Aumentar",
"Levels": "W/L",
"Magnify": "Lupa",
"Manual": "Manual",
"Measurements": "Mediciones",
"Measurements": "Medidas",
"More": "$t(Common:More)",
"Next": "$t(Common:Next)",
"Pan": "Mover",
@@ -32,11 +33,11 @@
"Rectangle": "Rectángulo",
"Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:Reset) por defecto",
"Rotate Right": "Rotar ->",
"Rotate Right": "Girar ->",
"Sagittal": "Sagital",
"Save": "Guardar",
"Stack Scroll": "Scroll en pila",
"Stack Scroll": "Scroll",
"Stop": "$t(Common:Stop)",
"Themes": "Temas",
"Zoom": "Zoom"
"Zoom": "Ampliar"
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"Image": "Imagen",
"Layout": "Disposición",
"Layout": "Formato",
"Measurements": "Medidas",
"More": "Más",
"Next": "Siguiente",
"Play": "Play",
"Previous": "Anterior",
"Reset": "Resetear",
"Reset": "Restaurar",
"RowsPerPage": "filas por página",
"Series": "Secuencia",
"Show": "Mostrar",
+3 -2
View File
@@ -10,6 +10,7 @@
"Clear": "Limpar",
"Coronal": "Coronal",
"Crosshairs": "Localizador",
"Download": "Baixar",
"Ellipse": "Elipse",
"Elliptical": "Elíptico",
"Flip H": "Inverter H",
@@ -30,8 +31,8 @@
"Probe": "Prova",
"ROI Window": "Janela ROI",
"Rectangle": "Retângulo",
"Reset": "Resetar",
"Reset to Defaults": "Restaurar para o Padrão",
"Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:Reset) para o Padrão",
"Rotate Right": "Girar à direita",
"Sagittal": "Sagital",
"Save": "Salvar",
+1 -1
View File
@@ -6,6 +6,6 @@
"Next": "Próximo",
"Play": "Play",
"Previous": "Anterior",
"Reset": "Reset",
"Reset": "Restaurar",
"Stop": "Stop"
}
@@ -1,6 +1,6 @@
{
"Cancel": "Cancelar",
"Reset to Defaults": "Restaurar Default",
"Reset to Defaults": "$t(Common:Reset) para Padrão",
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
"Save": "Salvar",
"SaveMessage": "Preferências salvas",
+38
View File
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
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)
**Note:** Version bump only for package @ohif/ui
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.4...@ohif/ui@1.0.0) (2019-12-09)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "1.0.0",
"version": "1.1.2",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -32,7 +32,7 @@ const ViewportDownloadForm = ({
maximumSize,
canvasClass,
}) => {
const [t] = useTranslation('ViewportDownloadForm');
const [t] = useTranslation('Buttons');
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
@@ -196,11 +196,12 @@ const ViewportDownloadForm = ({
)}
</div>
<div className="file-info-container">
<div className="file-info-container" data-cy="file-info-container">
<div className="col">
<div className="width">
<TextInput
type="number"
data-cy="image-width"
min={minimumSize}
max={maximumSize}
value={width}
@@ -211,6 +212,7 @@ const ViewportDownloadForm = ({
<div className="height">
<TextInput
type="number"
data-cy="image-height"
min={minimumSize}
max={maximumSize}
value={height}
@@ -224,6 +226,7 @@ const ViewportDownloadForm = ({
<div className="file-name">
<TextInput
type="text"
data-cy="file-name"
value={filename}
onChange={event => setFilename(event.target.value)}
label={t('File name')}
@@ -233,6 +236,7 @@ const ViewportDownloadForm = ({
<div className="file-type">
<Select
value={fileType}
data-cy="file-type"
onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS}
label={t('File type')}
@@ -245,6 +249,7 @@ const ViewportDownloadForm = ({
<label htmlFor="show-annotations" className="form-check-label">
<input
id="show-annotations"
data-cy="show-annotations"
type="checkbox"
className="form-check-input"
checked={showAnnotations}
@@ -278,23 +283,33 @@ const ViewportDownloadForm = ({
></canvas>
</div>
<div className="preview">
<div className="preview" data-cy="image-preview">
<h4> {t('Image Preview')}</h4>
<img
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
data-cy="viewport-preview-img"
/>
</div>
<div className="actions">
<div className="action-cancel">
<button type="button" className="btn btn-danger" onClick={onClose}>
<button
type="button"
data-cy="cancel-btn"
className="btn btn-danger"
onClick={onClose}
>
{t('Cancel')}
</button>
</div>
<div className="action-save">
<button onClick={downloadImage} className="btn btn-primary">
<button
onClick={downloadImage}
className="btn btn-primary"
data-cy="download-btn"
>
{t('Download')}
</button>
</div>
@@ -22,8 +22,9 @@ class MeasurementTable extends Component {
onDeleteClick: PropTypes.func,
onEditDescriptionClick: PropTypes.func,
selectedMeasurementNumber: PropTypes.number,
overwallWarnings: PropTypes.object,
t: PropTypes.func,
saveFunction: PropTypes.func,
onSaveComplete: PropTypes.func,
};
static defaultProps = {
@@ -38,8 +39,9 @@ class MeasurementTable extends Component {
};
render() {
const hasOverallWarnings =
this.props.overallWarnings.warningList.length > 0;
const { overallWarnings, saveFunction, t } = this.props;
const hasOverallWarnings = overallWarnings.warningList.length > 0;
return (
<div className="measurementTable">
<div className="measurementTableHeader">
@@ -55,7 +57,7 @@ class MeasurementTable extends Component {
style={{}}
>
<div className="warningTitle">
{this.props.t('Criteria nonconformities')}
{t('Criteria nonconformities')}
</div>
<div className="warningContent">
{this.getWarningContent()}
@@ -75,10 +77,42 @@ class MeasurementTable extends Component {
<ScrollableArea>
<div>{this.getMeasurementsGroups()}</div>
</ScrollableArea>
<div className="measurementTableFooter">
{saveFunction && (
<button onClick={this.saveFunction} className="saveBtn">
<Icon name="save" width="14px" height="14px" />
Save measurements
</button>
)}
</div>
</div>
);
}
saveFunction = async event => {
const { saveFunction, onSaveComplete } = this.props;
if (saveFunction) {
try {
const result = await saveFunction();
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: result.message,
type: 'success',
});
}
} catch (error) {
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: error.message,
type: 'error',
});
}
}
}
};
getMeasurementsGroups = () => {
return this.props.measurementCollection.map((measureGroup, index) => {
return (
@@ -157,7 +191,7 @@ class MeasurementTable extends Component {
};
getWarningContent = () => {
const { warningList = '' } = this.props.overwallWarnings;
const { warningList = '' } = this.props.overallWarnings;
if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => {
@@ -1,92 +1,117 @@
.measurementTable
background-color: var(--primary-background-color)
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
height: 100%
width: 100%
display: flex
flex-direction: column
.maxMeasurements
border-radius: 3px;
color: var(--primary-background-color);
font-size: 12px;
font-weight: 500;
height: 19px;
line-height: 17px;
margin-top: 22px;
margin-right: 15px;
padding: 2px 6px 0;
text-transform: uppercase;
transition: all 0.3s ease;
background-color: var(--default-color);
border-radius: 3px
color: var(--primary-background-color)
font-size: 12px
font-weight: 500
height: 19px
line-height: 17px
margin-top: 22px
margin-right: 15px
padding: 2px 6px 0
text-transform: uppercase
transition: all 0.3s ease
background-color: var(--default-color)
.measurementTableHeader
padding-left: 45px;
padding-left: 45px
.measurementTableHeaderItem
display: inline-block;
margin-top: 9px;
margin-left: 9px;
padding-left: 9px;
width: 90px;
font-size: 14px;
line-height: 20px;
border-left: 1px solid var(--text-secondary-color);
color: var(--text-primary-color);
display: inline-block
margin-top: 9px
margin-left: 9px
padding-left: 9px
width: 90px
font-size: 14px
line-height: 20px
border-left: 1px solid var(--text-secondary-color)
color: var(--text-primary-color)
.timepointLabel
color: var(--text-secondary-color);
font-size: 12px;
color: var(--text-secondary-color)
font-size: 12px
.warning-status
display: block;
position: absolute;
width: 50px;
height: 40px;
padding-left: 7px;
left: 15px;
top: 7px;
display: block
position: absolute
width: 50px
height: 40px
padding-left: 7px
left: 15px
top: 7px
.warning-border
display: block;
border: 2px solid #e29e4a;
border-radius: 36px;
padding: 6px 7px;
width: 40px;
height: 40px;
display: block
border: 2px solid #e29e4a
border-radius: 36px
padding: 6px 7px
width: 40px
height: 40px
svg
color: #e29e4a;
width: 22px;
height: 20px;
pointer-events: inherit;
color: #e29e4a
width: 22px
height: 20px
pointer-events: inherit
.measurementTableFooter
padding: 10px
color: #fff
text-align: center
z-index: 1
.saveBtn
border: 1px solid var(--ui-border-color-active)
border-radius: 25px
background-color: var(--active-color)
color: var(--ui-gray-dark)
line-height: 25px
padding: 0 20px
outline: none
cursor: pointer
&:hover, &:active
background-color: var(--ui-sky-blue)
svg
margin-right: 4px
position: relative
top: 2px
.tooltip-warning
&.in
opacity 1;
opacity 1
.tooltip-inner
padding: 0
.warningTitle
text-align: left
background-color: var(--ui-gray-darkest);
padding: 7px 15px;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
background-color: var(--ui-gray-darkest)
padding: 7px 15px
border-top-left-radius: 7px
border-top-right-radius: 7px
.warningContent
text-align center
background-color: var(--ui-gray);
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
background-color: var(--ui-gray)
border-bottom-left-radius: 7px
border-bottom-right-radius: 7px
ol
margin-top: auto;
padding-inline-start: 20px;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 25px;
text-align: left;
margin-top: auto
padding-inline-start: 20px
padding-top: 10px
padding-bottom: 10px
padding-left: 25px
text-align: left
li
padding-right: 10px;
padding-right: 10px
@@ -29,7 +29,7 @@ const OHIFModal = ({
const renderHeader = () => {
return (
title && (
<div className="OHIFModal__header">
<div className="OHIFModal__header" data-cy="modal-header">
<h4>{title}</h4>
{closeButton && (
<button data-cy="close-button" onClick={onClose}>
@@ -44,6 +44,7 @@ const OHIFModal = ({
return (
<Modal
className={classNames('OHIFModal', className)}
data-cy="modal"
shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen}
title={title}
@@ -51,7 +52,9 @@ const OHIFModal = ({
>
<>
{renderHeader()}
<div className="OHIFModal__content">{children}</div>
<div className="OHIFModal__content" data-cy="modal-content">
{children}
</div>
</>
</Modal>
);
+2
View File
@@ -62,6 +62,7 @@ import powerOff from './icons/power-off.svg';
import reset from './icons/reset.svg';
import rotate from './icons/rotate.svg';
import rotateRight from './icons/rotate-right.svg';
import saveRegular from './icons/save-regular.svg';
import search from './icons/search.svg';
import searchPlus from './icons/search-plus.svg';
import softTissue from './icons/soft-tissue.svg';
@@ -164,6 +165,7 @@ const ICONS = {
'soft-tissue': softTissue,
lung,
liver,
save: saveRegular,
};
/**
@@ -0,0 +1,11 @@
<svg
aria-hidden="true"
focusable="false"
data-icon="save"
role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<path
fill="currentColor"
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM272 80v80H144V80h128zm122 352H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h42v104c0 13.255 10.745 24 24 24h176c13.255 0 24-10.745 24-24V83.882l78.243 78.243a6 6 0 0 1 1.757 4.243V426a6 6 0 0 1-6 6zM224 232c-48.523 0-88 39.477-88 88s39.477 88 88 88 88-39.477 88-88-39.477-88-88-88zm0 128c-22.056 0-40-17.944-40-40s17.944-40 40-40 40 17.944 40 40-17.944 40-40 40z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 738 B

+59
View File
@@ -3,6 +3,65 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.1.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.2...@ohif/viewer@3.1.3) (2019-12-12)
**Note:** Version bump only for package @ohif/viewer
## [3.1.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.1...@ohif/viewer@3.1.2) (2019-12-12)
**Note:** Version bump only for package @ohif/viewer
## [3.1.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.0...@ohif/viewer@3.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
# [3.1.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.3...@ohif/viewer@3.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [3.0.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.2...@ohif/viewer@3.0.3) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
## [3.0.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.1...@ohif/viewer@3.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
## [3.0.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.0...@ohif/viewer@3.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/viewer
# [3.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.8...@ohif/viewer@3.0.0) (2019-12-09)
@@ -81,10 +81,8 @@ describe('OHIF Cornerstone Hotkeys', () => {
});
it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => {
//Click on Layout button
cy.get('@layoutBtn').click();
//Select 3 viewports
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
//Select viewport layout (3,1)
cy.setLayout(3, 1);
cy.waitViewportImageLoading();
// Press multiples hotkeys on viewport #1
@@ -0,0 +1,126 @@
describe('OHIF Download Snapshot File', () => {
before(() => {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(5);
});
beforeEach(() => {
cy.openDownloadImageModal();
});
afterEach(() => {
// Close modal
cy.get('[data-cy="close-button"]')
.scrollIntoView()
.click();
});
it('checks displayed information for Tablet experience', function() {
// Set Tablet resolution
cy.viewport(1000, 660);
// Visual comparison
cy.screenshot('Download Image Modal - Tablet experience');
cy.percyCanvasSnapshot('Download Image Modal - Tablet experience');
});
it('checks displayed information for Desktop experience', function() {
// Set Desktop resolution
cy.viewport(1750, 720);
// Visual comparison
cy.screenshot('Download Image Modal - Desktop experience');
cy.percyCanvasSnapshot('Download Image Modal - Desktop experience');
//Check if all elements are displayed
cy.get('[data-cy=modal-header]')
.as('downloadImageModal')
.should('contain.text', 'Download High Quality Image');
// Check input fields
cy.get('[data-cy=file-type]')
.select('png')
.should('have.value', 'png')
.select('jpg')
.should('have.value', 'jpg');
// Check image preview
cy.get('[data-cy="image-preview"]').should('contain.text', 'Image Preview');
cy.get('[data-cy="viewport-preview-img"]')
.should('have.attr', 'src')
.and('include', 'data:image');
// Check buttons
cy.get('[data-cy="cancel-btn"]')
.scrollIntoView()
.should('be.visible');
cy.get('[data-cy="download-btn"]')
.scrollIntoView()
.should('be.visible');
});
it('cancel changes on download modal', function() {
//Change Image Width, Filename and File Type
cy.get('[data-cy="image-width"]')
.clear()
.type('300');
cy.get('[data-cy="image-height"]') //Image Height should be the same as width
.should('have.value', '300');
cy.get('[data-cy="file-name"]')
.clear()
.type('new-filename');
cy.get('[data-cy=file-type]').select('png');
//Click on Cancel button
cy.get('[data-cy="cancel-btn"]')
.scrollIntoView()
.click();
//Check modal is closed
cy.get('[data-cy="modal"]').should('not.exist');
//Open Modal
cy.openDownloadImageModal();
//Verify default values was restored
cy.get('[data-cy="image-width"]').should('have.value', '512');
cy.get('[data-cy="file-name"]').should('have.value', 'image');
cy.get('[data-cy=file-type]').should('have.value', 'jpg');
});
// TO-DO once issue is fixed: https://github.com/OHIF/Viewers/issues/1217
// it('checks error messages for empty fields', function() {
// //Clear fields Image Width and Filename
// cy.get('[data-cy="image-width"]')
// .clear();
// cy.get('[data-cy="file-name"]').clear();
// //Click on Download button
// cy.get('[data-cy="download-btn"]')
// .scrollIntoView()
// .click();
// //Check error message
// });
it('checks if "Show Annotations" checkbox will display annotations', function() {
// Close modal that is initially opened
cy.get('[data-cy="close-button"]').click();
// Add measurements in the viewport
cy.addLengthMeasurement();
cy.addAngleMeasurement();
// Open Modal
cy.openDownloadImageModal();
// Select "Show Annotations" option
cy.get('[data-cy="show-annotations"]').check();
// Check image preview
cy.get('[data-cy="image-preview"]').scrollIntoView();
// Visual comparison
cy.screenshot('Download Image Modal - Show Annotations checked');
cy.percyCanvasSnapshot('Download Image Modal - Show Annotations checked');
//Compare classes that exists on Image Preview with Annotations and Without Annotation
cy.get('[data-cy="modal-content"]')
.find('canvas')
.should('have.class', 'magnifyTool'); //Class "MagnifyTool" exists with annotations displayed on Image preview
// Uncheck "Show Annotations" option
cy.get('[data-cy="show-annotations"]')
.uncheck()
.wait(300);
// Check that class "MagnifyTool" should not exist
cy.get('[data-cy="modal-content"]')
.find('canvas')
.should('not.have.class', 'magnifyTool');
});
});
@@ -0,0 +1,22 @@
//We excluded the tests for '**/studies/**' because the bulk/all of our other study/viewer tests use this route
describe('OHIF Routes', function() {
beforeEach(function() {
cy.openStudyList();
});
it('checks PT/CT json url study route', function() {
cy.visit('/viewer?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/JSON/PTCTStudy.json');
cy.server();
cy.route('GET', '**/PTCTStudy/**').as('getPTCTStudy');
cy.wait('@getPTCTStudy.all');
cy.get('@getPTCTStudy').should($route => {
expect($route.status).to.be.eq(200);
});
cy.screenshot();
cy.percyCanvasSnapshot('PT/CT json study route');
});
});
@@ -1,24 +0,0 @@
describe('ViewerRouting', () => {
beforeEach(() => {
cy.visit('/');
cy.contains('Study List');
cy.get(
'[data-cy="study-list-results"]> :nth-child(1) > .patientId'
).click();
});
// it('thumbnails list has more than 2 items', () => {
// cy.get('.scrollable-study-thumbnails div.thumbnail-container')
// .its('length')
// .should('be.gte', 2);
// });
// it('loads route with at least 2 thumbnails', () => {
// cy.get(
// ':nth-child(1) > .ThumbnailEntry > .p-x-1 > .ImageThumbnail > .image-thumbnail-canvas > canvas'
// ).should('be.visible');
// cy.get(
// ':nth-child(2) > .ThumbnailEntry > .p-x-1 > .ImageThumbnail > .image-thumbnail-canvas > canvas'
// ).should('be.visible');
// });
});
@@ -11,6 +11,7 @@ export function initCornerstoneToolsAliases() {
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
cy.get('.expandableToolMenu').as('moreBtn');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
cy.get('.viewport-element').as('viewport');
}
//Creating aliases for Common page elements
@@ -469,6 +469,18 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add('openDownloadImageModal', () => {
// Click on More button
cy.get('.expandableToolMenu')
.as('moreBtn')
.click();
// Click on Download button
cy.get('.tooltip-inner > :nth-child(13)')
.as('downloadBtn')
.click();
});
Cypress.Commands.add('setLanguage', (language, save = true) => {
cy.openPreferences();
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "3.0.0",
"version": "3.1.3",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -45,14 +45,14 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^2.0.0",
"@ohif/extension-cornerstone": "^2.0.0",
"@ohif/extension-dicom-html": "^1.0.2",
"@ohif/core": "^2.1.0",
"@ohif/extension-cornerstone": "^2.1.0",
"@ohif/extension-dicom-html": "^1.1.0",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.0",
"@ohif/extension-vtk": "^1.0.0",
"@ohif/i18n": "^0.52.2",
"@ohif/ui": "^1.0.0",
"@ohif/extension-vtk": "^1.1.2",
"@ohif/i18n": "^0.52.3",
"@ohif/ui": "^1.1.2",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
@@ -60,7 +60,7 @@
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4",
"hammerjs": "^2.0.8",
+1 -1
View File
@@ -70,5 +70,5 @@ window.config = {
// ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
],
cornerstoneExtensionConfig: { tools: {} },
cornerstoneExtensionConfig: {},
};
File diff suppressed because it is too large. Load diff
+16 -15
View File
@@ -9,7 +9,7 @@ import { hot } from 'react-hot-loader/root';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import ToolContextMenu from './connectedComponents/ToolContextMenu';
import LabellingManager from './components/Labelling/LabellingManager';
import LabellingFlow from './components/Labelling/LabellingFlow';
import {
SnackbarProvider,
@@ -28,11 +28,11 @@ import {
ExtensionManager,
ServicesManager,
HotkeysManager,
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
utils,
redux as reduxOHIF,
} from '@ohif/core';
@@ -70,13 +70,6 @@ const commandsManagerConfig = {
getActiveContexts: () => getActiveContexts(store.getState()),
};
/** Services */
const UINotificationService = createUINotificationService();
const UIModalService = createUIModalService();
const UIDialogService = createUIDialogService();
const UIContextMenuService = createUIContextMenuService();
const UILabellingFlowService = createUILabellingFlowService();
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
@@ -165,6 +158,14 @@ class App extends Component {
render() {
const { whiteLabelling, routerBasename } = this._appConfig;
const {
UINotificationService,
UIDialogService,
UILabellingFlowService,
UIModalService,
UIContextMenuService,
} = servicesManager.services;
if (this._userManager) {
return (
<AppContext.Provider value={{ appConfig: this._appConfig }}>
@@ -182,7 +183,7 @@ class App extends Component {
>
<LabellingFlowProvider
service={UILabellingFlowService}
labellingComponent={LabellingManager}
labellingComponent={LabellingFlow}
commandsManager={commandsManager}
>
<ContextMenuProvider
@@ -219,7 +220,7 @@ class App extends Component {
<ModalProvider modal={OHIFModal} service={UIModalService}>
<LabellingFlowProvider
service={UILabellingFlowService}
labellingComponent={LabellingManager}
labellingComponent={LabellingFlow}
commandsManager={commandsManager}
>
<ContextMenuProvider
@@ -3,12 +3,20 @@ import { MeasurementTable } from '@ohif/ui';
import OHIF from '@ohif/core';
import moment from 'moment';
import cornerstone from 'cornerstone-core';
import DICOMSR from './../../lib/DICOMSR';
import jumpToRowItem from './jumpToRowItem.js';
const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements;
/**
* Takes a list of objects and a property and return the list grouped by the property
*
* @param {Array} list - The objects to be grouped by
* @param {string} props - The property to group the objects
* @returns {Object}
*/
function groupBy(list, props) {
return list.reduce((a, b) => {
(a[b[props]] = a[b[props]] || []).push(b);
@@ -16,16 +24,29 @@ function groupBy(list, props) {
}, {});
}
function getAllTools() {
const config = OHIF.measurements.MeasurementApi.getConfiguration();
/**
* Takes a list of tools grouped and return all tools separately
*
* @param {Array} [toolGroups=[]] - The grouped tools
* @returns {Array} - The list of all tools on all groups
*/
function getAllTools(toolGroups = []) {
let tools = [];
config.measurementTools.forEach(
toolGroups.forEach(
toolGroup => (tools = tools.concat(toolGroup.childTools))
);
return tools;
}
/**
* Takes measurementData and build the measurement text to be used into the table
*
* @param {Object} [measurementData={}]
* @param {string} measurementData.location - The measurement location
* @param {string} measurementData.description - The measurement description
* @returns {string}
*/
function getMeasurementText(measurementData = {}) {
const defaultText = '...';
const { location = '', description = '' } = measurementData;
@@ -34,6 +55,14 @@ function getMeasurementText(measurementData = {}) {
return result || defaultText;
}
/**
* Takes a list of measurements grouped by measurement numbers and return each measurement data by available timepoint
*
* @param {Array} measurementNumberList - The list of measurements
* @param {Array} timepoints - The list of available timepoints
* @param {Function} displayFunction - The function that builds the display text by each tool
* @returns
*/
function getDataForEachMeasurementNumber(
measurementNumberList,
timepoints,
@@ -56,10 +85,35 @@ function getDataForEachMeasurementNumber(
return data;
}
/**
* Take a measurement toolName and return if any warnings
*
* @param {string} toolName - The tool name
* @returns {string}
*/
function getWarningsForMeasurement(toolName) {
const isToolSupported = DICOMSR.isToolSupported(toolName);
return {
hasWarnings: !isToolSupported,
warningTitle: isToolSupported ? '' : 'Unsupported Tool',
warningList: isToolSupported
? []
: [`${toolName} cannot be persisted at this time`],
};
}
/**
* Take measurements from MeasurementAPI structure and convert into a measurementTable structure
*
* @param {Object} toolCollections - The list of all measurement grouped by groupTool and toolName
* @param {Array} timepoints - The list of available timepoints
* @returns
*/
function convertMeasurementsToTableData(toolCollections, timepoints) {
const config = OHIF.measurements.MeasurementApi.getConfiguration();
const toolGroups = config.measurementTools;
const tools = getAllTools();
const tools = getAllTools(toolGroups);
const tableMeasurements = toolGroups.map(toolGroup => {
return {
@@ -88,6 +142,12 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
} = measurementData;
const measurementId = measurementData._id;
const {
hasWarnings,
warningTitle,
warningList,
} = getWarningsForMeasurement(toolType);
//check if all measurements with same measurementNumber will have same LABEL
const tableMeasurement = {
itemNumber: lesionNamingNumber,
@@ -96,10 +156,10 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
measurementNumber,
lesionNamingNumber,
toolType,
hasWarnings: false, //TODO
warningTitle: '', //TODO
hasWarnings,
warningTitle,
warningList,
isSplitLesion: false, //TODO
warningList: [], //TODO
data: getDataForEachMeasurementNumber(
measurementNumberList,
timepoints,
@@ -126,6 +186,13 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
return tableMeasurements;
}
/**
* Take a list of available timepoints and return a list header information for each timepoint
*
* @param {Array} timepoints - The list of available timepoints
* @param {string} timepoints[].latestDate - The date of the last study taken on the timepoint
* @returns {{label: string, key: string, date: string}[]}
*/
function convertTimepointsToTableData(timepoints) {
if (!timepoints || !timepoints.length) {
return [];
@@ -140,8 +207,28 @@ function convertTimepointsToTableData(timepoints) {
];
}
/**
* Takes server type and return a function or undefined
*
* @param {string} serverType - The server type
* @returns {undefined|Function}
*/
function getSaveFunction(serverType) {
if (serverType === 'dicomWeb') {
return () => {
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const promise = measurementApi.storeMeasurements();
return promise;
};
}
}
const mapStateToProps = state => {
const { timepoints, measurements } = state.timepointManager;
const { timepointManager, servers } = state;
const { timepoints, measurements } = timepointManager;
const activeServer = servers.servers.find(a => a.active === true);
const saveFunction = getSaveFunction(activeServer.type);
return {
timepoints: convertTimepointsToTableData(timepoints),
measurementCollection: convertMeasurementsToTableData(
@@ -150,6 +237,7 @@ const mapStateToProps = state => {
),
timepointManager: state.timepointManager,
viewports: state.viewports,
saveFunction,
};
};
@@ -265,10 +353,15 @@ const mapDispatchToProps = (dispatch, ownProps) => {
};
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
const { timepoints, saveFunction, measurementCollection } = propsFromState;
const { onSaveComplete, selectedMeasurementNumber } = ownProps;
return {
timepoints: propsFromState.timepoints,
measurementCollection: propsFromState.measurementCollection,
selectedMeasurementNumber: ownProps.selectedMeasurementNumber,
timepoints,
saveFunction,
measurementCollection,
onSaveComplete,
selectedMeasurementNumber,
...propsFromDispatch,
onItemClick: (event, measurementData) => {
// TODO: Add timepointId to .data for measurementData?
@@ -11,8 +11,9 @@ export default {
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration });
},
getPanelModule({ servicesManager, commandsManager }) {
const { UILabellingFlowService } = servicesManager.services;
const { UILabellingFlowService, UINotificationService } = servicesManager.services;
const ExtendedConnectedMeasurementTable = () => (
<ConnectedMeasurementTable
onRelabel={tool => {
@@ -38,6 +39,11 @@ export default {
});
}
}}
onSaveComplete={message => {
if (UINotificationService) {
UINotificationService.show(message);
}
}}
/>
);
return {
@@ -6,8 +6,7 @@
max-height: 500px;
}
.labellingComponent .selectedLabel,
.labellingComponent .selectedDescription {
.labellingComponent .selectedLabel, .labellingComponent .selectedDescription {
padding: 5px;
background-color: white;
width: 150px;
@@ -86,14 +85,12 @@
color: #337ab7;
}
.labellingComponent .commonButtons,
.labellingComponent.editDescription .editDescriptionButtons {
.labellingComponent .commonButtons, .labellingComponent.editDescription .editDescriptionButtons {
display: block;
margin-left: 55px;
}
.labellingComponent.editDescription .commonButtons,
.labellingComponent .editDescriptionButtons {
.labellingComponent.editDescription .commonButtons, .labellingComponent .editDescriptionButtons {
display: none;
}
@@ -1,112 +1,165 @@
import { Icon, SelectTree } from '@ohif/ui';
import React, { Component } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import LabellingTransition from './LabellingTransition.js';
import OHIFLabellingData from './OHIFLabellingData.js';
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
import './LabellingFlow.css';
export default class LabellingFlow extends Component {
static propTypes = {
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
initialTopDistance: PropTypes.number,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
const LabellingFlow = ({
measurementData,
editLocation,
editDescription,
skipAddLabelButton,
updateLabelling,
labellingDoneCallback,
editDescriptionOnDialog,
}) => {
const [fadeOutTimer, setFadeOutTimer] = useState();
const [showComponent, setShowComponent] = useState(true);
const descriptionInput = useRef();
const [state, setState] = useState({
measurementData,
editLocation,
editDescription,
skipAddLabelButton,
});
useEffect(() => {
const newMeasurementData = cloneDeep(measurementData);
if (editDescription) {
newMeasurementData.description = undefined;
}
if (editLocation) {
newMeasurementData.location = undefined;
}
let newEditLocation = editLocation;
if (!editDescription && !editLocation) {
newEditLocation = true;
}
setState(state => ({
...state,
editLocation: newEditLocation,
measurementData: newMeasurementData,
}));
}, [editDescription, editLocation, measurementData]);
useEffect(() => {
if (descriptionInput.current) {
descriptionInput.current.focus();
}
}, [state]);
const relabel = event =>
setState(state => ({ ...state, editLocation: true }));
const setDescriptionUpdateMode = () => {
descriptionInput.current.focus();
setState(state => ({ ...state, editDescription: true }));
};
constructor(props) {
super(props);
const descriptionCancel = () => {
const { description = '' } = cloneDeep(state);
descriptionInput.current.value = description;
setState(state => ({ ...state, editDescription: false }));
};
const { location, locationLabel, description } = props.measurementData;
this.state = {
location,
locationLabel,
description,
skipAddLabelButton: props.skipAddLabelButton,
editDescription: props.editDescription,
editLocation: props.editLocation,
confirmationState: false,
displayComponent: true,
};
this.mainElement = React.createRef();
this.descriptionInput = React.createRef();
this.initialItems = OHIFLabellingData;
this.currentItems = cloneDeep(this.initialItems);
}
componentDidUpdate = () => {
if (this.state.editDescription) {
this.descriptionInput.current.focus();
const handleKeyPress = event => {
if (event.key === 'Enter') {
descriptionSave();
}
};
render() {
let mainElementClassName = 'labellingComponent';
if (this.state.editDescription) {
mainElementClassName += ' editDescription';
}
const descriptionSave = () => {
const description = descriptionInput.current.value;
updateLabelling({ description });
return (
<LabellingTransition
displayComponent={this.state.displayComponent}
onTransitionExit={this.props.labellingDoneCallback}
>
<>
<div
className={mainElementClassName}
ref={this.mainElement}
onMouseLeave={this.fadeOutAndLeave}
onMouseEnter={this.clearFadeOutTimer}
>
{this.labellingStateFragment()}
</div>
</>
</LabellingTransition>
);
}
labellingStateFragment = () => {
const {
skipAddLabelButton,
editLocation,
setState(state => ({
...state,
description,
locationLabel,
} = this.state;
editDescription: false,
}));
};
const selectTreeSelectCallback = (event, itemSelected) => {
const location = itemSelected.value;
const locationLabel = itemSelected.label;
updateLabelling({ location });
setState(state => ({
...state,
editLocation: false,
measurementData: {
...state.measurementData,
location,
locationLabel,
},
}));
};
const showLabelling = () => {
setState(state => ({
...state,
skipAddLabelButton: true,
editLocation: false,
}));
};
/*
* Waits for 1 sec to dismiss the labelling component.
*
*/
const fadeOutAndLeave = () =>
setFadeOutTimer(setTimeout(fadeOutAndLeaveFast, 1000));
const fadeOutAndLeaveFast = () => setShowComponent(false);
const clearFadeOutTimer = () => {
if (fadeOutTimer) {
clearTimeout(fadeOutTimer);
setFadeOutTimer(null);
}
};
const descriptionDialogUpdate = description => {
updateLabelling({ description });
labellingDoneCallback();
};
const labellingStateFragment = () => {
const { skipAddLabelButton, editLocation, measurementData } = state;
const { description, locationLabel, location } = measurementData;
if (!skipAddLabelButton) {
return (
<>
<button
type="button"
className="addLabelButton"
onClick={this.showLabelling}
>
{this.state.location ? 'Edit' : 'Add'} Label
</button>
</>
<button
type="button"
className="addLabelButton"
onClick={showLabelling}
>
{location ? 'Edit' : 'Add'} Label
</button>
);
} else {
if (editLocation) {
return (
<SelectTree
items={this.currentItems}
items={OHIFLabellingData}
columns={1}
onSelected={this.selectTreeSelectCallback}
onSelected={selectTreeSelectCallback}
selectTreeFirstTitle="Assign Label"
/>
);
} else {
return (
<>
<div
className="checkIconWrapper"
onClick={this.fadeOutAndLeaveFast}
>
<div className="checkIconWrapper" onClick={fadeOutAndLeaveFast}>
<Icon name="check" className="checkIcon" />
</div>
<div className="locationDescriptionWrapper">
@@ -114,10 +167,10 @@ export default class LabellingFlow extends Component {
<div className="description">
<input
id="descriptionInput"
ref={this.descriptionInput}
ref={descriptionInput}
defaultValue={description || ''}
autoComplete="off"
onKeyPress={this.handleKeyPress}
onKeyPress={handleKeyPress}
/>
</div>
</div>
@@ -125,14 +178,14 @@ export default class LabellingFlow extends Component {
<button
type="button"
className="commonButton left"
onClick={this.relabel}
onClick={relabel}
>
Relabel
</button>
<button
type="button"
className="commonButton right"
onClick={this.setDescriptionUpdateMode}
onClick={setDescriptionUpdateMode}
>
{description ? 'Edit ' : 'Add '}
Description
@@ -142,14 +195,14 @@ export default class LabellingFlow extends Component {
<button
type="button"
className="commonButton left"
onClick={this.descriptionCancel}
onClick={descriptionCancel}
>
Cancel
</button>
<button
type="button"
className="commonButton right"
onClick={this.descriptionSave}
onClick={descriptionSave}
>
Save
</button>
@@ -160,77 +213,51 @@ export default class LabellingFlow extends Component {
}
};
relabel = event => this.setState({ editLocation: true });
setDescriptionUpdateMode = () => {
this.descriptionInput.current.focus();
this.setState({ editDescription: true });
};
descriptionCancel = () => {
const { description = '' } = cloneDeep(this.state);
this.descriptionInput.current.value = description;
this.setState({ editDescription: false });
};
handleKeyPress = e => {
if (e.key === 'Enter') {
this.descriptionSave();
}
};
descriptionSave = () => {
const description = this.descriptionInput.current.value;
this.props.updateLabelling({ description });
this.setState({
description,
editDescription: false,
});
};
selectTreeSelectCallback = (event, itemSelected) => {
const location = itemSelected.value;
this.props.updateLabelling({ location });
this.setState({
editLocation: false,
confirmationState: true,
location: itemSelected.value,
locationLabel: itemSelected.label,
});
if (this.isTouchScreen) {
this.setTimeout = setTimeout(() => {
this.setState({
displayComponent: false,
});
}, 2000);
}
};
showLabelling = () => {
this.setState({
skipAddLabelButton: true,
editLocation: false,
});
};
fadeOutAndLeave = () => {
// Wait for 1 sec to dismiss the labelling component
this.fadeOutTimer = setTimeout(
() => this.setState({ displayComponent: false }),
1000
if (editDescriptionOnDialog) {
return (
<EditDescriptionDialog
onCancel={labellingDoneCallback}
onUpdate={descriptionDialogUpdate}
measurementData={state.measurementData}
/>
);
};
}
fadeOutAndLeaveFast = () => this.setState({ displayComponent: false });
return (
<LabellingTransition
displayComponent={showComponent}
onTransitionExit={labellingDoneCallback}
>
<>
<div
className={`labellingComponent ${state.editDescription &&
'editDescription'}`}
onMouseLeave={fadeOutAndLeave}
onMouseEnter={clearFadeOutTimer}
>
{labellingStateFragment()}
</div>
</>
</LabellingTransition>
);
};
clearFadeOutTimer = () => {
if (!this.fadeOutTimer) {
return;
}
LabellingFlow.propTypes = {
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
initialTopDistance: PropTypes.number,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
editDescriptionOnDialog: PropTypes.bool,
};
clearTimeout(this.fadeOutTimer);
};
}
LabellingFlow.defaultProps = {
skipAddLabelButton: false,
editLocation: false,
editDescription: false,
editDescriptionOnDialog: false,
};
export default LabellingFlow;
@@ -1,103 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
import LabellingFlow from './LabellingFlow.js';
import './LabellingManager.css';
export default class LabellingManager extends Component {
static propTypes = {
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
editDescriptionOnDialog: PropTypes.bool,
};
static defaultProps = {
skipAddLabelButton: false,
editLocation: false,
editDescription: false,
editDescriptionOnDialog: false,
};
constructor(props) {
super(props);
const measurementData = cloneDeep(props.measurementData);
this.treatMeasurementData(measurementData);
let editLocation = props.editLocation;
if (!props.editDescription && !props.editLocation) {
editLocation = true;
}
this.state = {
skipAddLabelButton: props.skipAddLabelButton,
editLocation: editLocation,
editDescription: props.editDescription,
editDescriptionOnDialog: props.editDescriptionOnDialog,
measurementData: measurementData,
};
}
componentDidMount = () => {
document.addEventListener('touchstart', this.onTouchStart);
};
componentWillUnmount = () => {
document.removeEventListener('touchstart', this.onTouchStart);
};
render() {
return this.getRenderComponent();
}
getRenderComponent = () => {
const {
editLocation,
editDescription,
editDescriptionOnDialog,
measurementData,
} = this.state;
if (editDescriptionOnDialog) {
return (
<EditDescriptionDialog
onCancel={this.props.labellingDoneCallback}
onUpdate={this.descriptionDialogUpdate}
measurementData={measurementData}
/>
);
}
if (editLocation || editDescription) {
return <LabellingFlow {...this.props} />;
}
};
treatMeasurementData = measurementData => {
const { editDescription, editLocation } = this.props;
if (editDescription) {
measurementData.description = undefined;
}
if (editLocation) {
measurementData.location = undefined;
}
};
responseDialogUpdate = response => {
this.props.updateLabelling({ response });
this.props.labellingDoneCallback();
};
descriptionDialogUpdate = description => {
this.props.updateLabelling({ description });
this.props.labellingDoneCallback();
};
}
@@ -1,13 +1,20 @@
import { connect } from "react-redux";
import Viewer from "./Viewer.js";
import OHIF from "@ohif/core";
import { connect } from 'react-redux';
import Viewer from './Viewer.js';
import OHIF from '@ohif/core';
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
const mapStateToProps = (state, ownProps) => {
const getActiveServer = servers => {
const isActive = a => a.active === true;
return servers.servers.find(isActive);
};
const mapStateToProps = state => {
const { viewports, servers } = state;
return {
viewports: state.viewports.viewportSpecificData,
activeViewportIndex: state.viewports.activeViewportIndex
viewports: viewports.viewportSpecificData,
activeViewportIndex: viewports.activeViewportIndex,
activeServer: getActiveServer(servers),
};
};
@@ -18,7 +25,7 @@ const mapDispatchToProps = dispatch => {
},
onMeasurementsUpdated: measurements => {
dispatch(setMeasurements(measurements));
}
},
};
};
@@ -11,6 +11,7 @@ import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js';
import { extensionManager } from './../App.js';
import DICOMSR from '../lib/DICOMSR';
// Contexts
import WhiteLabellingContext from '../context/WhiteLabellingContext.js';
@@ -57,21 +58,31 @@ class Viewer extends Component {
static propTypes = {
studies: PropTypes.array,
studyInstanceUids: PropTypes.array,
activeServer: PropTypes.shape({
type: PropTypes.string,
wadoRoot: PropTypes.string,
}),
onTimepointsUpdated: PropTypes.func,
onMeasurementsUpdated: PropTypes.func,
// window.store.getState().viewports.viewportSpecificData
viewports: PropTypes.object.isRequired,
// window.store.getState().viewports.activeViewportIndex
activeViewportIndex: PropTypes.number.isRequired,
isStudyLoaded: PropTypes.bool,
};
constructor(props) {
super(props);
const { activeServer } = this.props;
const server = Object.assign({}, activeServer);
OHIF.measurements.MeasurementApi.setConfiguration({
dataExchange: {
retrieve: this.retrieveMeasurements,
store: this.storeMeasurements,
retrieve: DICOMSR.retrieveMeasurements,
store: DICOMSR.storeMeasurements,
},
server,
});
OHIF.measurements.TimepointApi.setConfiguration({
@@ -93,18 +104,6 @@ class Viewer extends Component {
thumbnails: [],
};
retrieveMeasurements = (patientId, timepointIds) => {
OHIF.log.info('retrieveMeasurements');
// TODO: Retrieve the measurements from the latest available SR
return Promise.resolve();
};
storeMeasurements = (measurementData, timepointIds) => {
OHIF.log.info('storeMeasurements');
// TODO: Store the measurements into a new SR sent to the active server
return Promise.resolve();
};
retrieveTimepoints = filter => {
OHIF.log.info('retrieveTimepoints');
@@ -171,7 +170,7 @@ class Viewer extends Component {
};
componentDidMount() {
const { studies } = this.props;
const { studies, isStudyLoaded } = this.props;
const { TimepointApi, MeasurementApi } = OHIF.measurements;
const currentTimepointId = 'TimepointId';
@@ -191,8 +190,11 @@ class Viewer extends Component {
const patientId = studies[0] && studies[0].patientId;
timepointApi.retrieveTimepoints({ patientId });
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
if (isStudyLoaded) {
this.measurementApi.retrieveMeasurements(patientId, [
currentTimepointId,
]);
}
this.setState({
thumbnails: _mapStudiesToThumbnails(studies),
});
@@ -200,18 +202,19 @@ class Viewer extends Component {
}
componentDidUpdate(prevProps) {
if (this.props.studies !== prevProps.studies) {
const { studies } = this.props;
const patientId = studies[0] && studies[0].patientId;
const currentTimepointId = this.currentTimepointId;
this.timepointApi.retrieveTimepoints({ patientId });
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
const { studies, isStudyLoaded } = this.props;
if (studies !== prevProps.studies) {
this.setState({
thumbnails: _mapStudiesToThumbnails(studies),
});
}
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
const patientId = studies[0] && studies[0].patientId;
const { currentTimepointId } = this;
this.timepointApi.retrieveTimepoints({ patientId });
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
}
}
render() {
@@ -294,11 +297,11 @@ class Viewer extends Component {
activeIndex={this.props.activeViewportIndex}
/>
) : (
<ConnectedStudyBrowser
studies={this.state.thumbnails}
studyMetadata={this.props.studies}
/>
)}
<ConnectedStudyBrowser
studies={this.state.thumbnails}
studyMetadata={this.props.studies}
/>
)}
</SidePanel>
{/* MAIN */}
@@ -334,7 +337,7 @@ export default Viewer;
* @param {Study[]} studies
* @param {DisplaySet[]} studies[].displaySets
*/
const _mapStudiesToThumbnails = function(studies) {
const _mapStudiesToThumbnails = function (studies) {
return studies.map(study => {
const { studyInstanceUid } = study;
@@ -178,6 +178,7 @@ function ViewerRetrieveStudyData({
// hooks
const [error, setError] = useState(false);
const [studies, setStudies] = useState([]);
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
const snackbarContext = useSnackbarContext();
const { appConfig = {} } = useContext(AppContext);
const { filterQueryParam: isFilterStrategy = false } = appConfig;
@@ -218,6 +219,7 @@ function ViewerRetrieveStudyData({
);
setStudies([...studies, study]);
setIsStudyLoaded(true);
};
/**
@@ -332,7 +334,11 @@ function ViewerRetrieveStudyData({
}
return (
<ConnectedViewer studies={studies} studyInstanceUids={studyInstanceUids} />
<ConnectedViewer
studies={studies}
isStudyLoaded={isStudyLoaded}
studyInstanceUids={studyInstanceUids}
/>
);
}
@@ -0,0 +1,73 @@
import { log, studies, utils } from '@ohif/core';
import { retrieveMeasurementFromSR, stowSRFromMeasurements } from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
/**
*
* @typedef serverType
* @property {string} type - type of the server
* @property {string} wadoRoot - server wado root url
*
*/
/**
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
*
* @param {serverType} server
* @returns {Promise} Should resolve with OHIF measurementData object
*/
const retrieveMeasurements = server => {
log.info('[DICOMSR] retrieveMeasurements');
if (!server || server.type !== 'dicomWeb') {
log.error('[DICOMSR] DicomWeb server is required!');
return Promise.reject({});
}
const serverUrl = server.wadoRoot;
const studies = utils.studyMetadataManager.all();
const latestSeries = findMostRecentStructuredReport(studies);
if (!latestSeries) return Promise.resolve({});
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
};
/**
* Function to be registered into MeasurementAPI to store measurements into DICOM Structured Reports
*
* @param {Object} measurementData - OHIF measurementData object
* @param {Object} filter
* @param {serverType} server
* @returns {Object} With message to be displayed on success
*/
const storeMeasurements = async (measurementData, filter, server) => {
log.info('[DICOMSR] storeMeasurements');
if (!server || server.type !== 'dicomWeb') {
log.error('[DICOMSR] DicomWeb server is required!');
return Promise.reject({});
}
const serverUrl = server.wadoRoot;
const firstMeasurementKey = Object.keys(measurementData)[0];
const firstMeasurement = measurementData[firstMeasurementKey][0];
const studyInstanceUid =
firstMeasurement && firstMeasurement.studyInstanceUid;
try {
await stowSRFromMeasurements(measurementData, serverUrl);
if (studyInstanceUid) {
studies.deleteStudyMetadataPromise(studyInstanceUid);
}
return {
message: 'Measurements were saved with success',
};
} catch (error) {
log.error(`[DICOMSR] Error while saving the measurements: ${error.message}`);
throw new Error('Error while saving the measurements.');
}
};
export { retrieveMeasurements, storeMeasurements };
@@ -0,0 +1,84 @@
import * as dcmjs from 'dcmjs';
import { DICOMWeb } from '@ohif/core';
import parseDicomStructuredReport from './parseDicomStructuredReport';
import parseMeasurementsData from './parseMeasurementsData';
import getAllDisplaySets from './utils/getAllDisplaySets';
import { api } from 'dicomweb-client';
const VERSION_NAME = 'dcmjs-0.0';
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
/**
* Function to retrieve measurements from DICOM Structured Reports coming from determined server
*
* @param {Array} series - List of all series metaData loaded
* @param {Array} studies - List of all studies metaData loaded
* @param {string} serverUrl - Server URL to be used on request
* @returns {Object} MeasurementData
*/
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
};
const dicomWeb = new api.DICOMwebClient(config);
const instance = series.getFirstInstance();
const options = {
studyInstanceUID: instance.getStudyInstanceUID(),
seriesInstanceUID: instance.getSeriesInstanceUID(),
sopInstanceUID: instance.getSOPInstanceUID(),
};
const part10SRArrayBuffer = await dicomWeb.retrieveInstance(options);
const displaySets = getAllDisplaySets(studies);
const measurementsData = parseDicomStructuredReport(part10SRArrayBuffer, displaySets);
return measurementsData;
};
/**
* Function to store measurements to DICOM Structured Reports in determined server
*
* @param {Object} measurements - OHIF measurementData object
* @param {string} serverUrl - Server URL to be used on request
* @returns {Promise}
*/
const stowSRFromMeasurements = async (measurements, serverUrl) => {
const { dataset } = parseMeasurementsData(
measurements
);
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const meta = {
FileMetaInformationVersion:
dataset._meta.FileMetaInformationVersion.Value,
MediaStorageSOPClassUID: dataset.SOPClassUID,
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
TransferSyntaxUID: TRANSFER_SYNTAX_UID,
ImplementationClassUID: DicomMetaDictionary.uid(),
ImplementationVersionName: VERSION_NAME,
};
const denaturalized = DicomMetaDictionary.denaturalizeDataset(meta);
const dicomDict = new DicomDict(denaturalized);
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset);
const part10Buffer = dicomDict.write();
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
datasets: [part10Buffer],
};
await dicomWeb.storeInstances(options);
};
export { retrieveMeasurementFromSR, stowSRFromMeasurements };
+10
View File
@@ -0,0 +1,10 @@
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
import isToolSupported from './utils/isToolSupported';
const DICOMSR = {
retrieveMeasurements,
storeMeasurements,
isToolSupported,
};
export default DICOMSR;
@@ -0,0 +1,82 @@
import * as dcmjs from 'dcmjs';
import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBySopInstanceUid';
/**
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
* (e.g. measurement table)
*
* @param {ArrayBuffer} part10SRArrayBuffer
* @param {Array} displaySets
* @returns
*/
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
// Get the dicom data as an Object
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const storedMeasurementByToolType = MeasurementReport.generateToolState(
dataset
);
const measurementData = {};
let measurementNumber = 0;
Object.keys(storedMeasurementByToolType).forEach(toolName => {
const measurements = storedMeasurementByToolType[toolName];
measurementData[toolName] = [];
measurements.forEach(measurement => {
const instanceMetadata = findInstanceMetadataBySopInstanceUid(
displaySets,
measurement.sopInstanceUid
);
const { _study: study, _series: series } = instanceMetadata;
const { studyInstanceUid, patientId } = study;
const { seriesInstanceUid } = series;
const { sopInstanceUid, frameIndex } = measurement;
const imagePath = getImagePath(studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex);
const imageId = instanceMetadata.getImageId();
if (!imageId) {
return;
}
// TODO: We need the currentTimepointID set into the viewer
const currentTimepointId = 'TimepointId';
const toolData = Object.assign({}, measurement, {
imageId,
imagePath,
seriesInstanceUid,
studyInstanceUid,
patientId,
measurementNumber: ++measurementNumber,
timepointId: currentTimepointId,
toolType: toolName,
_id: imageId + measurementNumber,
});
measurementData[toolName].push(toolData);
});
});
return measurementData;
};
/**
* Function to create imagePath with all imageData related
*
* @param {string} studyInstanceUid
* @param {string} seriesInstanceUid
* @param {string} sopInstanceUid
* @param {string} frameIndex
* @returns
*/
const getImagePath = (studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex) => {
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join('_');
}
export default parseDicomStructuredReport;
@@ -0,0 +1,55 @@
import * as dcmjs from 'dcmjs';
import { log, measurements } from '@ohif/core';
import cornerstone from 'cornerstone-core';
import isToolSupported from './utils/isToolSupported';
/**
* Function to parse OHIF viewer measurementData into a dcmjs MeasurementReport
*
* @param {Object} measurementsData - OHIF measurementData object
* @returns {Object} Dataset: measurement report from dcmjs
*/
const parseMeasurementsData = measurementsData => {
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const { getImageIdForImagePath } = measurements;
const toolState = {};
const unsupportedTools = [];
Object.keys(measurementsData).forEach(measurementType => {
const annotations = measurementsData[measurementType];
annotations.forEach(annotation => {
const { toolType, imagePath } = annotation;
if (isToolSupported(toolType)) {
const imageId = getImageIdForImagePath(imagePath);
toolState[imageId] = toolState[imageId] || {};
toolState[imageId][toolType] = toolState[imageId][toolType] || {
data: [],
};
toolState[imageId][toolType].data.push(annotation);
} else {
unsupportedTools.push(toolType);
}
});
});
if (unsupportedTools.length > 0) {
log.warn(
`[DICOMSR] Tooltypes not supported: ${unsupportedTools.join(', ')}`
);
}
const report = MeasurementReport.generateReport(
toolState,
cornerstone.metaData
);
return {
dataset: report.dataset,
};
};
export default parseMeasurementsData;
@@ -0,0 +1,22 @@
/**
* Should Find the requested instance metadata into the displaySets and return
*
* @param {Array} displaySets - List of displaySets
* @param {string} sopInstanceUid - sopInstanceUID to look for
* @returns {Object} - instance metadata found
*/
const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
let instanceFound;
displaySets.find(displaySet => {
if (!displaySet.images) return false;
instanceFound = displaySet.images.find(instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid);
return !!instanceFound;
});
return instanceFound;
};
export default findInstanceMetadataBySopInstanceUid;
@@ -0,0 +1,57 @@
/**
* Should find the most recent Structured Report metadata
*
* @param {Array} studies
* @returns {Object} Series
*/
const findMostRecentStructuredReport = (studies) => {
let mostRecentStructuredReport;
studies.forEach(study => {
const allSeries = study.getSeries ? study.getSeries() : [];
allSeries.forEach(series => {
if (isStructuredReportSeries(series)) {
if (!mostRecentStructuredReport || compareSeriesDate(series, mostRecentStructuredReport)) {
mostRecentStructuredReport = series;
}
}
});
});
return mostRecentStructuredReport;
};
/**
* Checks if series sopClassUID matches with the supported Structured Reports sopClassUID
*
* @param {Object} series - Series metadata
* @returns {boolean}
*/
const isStructuredReportSeries = (series) => {
const supportedSopClassUIDs = [
'1.2.840.10008.5.1.4.1.1.88.22',
'1.2.840.10008.5.1.4.1.1.11.1',
];
const firstInstance = series.getFirstInstance();
const sopClassUid = firstInstance._instance.sopClassUid;
return supportedSopClassUIDs.includes(sopClassUid);
}
/**
* Checkes if series1 is newer than series2
*
* @param {Object} series1 - Series Metadata 1
* @param {Object} series2 - Series Metadata 2
* @returns {boolean} true/false if series1 is newer than series2
*/
const compareSeriesDate = (series1, series2) => {
return series1._data.seriesDate > series2._data.seriesDate ||
(series1._data.seriesDate === series2._data.seriesDate &&
series1._data.seriesTime > series2._data.seriesTime);
}
export default findMostRecentStructuredReport;
@@ -0,0 +1,19 @@
/**
* Retrieve a list of all displaySets of all studies
*
* @param {Object} studies - List of studies loaded into the viewer
* @returns {Object} List of DisplaySets
*/
const getAllDisplaySets = (studies) => {
let allDisplaySets = [];
studies.forEach(study => {
if (study.getDisplaySets) {
allDisplaySets = allDisplaySets.concat(study.getDisplaySets());
}
});
return allDisplaySets;
};
export default getAllDisplaySets;
@@ -0,0 +1,14 @@
import * as dcmjs from 'dcmjs';
/**
* Checks if dcmjs has support to determined tool
*
* @param {string} toolName
* @returns {boolean}
*/
const isToolSupported = toolName => {
const adapter = dcmjs.adapters.Cornerstone;
return !!adapter[toolName];
};
export default isToolSupported;
+10 -29
View File
@@ -876,7 +876,7 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.2.5":
"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc"
integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw==
@@ -2654,16 +2654,6 @@
dependencies:
"@types/node" "^12.11.1"
"@ohif/extension-cornerstone@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-2.0.0.tgz#b4ee3b594212502192cdd311a100354857280548"
integrity sha512-hvJ3t2GRcu906NiYpUWaJIB9ja/M/MKEYLy19t4XBFpqf4VqoezR2KJpZelmZjQscXGaDQeZgvr3DJO5Nva8Uw==
dependencies:
"@babel/runtime" "^7.5.5"
classnames "^2.2.6"
lodash.throttle "^4.1.1"
react-cornerstone-viewport "0.1.30"
"@ohif/i18n@^0.2.3":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@ohif/i18n/-/i18n-0.2.5.tgz#2b92a78109823ce0f50dd590201d980a5fd64974"
@@ -6545,14 +6535,14 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dcmjs@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.6.1.tgz#453f764bca2cd8e3430126ba8385ef4c77071bcd"
integrity sha512-sJ0cRVZY02GSaAW5S9JPcv8+7iDtgrM0L/niYWAvu2piSa4bFSXwKi+t4XevAfkm+mF0sXZtaD15bw+/oiCYkw==
dcmjs@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.8.2.tgz#e1107273764de0a3052244b79d999fd870e58650"
integrity sha512-HEd1Rq12qxZXVXr5B4xgl/foCWaXxioO5v42FLgiUjKrbQR6iKIVcZsmE+3/AvAurcHiQFfW5vk5K4AY7eAp5A==
dependencies:
"@babel/polyfill" "^7.2.5"
"@babel/runtime" "^7.3.1"
loglevelnext "^3.0.0"
"@babel/polyfill" "^7.6.0"
"@babel/runtime" "^7.6.3"
loglevelnext "^3.0.1"
ndarray "^1.0.18"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
@@ -12383,7 +12373,7 @@ loglevelnext@^1.0.1, loglevelnext@^1.0.2:
es6-symbol "^3.1.1"
object.assign "^4.1.0"
loglevelnext@^3.0.0:
loglevelnext@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04"
integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA==
@@ -13103,7 +13093,7 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
moment@2.24.0, moment@>=1.6.0, moment@^2.23.0, moment@^2.24.0:
moment@2.24.0, moment@>=1.6.0, moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
@@ -15839,15 +15829,6 @@ react-codemirror2@^6.0.0:
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-6.0.0.tgz#180065df57a64026026cde569a9708fdf7656525"
integrity sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag==
react-cornerstone-viewport@0.1.30:
version "0.1.30"
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-0.1.30.tgz#3546a5164feef5f1f3b45ade10fd9a66f82fd101"
integrity sha512-DGxdpS7FJvWi57NhpKhKMdaW3go/Fs/zfWJFtaEzGVHq2zQnEEV5G1pamC4ttRhKMfzy7T7y4h14xUVzE7IqPA==
dependencies:
lodash.debounce "^4.0.8"
moment "^2.23.0"
stylus "^0.54.5"
react-cornerstone-viewport@2.x.x:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.1.0.tgz#1f353e3b0a57c45add70e5040b819290d9f55f4b"