Compare commits

...
Author SHA1 Message Date
ohif-bot e8d56c11f0 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.15
 - @ohif/core@2.3.9
 - @ohif/viewer@3.5.1
2020-02-07 16:11:02 +00:00
Erik Ziegler a6b92a4505 Fix issues with isDisplaySetReconstructable when we have empty DICOM tags (closes #1280) (#1426) 2020-02-07 17:09:37 +01:00
ohif-bot a4c9a86772 chore(release): publish [skip ci]
- @ohif/extension-lesion-tracker@0.1.0
 - @ohif/viewer@3.5.0
2020-02-06 19:07:34 +00:00
Danny Brown 73e440968c feat: lesion-tracker extension (#1420)
* chore: init lesion-tracker extension

* chore: dev build for lesion-tracker extension

* fix: a more appropriate library name

* chore: fix script naming
2020-02-06 14:04:18 -05:00
ohif-bot 3f7eb8a5d3 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.14
 - @ohif/core@2.3.8
 - @ohif/viewer@3.4.14
2020-02-06 02:56:29 +00:00
Gustavo André LelisandDanny Brown 7b2d44f2c1 fix: Remove trash data from redux storage after updates (#1358)
* Remove tash data from redux storage after updates

* Imprelemnted more unit tests

* Remove missing comment

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-02-05 21:54:57 -05:00
ohif-bot 225da81135 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.13
 - @ohif/core@2.3.7
 - @ohif/viewer@3.4.13
2020-01-30 18:33:22 +00:00
Rodrigo AntinarelliandDanny Brown 686d12da5c fix: Set VTK viewport as active by interaction (#1139)
* fix: Set VTK viewport as active by interaction

* feat: listen for vtkscrollevent in wrapper component

* fix: definitions can skip empty storeContexts key

* hoc to set/pass in commandsManager

* Bump minimum react-vtkjs-viewport version to leverage new event

* Simplify to use onScroll event instead of passing down commandsManager to base component

* fix: make sure we include @JamesAPetts bug fix

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-01-30 13:26:06 -05:00
ohif-bot c22756051b chore(release): publish [skip ci]
- @ohif/viewer@3.4.12
2020-01-30 03:46:42 +00:00
8fdfcc0c7d chore: Remove unused dependency on favicons-webpack-plugin. (#1351)
This dependency depends on the `favicons` library which depends on
`sharp` which depends on a native library.

This complicates the build process as the library is built from source
because there's no binary available for my macOS Catalina
installation. As this is a common developer setup I expect others to
run into this issue as well, which is why I opted to fix this issue.

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
2020-01-29 22:43:15 -05:00
ohif-bot 44ab65c981 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.12
 - @ohif/i18n@0.52.5
 - @ohif/ui@1.1.9
 - @ohif/viewer@3.4.11
2020-01-30 03:42:31 +00:00
b9574b6efc fix: download tool fixes & improvements (#1235)
* fix: download tool fixes & improvements

* fix filename error

* Create variable to track erros

* Fix small console error

* Fix all conflicts and merge changes from latest master with this PR's improvements

* Small improvement on select and textinput labels

* Add new icon for unlink

* Refactor on download image modal

* Adding loading screen

* Fix translation issue

* Fixing aspect Ratio and E2E tests

* Allow empty value without setting to 0

* Remove eslint comments

* Fixing typos

Co-authored-by: Gustavo André Lelis <galelis@gmail.com>
Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-01-29 22:39:44 -05:00
ohif-bot 96a8e26786 chore(release): publish [skip ci]
- @ohif/viewer@3.4.10
2020-01-28 21:32:03 +00:00
Mirna SilvaandDanny Brown 80f5b23622 test: 1316 - E2E tests for /local (#1379)
* 1316 - E2E tests for /local

* fix test title

* update yarn.lock

* Increased timeout for thumbnails

* Removing PDF file test from script-tag server

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-01-28 16:29:17 -05:00
ohif-bot 2b4950e954 chore(release): publish [skip ci]
- @ohif/viewer@3.4.9
2020-01-28 19:18:20 +00:00
dependabot[bot] e16f14089d chore(deps-dev): bump lodash from 4.17.11 to 4.17.15 in /platform/viewer (#1403)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2020-01-28 14:15:59 -05:00
Danny Brown c3807a89ed chore: exclude extension discussion 2020-01-28 12:46:05 -05:00
Danny Brown 049ef4a666 chore: scare away stale bot 2020-01-28 12:43:07 -05:00
ohif-bot d5fa880a29 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.11
 - @ohif/core@2.3.6
 - @ohif/viewer@3.4.8
2020-01-28 14:03:08 +00:00
Erik Ziegler 1b9a6db73f Handle Palette Color images where LUT has been returned as InlineBinary (#1350) (#1401) 2020-01-28 15:01:13 +01:00
ohif-bot ed2692908e chore(release): publish [skip ci]
- @ohif/viewer@3.4.7
2020-01-28 12:54:21 +00:00
Joël Spaltenstein 0c6c5a5d51 OIDC Initiating Login from a Third Party
https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin
2020-01-28 13:50:54 +01:00
ohif-bot b449f8eb0e chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.2
 - @ohif/extension-vtk@1.2.10
 - @ohif/core@2.3.5
 - @ohif/viewer@3.4.6
2020-01-28 12:41:49 +00:00
Erik Ziegler cf5160fb13 Add DICOM Group 6xxx Overlay Plane rendering (#1402)
* Add DICOM Group 6xxx Overlay Plane rendering

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

Clean up JSON launch with DICOMWeb server

✅ Closes: #1387

* Remove test file

* parse seriesinstanceuids in query

* Add default studies

* Remove test file

* Cleanup retrievedata changes

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

* Fixed bug in script.

* Fixed PR remark.

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

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

Closes: #1241

* Move setCornerstoneViewport command into cornerstone extension

* Exposing extensionManager into extension's get modules functions

* Remove Plugin switch

* Updating set cornerstonelayout to work on VIEWER context

* Creating a customComponent for 2D MPR button

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

* Make buttons hidden depending on extension registration

* Fix unit test after changes on extensionManager

* Remove workaround for extensionManager

* Use studyMeatadataManager and remove TODO's

* Remove storeContext not needed

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

* Remove PluginSwitch files

* Fix E2E tests

* Adding cypress screenshots into gitignore

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

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 15:59:36 -05:00
103 changed files with 2752 additions and 1947 deletions

No files matched your search

+2 -1
View File
@@ -328,6 +328,7 @@ workflows:
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
@@ -352,7 +353,7 @@ workflows:
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*'
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
+23 -9
View File
@@ -1,17 +1,31 @@
#!/bin/bash
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
if [ ! -z "$CLIENT_ID" ]
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
echo "Google Cloud Healthcare $CLIENT_ID has been provided: "
echo $CLIENT_ID
echo "Updating config..."
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
if [ -n "$CLIENT_ID" ]
then
echo "Google Cloud Healthcare \$CLIENT_ID has been provided: "
echo "$CLIENT_ID"
echo "Updating config..."
# - Use SED to replace the CLIENT_ID that is currently in google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
# - Use SED to replace the CLIENT_ID that is currently in google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
fi
# - Copy google.js to overwrite app-config.js
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
# If HEALTHCARE_API_ENDPOINT is specified, use the google.js configuration with the modified endpoint
if [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
echo "Google Cloud Healthcare \$HEALTHCARE_API_ENDPOINT has been provided: "
echo "$HEALTHCARE_API_ENDPOINT"
echo "Updating config..."
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
sed -i -e "s+https://healthcare.googleapis.com/v1beta1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
fi
# - Copy google.js to overwrite app-config.js
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
fi
echo "Starting Nginx to serve the OHIF Viewer..."
+2 -1
View File
@@ -12,10 +12,11 @@ exemptLabels:
- 'Task: CI/Tooling :robot:'
- 'Task: Docs 📖'
- 'Task: Docs :book:'
- 'Task: Refactor 🛠'
- 'Task: Refactor :hammer_and_wrench:'
- 'Task: Tests :microscope:'
- 'PR: Awaiting Review 👀'
- 'Triage :white_flag:'
- 'Extension: Discussion'
- 'Announcement 🎉'
# Label to use when marking an issue as stale
staleLabel: 'Stale :baguette_bread:'
+1
View File
@@ -30,6 +30,7 @@ docker/dcm4che/dcm4che-arc
# Cypress test results
videos/
screenshots/
# Locize settings
.locize
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.1...@ohif/extension-cornerstone@2.2.2) (2020-01-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.0...@ohif/extension-cornerstone@2.2.1) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.1...@ohif/extension-cornerstone@2.2.0) (2019-12-20)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.2.0",
"version": "2.2.2",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -32,7 +32,7 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.9.0",
"cornerstone-tools": "^4.12.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
@@ -2,6 +2,7 @@ import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll');
@@ -242,6 +243,9 @@ const commandsModule = ({ servicesManager }) => {
cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element);
},
setCornerstoneLayout: () => {
setCornerstoneLayout();
}
};
const definitions = {
@@ -348,6 +352,12 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
setCornerstoneLayout: {
commandFn: actions.setCornerstoneLayout,
storeContexts: [],
options: {},
context: 'VIEWER',
},
};
return {
+2
View File
@@ -98,6 +98,7 @@ export default function init({ servicesManager, configuration }) {
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
csTools.OverlayTool,
],
};
@@ -179,4 +180,5 @@ export default function init({ servicesManager, configuration }) {
csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required
csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
csTools.setToolActive('ZoomTouchPinch', {});
csTools.setToolEnabled('Overlay', {});
}
@@ -237,6 +237,15 @@ const definitions = [
},
],
},
{
id: 'Exit2DMPR',
label: 'Exit 2D MPR',
icon: 'times',
//
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'setCornerstoneLayout',
context: 'ACTIVE_VIEWPORT::VTK',
}
];
export default {
@@ -0,0 +1,22 @@
import { redux } from '@ohif/core';
const { setLayout } = redux.actions;
/**
* Update the current layout with a simple Cornerstone one
*
* @return void
*/
const setCornerstoneLayout = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
const action = setLayout(layout);
window.store.dispatch(action);
}
export default setCornerstoneLayout;
@@ -0,0 +1,8 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
};
@@ -0,0 +1,38 @@
const merge = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'OHIFExtLesionTracker',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
});
};
+11
View File
@@ -0,0 +1,11 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# 0.1.0 (2020-02-06)
### Features
* lesion-tracker extension ([#1420](https://github.com/OHIF/Viewers/issues/1420)) ([73e4409](https://github.com/OHIF/Viewers/commit/73e440968ce4699d081a9c9f2d21dd68095b3056))
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+38
View File
@@ -0,0 +1,38 @@
# @ohif/extension-lesion-tracker
This project is an OHIF extension that can be used with the Core OHIF Platform,
either at runtime, or as an ES6 dependency, to create a medical image viewing
application similar to that of the [legacy Lesion
Tracker][legacy-lesion-tracker] viewer.
## About
...
## Scope
...
### Configuration
...
### Extensions
...
## Build & Deploy
...
## Funding/Support
...
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[legacy-lesion-tracker]: http://lesiontracker.ohif.org/studylist
<!-- prettier-ignore-end -->
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@ohif/extension-lesion-tracker",
"version": "0.1.0",
"description": "OHIF extension for Lesion Tracker",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=10",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"files": [
"dist",
"README.md"
],
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
"dev:lesion-tracker": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"prepublishOnly": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6"
}
}
+101
View File
@@ -0,0 +1,101 @@
// SEE: Async imports (code-splitting)
// import asyncComponent from './asyncComponent.js';
// import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js';
// const ConnectedOHIFDicomPDFViewer = asyncComponent(() =>
// import(
// /* webpackChunkName: "ConnectedOHIFDicomPDFViewer" */ './ConnectedOHIFDicomPDFViewer'
// )
// );
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'lesion-tracker',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getPanelModule({ servicesManager, commandsManager }) {
return undefined;
// {
// TODO:
// - Right, alternative StudyBrowser
// - Left, alternative Measurements Table
// menuOptions: [
// {
// // A suggested icon
// // Available icons determined by consuming app
// icon: 'list',
// // A suggested label
// label: 'Magic',
// // 'right' or 'left'
// from: 'right',
// // The target component to toggle open/close
// target: 'target-component-id',
// // Overrides `defaultContext`, if specified
// context: ['ACTIVE_VIEWPORT:MAGIC'],
// },
// ],
// components: [
// {
// id: 'target-component-id',
// component: MyComponent,
// },
// ],
// defaultContext: ['ROUTE:VIEWER'],
// };
},
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns Object
*/
getToolbarModule({ servicesManager, commandsManager }) {
// TODO: Zoom, Levels, Pan, More (see lesiontracker.ohif.org)
// These may be driven through configuration of `cornerstone` extension instead of defined here
return {
definitions: [
{
id: 'say-hell-world',
label: '🎉 HELLO WORLD 🎉',
icon: 'exclamation-triangle',
type: 'command',
commandName: 'sayHelloWorld',
},
],
defaultContext: 'VIEWER',
};
},
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns Object
*/
getCommandsModule({ servicesManager }) {
const { UINotificationService } = servicesManager.services;
return {
definitions: {
sayHelloWorld: {
commandFn: function() {
console.log(UINotificationService);
UINotificationService.show({
title: 'What does a nosey pepper do?',
message: 'Gets jalapeno business!',
});
},
storeContexts: [],
options: {},
},
},
defaultContext: ['VIEWER'],
};
},
};
+110
View File
@@ -3,6 +3,116 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.14...@ohif/extension-vtk@1.2.15) (2020-02-07)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.13...@ohif/extension-vtk@1.2.14) (2020-02-06)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.12...@ohif/extension-vtk@1.2.13) (2020-01-30)
### Bug Fixes
* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c))
## [1.2.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.11...@ohif/extension-vtk@1.2.12) (2020-01-30)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.10...@ohif/extension-vtk@1.2.11) (2020-01-28)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.9...@ohif/extension-vtk@1.2.10) (2020-01-28)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.8...@ohif/extension-vtk@1.2.9) (2020-01-27)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.7...@ohif/extension-vtk@1.2.8) (2020-01-24)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.6...@ohif/extension-vtk@1.2.7) (2020-01-08)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.5...@ohif/extension-vtk@1.2.6) (2020-01-07)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.4...@ohif/extension-vtk@1.2.5) (2020-01-06)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.3...@ohif/extension-vtk@1.2.4) (2019-12-30)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.2...@ohif/extension-vtk@1.2.3) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.1...@ohif/extension-vtk@1.2.2) (2019-12-20)
**Note:** Version bump only for package @ohif/extension-vtk
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.2.2",
"version": "1.2.15",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -49,12 +49,12 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"lodash.throttle": "^4.1.1",
"react-vtkjs-viewport": "^0.3.9"
"react-vtkjs-viewport": "^0.8.3"
},
"devDependencies": {
"@ohif/core": "^2.3.0",
"@ohif/ui": "^1.1.7",
"cornerstone-tools": "^4.9.0",
"@ohif/core": "^2.3.9",
"@ohif/ui": "^1.1.9",
"cornerstone-tools": "^4.12.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1",
+3 -2
View File
@@ -1,6 +1,6 @@
import OHIF from '@ohif/core';
import { View2D } from 'react-vtkjs-viewport';
import { connect } from 'react-redux';
import VTKViewport from './VTKViewport';
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
@@ -18,6 +18,7 @@ const mapStateToProps = (state, ownProps) => {
const pluginDetails = viewportLayout.vtk || {};
return {
activeViewportIndex: state.viewports.activeViewportIndex,
layout: state.viewports.layout,
isActive,
...pluginDetails,
@@ -75,6 +76,6 @@ const ConnectedVTKViewport = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(View2D);
)(VTKViewport);
export default ConnectedVTKViewport;
+6
View File
@@ -77,6 +77,11 @@ class OHIFVTKViewport extends Component {
}),
viewportIndex: PropTypes.number,
children: PropTypes.node,
onScroll: PropTypes.func,
};
static defaultProps = {
onScroll: () => {},
};
static id = 'OHIFVTKViewport';
@@ -386,6 +391,7 @@ class OHIFVTKViewport extends Component {
this.state.paintFilterBackgroundImageData
}
viewportIndex={this.props.viewportIndex}
onScroll={this.props.onScroll}
/>
)}
</div>
+3
View File
@@ -0,0 +1,3 @@
.vtk-viewport-handler svg {
pointer-events: none;
}
+58
View File
@@ -0,0 +1,58 @@
import React, { useEffect, useCallback } from 'react';
import { View2D } from 'react-vtkjs-viewport';
import PropTypes from 'prop-types';
import './VTKViewport.css';
const VTKViewport = props => {
const style = { width: '100%', height: '100%', position: 'relative' };
const setViewportActiveHandler = useCallback(() => {
const { setViewportActive, viewportIndex, activeViewportIndex } = props;
if (viewportIndex !== activeViewportIndex) {
// set in Connected
setViewportActive();
}
});
useEffect(() => {
const handleScrollEvent = evt => {
const vtkViewportApiReference = props.onScroll(props.viewportIndex) || {};
const viewportUid = vtkViewportApiReference.uid;
const viewportWasScrolled = viewportUid === evt.detail.uid;
if (viewportWasScrolled) {
setViewportActiveHandler();
}
};
window.addEventListener('vtkscrollevent', handleScrollEvent);
return () =>
window.removeEventListener('vtkscrollevent', handleScrollEvent);
}, [props, props.onScroll, props.viewportIndex, setViewportActiveHandler]);
return (
<div
className="vtk-viewport-handler"
style={style}
onClick={setViewportActiveHandler}
>
<View2D {...props} />
</div>
);
};
VTKViewport.propTypes = {
setViewportActive: PropTypes.func.isRequired,
viewportIndex: PropTypes.number.isRequired,
activeViewportIndex: PropTypes.number.isRequired,
/* Receives viewportIndex */
onScroll: PropTypes.func,
};
VTKViewport.defaultProps = {
onScroll: () => {},
};
export default VTKViewport;
+10 -12
View File
@@ -103,6 +103,9 @@ const commandsModule = ({ commandsManager }) => {
}
const actions = {
getVtkApis: ({ index }) => {
return apis[index];
},
axial: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
@@ -234,7 +237,7 @@ const commandsModule = ({ commandsManager }) => {
},
},
{
// Sagital
// Sagittal
orientation: {
sliceNormal: [1, 0, 0],
viewUp: [0, 0, 1],
@@ -266,11 +269,12 @@ const commandsModule = ({ commandsManager }) => {
'crosshairsWidget'
);
const uid = api.uid;
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
configuration: { apis, apiIndex, uid },
});
});
},
@@ -296,55 +300,45 @@ const commandsModule = ({ commandsManager }) => {
},
enableRotateTool: {
commandFn: actions.enableRotateTool,
storeContexts: [],
options: {},
},
enableCrosshairsTool: {
commandFn: actions.enableCrosshairsTool,
storeContexts: [],
options: {},
},
enableLevelTool: {
commandFn: actions.enableLevelTool,
storeContexts: [],
options: {},
},
setBlendModeToComposite: {
commandFn: actions.setBlendModeToComposite,
storeContexts: [],
options: { blendMode: BlendMode.COMPOSITE_BLEND },
},
setBlendModeToMaximumIntensity: {
commandFn: actions.setBlendModeToMaximumIntensity,
storeContexts: [],
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
},
setBlendModeToMinimumIntensity: {
commandFn: actions.setBlendMode,
storeContexts: [],
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
},
setBlendModeToAverageIntensity: {
commandFn: actions.setBlendMode,
storeContexts: [],
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
},
setSlabThickness: {
// TODO: How do we pass in a function argument?
commandFn: actions.setSlabThickness,
storeContexts: [],
options: {},
},
increaseSlabThickness: {
commandFn: actions.changeSlabThickness,
storeContexts: [],
options: {
change: 3,
},
},
decreaseSlabThickness: {
commandFn: actions.changeSlabThickness,
storeContexts: [],
options: {
change: -3,
},
@@ -355,6 +349,10 @@ const commandsModule = ({ commandsManager }) => {
options: {},
context: 'VIEWER',
},
getVtkApiForViewportIndex: {
commandFn: actions.getVtkApis,
context: 'VIEWER',
},
};
return {
+3 -2
View File
@@ -1,6 +1,7 @@
import asyncComponent from './asyncComponent.js';
import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js';
import withCommandsManager from './withCommandsManager.js';
// This feels weird
// import loadLocales from './loadLocales';
@@ -14,8 +15,8 @@ const vtkExtension = {
*/
id: 'vtk',
getViewportModule() {
return OHIFVTKViewport;
getViewportModule({ commandsManager }) {
return withCommandsManager(OHIFVTKViewport, commandsManager);
},
getToolbarModule() {
return toolbarModule;
@@ -0,0 +1,87 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { ToolbarButton } from '@ohif/ui';
import { utils } from '@ohif/core';
const { studyMetadataManager } = utils;
let isVisible = true;
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
if (!viewportSpecificData[activeViewportIndex]) {
return false;
};
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
activeViewportIndex
];
const studies = studyMetadataManager.all();
const study = studies.find(
study => study.studyInstanceUID === studyInstanceUid
);
if (!study) {
return false;
}
const displaySet = study._displaySets.find(set => set.displaySetInstanceUid === displaySetInstanceUid);
if (!displaySet) {
return false;
};
return displaySet.isReconstructable;
};
function VTKMPRToolbarButton({
parentContext,
toolbarClickCallback,
button,
activeButtons,
isActive,
className,
}) {
const { id, label, icon } = button;
const { viewportSpecificData, activeViewportIndex } = useSelector(state => {
const { viewports = {} } = state;
const { viewportSpecificData, activeViewportIndex } = viewports;
return {
viewportSpecificData,
activeViewportIndex,
}
});
isVisible = _isDisplaySetReconstructable(
viewportSpecificData,
activeViewportIndex,
);
return (
<React.Fragment>
{isVisible && (
<ToolbarButton
key={id}
label={label}
icon={icon}
onClick={evt => toolbarClickCallback(button, evt)}
isActive={isActive}
/>
)}
</React.Fragment>
);
}
VTKMPRToolbarButton.propTypes = {
parentContext: PropTypes.object.isRequired,
toolbarClickCallback: PropTypes.func.isRequired,
button: PropTypes.object.isRequired,
activeButtons: PropTypes.array.isRequired,
isActive: PropTypes.bool,
className: PropTypes.string,
};
export default VTKMPRToolbarButton;
+12 -1
View File
@@ -1,4 +1,5 @@
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
import VTKMPRToolbarButton from './toolbarComponents/VTKMPRToolbarButton';
const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command',
@@ -115,9 +116,19 @@ const definitions = [
},
],
},
{
id: '2DMPR',
label: '2D MPR',
icon: 'cube',
//
CustomComponent: VTKMPRToolbarButton,
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'mpr2d',
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
},
];
export default {
definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK',
};
}
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
export default function withCommandsManager(Component, commandsManager = {}) {
return class WithCommandsManager extends React.Component {
render() {
return (
<Component
{...this.props}
onScroll={viewportIndex =>
commandsManager.runCommand('getVtkApiForViewportIndex', {
index: viewportIndex,
})
}
/>
);
}
};
}
+3 -1
View File
@@ -13,11 +13,13 @@
"scripts": {
"cm": "npx git-cz",
"build": "lerna run build:viewer --stream",
"build:lt": "lerna run build:viewer:lesion-tracker --stream",
"build:ci": "lerna run build:viewer:ci --stream",
"build:demo": "lerna run build:viewer:demo --stream",
"build:package": "lerna run build:viewer:package --stream",
"build:package-all": "lerna run build:package --parallel --stream",
"dev": "lerna run dev:viewer --stream",
"dev:lt": "lerna run dev:viewer:lesion-tracker --stream",
"dev:project": ".scripts/dev.sh",
"dev:orthanc": "lerna run dev:orthanc --stream",
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
@@ -59,6 +61,7 @@
"copy-webpack-plugin": "^5.0.3",
"cross-env": "^5.2.0",
"css-loader": "^3.2.0",
"cypress-file-upload": "^3.5.3",
"dotenv": "^8.1.0",
"eslint": "5.16.0",
"eslint-config-prettier": "^6.4.0",
@@ -72,7 +75,6 @@
"eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "1.5.0",
"extract-css-chunks-webpack-plugin": "^4.5.4",
"favicons-webpack-plugin": "^1.0.2",
"html-webpack-plugin": "^3.2.0",
"husky": "^3.0.0",
"jest": "^24.8.0",
+81
View File
@@ -3,6 +3,87 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.3.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.8...@ohif/core@2.3.9) (2020-02-07)
**Note:** Version bump only for package @ohif/core
## [2.3.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.7...@ohif/core@2.3.8) (2020-02-06)
### Bug Fixes
* Remove trash data from redux storage after updates ([#1358](https://github.com/OHIF/Viewers/issues/1358)) ([7b2d44f](https://github.com/OHIF/Viewers/commit/7b2d44f2c18241ea521b8d3652aee32e36eaddb8))
## [2.3.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.6...@ohif/core@2.3.7) (2020-01-30)
### Bug Fixes
* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c))
## [2.3.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.5...@ohif/core@2.3.6) (2020-01-28)
**Note:** Version bump only for package @ohif/core
## [2.3.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.4...@ohif/core@2.3.5) (2020-01-28)
**Note:** Version bump only for package @ohif/core
## [2.3.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.3...@ohif/core@2.3.4) (2020-01-27)
**Note:** Version bump only for package @ohif/core
## [2.3.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.2...@ohif/core@2.3.3) (2020-01-24)
**Note:** Version bump only for package @ohif/core
## [2.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.1...@ohif/core@2.3.2) (2020-01-06)
**Note:** Version bump only for package @ohif/core
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.0...@ohif/core@2.3.1) (2019-12-30)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.1...@ohif/core@2.3.0) (2019-12-20)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.3.0",
"version": "2.3.9",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -32,7 +32,7 @@
},
"peerDependencies": {
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.9.0",
"cornerstone-tools": "^4.12.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3"
},
+1 -1
View File
@@ -161,7 +161,7 @@ export class CommandsManager {
return;
}
const { commandFn, storeContexts } = definition;
const { commandFn, storeContexts = [] } = definition;
const definitionOptions = definition.options;
let commandParams = {};
@@ -226,7 +226,7 @@ describe('ExtensionManager.js', () => {
return {
definitions: {
exampleDefinition: {
commandFn: () => {},
commandFn: () => { },
storeContexts: [],
options: {},
},
@@ -11,6 +11,16 @@ const servers = (state = defaultState, action) => {
servers.forEach(s => (s.active = true));
return { ...state, servers };
case 'ACTIVATE_SERVER': {
const newServer = { ...action.server, active: true };
const newServers = state.servers;
newServers.forEach(s => (s.active = false));
return {
...state,
servers: uniqBy([...newServers, newServer], 'wadoRoot'),
};
}
case 'SET_SERVERS':
return { ...state, servers: action.servers };
+83 -8
View File
@@ -11,20 +11,63 @@ import {
SET_VIEWPORT_LAYOUT_AND_DATA,
} from './../constants/ActionTypes.js';
const DEFAULT_STATE = {
export const DEFAULT_STATE = {
numRows: 1,
numColumns: 1,
activeViewportIndex: 0,
layout: {
viewports: [
{
// plugin: 'cornerstone',
},
],
viewports: [{}],
},
viewportSpecificData: {},
};
/**
* Take the new number of rows and columns, delete all not used viewport data and also set
* active viewport as default in case current one is not available anymore.
*
* @param {Number} numRows
* @param {Number} numColumns
* @param {Object} currentViewportSpecificData
* @returns
*/
const findActiveViewportSpecificData = (
numRows,
numColumns,
currentViewportSpecificData = {}
) => {
const numberOfViewports = numRows * numColumns;
const viewportSpecificData = cloneDeep(currentViewportSpecificData);
if (numberOfViewports < Object.keys(viewportSpecificData).length) {
Object.keys(viewportSpecificData).forEach(key => {
if (key > numberOfViewports - 1) {
delete viewportSpecificData[key];
}
});
}
return viewportSpecificData;
};
/**
* Take new number of rows and columns and make sure the current active viewport index is still available, if not, return the default
*
* @param {Number} numRows
* @param {Number} numColumns
* @param {Number} currentActiveViewportIndex
* @returns
*/
const getActiveViewportIndex = (
numRows,
numColumns,
currentActiveViewportIndex
) => {
const numberOfViewports = numRows * numColumns;
return currentActiveViewportIndex > numberOfViewports - 1
? DEFAULT_STATE.activeViewportIndex
: currentActiveViewportIndex;
};
/**
* The definition of a viewport action.
*
@@ -50,7 +93,12 @@ const viewports = (state = DEFAULT_STATE, action) => {
* @return {Object} New state.
*/
case SET_VIEWPORT_ACTIVE: {
return { ...state, activeViewportIndex: action.viewportIndex };
const activeViewportIndex = getActiveViewportIndex(
state.numRows,
state.numColumns,
action.viewportIndex
);
return { ...state, activeViewportIndex };
}
/**
@@ -59,11 +107,25 @@ const viewports = (state = DEFAULT_STATE, action) => {
* @return {Object} New state.
*/
case SET_VIEWPORT_LAYOUT: {
const { numRows, numColumns } = action;
const viewportSpecificData = findActiveViewportSpecificData(
numRows,
numColumns,
state.viewportSpecificData
);
const activeViewportIndex = getActiveViewportIndex(
numRows,
numColumns,
state.activeViewportIndex
);
return {
...state,
numRows: action.numRows,
numColumns: action.numColumns,
layout: { viewports: [...action.viewports] },
viewportSpecificData,
activeViewportIndex,
};
}
@@ -73,12 +135,25 @@ const viewports = (state = DEFAULT_STATE, action) => {
* @return {Object} New state.
*/
case SET_VIEWPORT_LAYOUT_AND_DATA: {
const { numRows, numColumns } = action;
const viewportSpecificData = findActiveViewportSpecificData(
numRows,
numColumns,
action.viewportSpecificData
);
const activeViewportIndex = getActiveViewportIndex(
numRows,
numColumns,
state.activeViewportIndex
);
return {
...state,
numRows: action.numRows,
numColumns: action.numColumns,
layout: { viewports: [...action.viewports] },
viewportSpecificData: cloneDeep(action.viewportSpecificData),
viewportSpecificData,
activeViewportIndex,
};
}
@@ -1,35 +1,61 @@
// https://medium.com/@netxm/testing-redux-reducers-with-jest-6653abbfe3e1
import reducer from './viewports.js';
import { Reducer } from 'redux-testkit';
import reducer, { DEFAULT_STATE } from './viewports.js';
import * as types from './../constants/ActionTypes.js';
describe('viewports reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual({
activeViewportIndex: 0,
numRows: 1,
numColumns: 1,
layout: {
viewports: [{}],
},
viewportSpecificData: {},
});
expect(reducer(undefined, {})).toEqual(DEFAULT_STATE);
});
it('should handle SET_VIEWPORT_ACTIVE', () => {
const setViewportActiveAction = {
it('should handle SET_VIEWPORT_ACTIVE with inexistent viewport index', () => {
const initialState = {
numRows: 4,
numColumns: 4,
activeViewportIndex: 0,
};
const action = {
type: types.SET_VIEWPORT_ACTIVE,
viewportIndex: 100,
};
const updatedState = reducer({}, setViewportActiveAction);
const expectedToChangeInState = {
activeViewportIndex: 0,
};
expect(updatedState.activeViewportIndex).toEqual(
setViewportActiveAction.viewportIndex
);
Reducer(reducer)
.withState(initialState)
.expect(action)
.toChangeInState(expectedToChangeInState);
});
it('should handle SET_VIEWPORT_ACTIVE with existent viewport index', () => {
const initialState = {
numRows: 4,
numColumns: 4,
activeViewportIndex: 0,
};
const action = {
type: types.SET_VIEWPORT_ACTIVE,
viewportIndex: 5,
};
const expectedToChangeInState = {
activeViewportIndex: 5,
};
Reducer(reducer)
.withState(initialState)
.expect(action)
.toChangeInState(expectedToChangeInState);
});
it('should handle SET_VIEWPORT_LAYOUT', () => {
const setViewportLayoutAction = {
const initialState = DEFAULT_STATE;
const action = {
type: types.SET_VIEWPORT_LAYOUT,
numRows: 1,
numColumns: 2,
@@ -43,18 +69,193 @@ describe('viewports reducer', () => {
],
};
const updatedState = reducer({}, setViewportLayoutAction);
const expectedToChangeInState = {
numRows: 1,
numColumns: 2,
layout: {
viewports: [
{
plugin: 'cornerstone',
},
{
plugin: 'vtk',
},
],
},
};
expect(updatedState.numRows).toEqual(setViewportLayoutAction.numRows);
expect(updatedState.numColumns).toEqual(setViewportLayoutAction.numColumns);
expect(updatedState.layout.viewports).toEqual(
setViewportLayoutAction.viewports
);
Reducer(reducer)
.withState(initialState)
.expect(action)
.toChangeInState(expectedToChangeInState);
});
it('should handle SET_VIEWPORT_LAYOUT when we reduce the number of viewports', () => {
const initialState = {
numRows: 1,
numColumns: 2,
viewportSpecificData: {
0: { viewportData0: 'viewportData0' },
1: { viewportData1: 'viewportData1' },
},
layout: {
viewports: [],
},
activeViewportIndex: 0,
};
const action = {
type: types.SET_VIEWPORT_LAYOUT,
numRows: 1,
numColumns: 1,
viewports: [],
};
const expectedState = {
numRows: 1,
numColumns: 1,
viewportSpecificData: {
0: { viewportData0: 'viewportData0' },
},
layout: {
viewports: [],
},
activeViewportIndex: 0,
};
Reducer(reducer)
.withState(initialState)
.expect(action)
.toReturnState(expectedState);
});
it('should handle SET_VIEWPORT_LAYOUT_AND_DATA', () => {
const initialState = {
numRows: 1,
numColumns: 1,
viewportSpecificData: {
0: { viewportData0: 'data0' },
},
layout: {
viewports: [{ plugin: 'cornerstone' }],
},
activeViewportIndex: 0,
};
const action = {
type: types.SET_VIEWPORT_LAYOUT_AND_DATA,
numRows: 1,
numColumns: 2,
viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }],
viewportSpecificData: {
0: { viewportData0: 'NEWdata0' },
1: { viewportData1: 'NEWdata1' },
},
};
const expectedState = {
numRows: 1,
numColumns: 2,
viewportSpecificData: {
0: { viewportData0: 'NEWdata0' },
1: { viewportData1: 'NEWdata1' },
},
layout: {
viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }],
},
activeViewportIndex: 0,
};
Reducer(reducer)
.withState(initialState)
.expect(action)
.toReturnState(expectedState);
});
it('should handle SET_VIEWPORT_LAYOUT_AND_DATA when we reduce the number of viewports', () => {
const initialState = {
numRows: 1,
numColumns: 3,
viewportSpecificData: {
0: { viewportData0: 'vtkData0' },
1: { viewportData1: 'vtkData1' },
2: { viewportData2: 'vtkData2' },
},
layout: {
viewports: [{ plugin: 'vtk' }, { plugin: 'vtk' }, { plugin: 'vtk' }],
},
activeViewportIndex: 0,
};
const action = {
type: types.SET_VIEWPORT_LAYOUT_AND_DATA,
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
viewportSpecificData: {
0: { viewportData0: 'cornerstoneData0' },
},
};
const expectedState = {
numRows: 1,
numColumns: 1,
viewportSpecificData: {
0: { viewportData0: 'cornerstoneData0' },
},
layout: {
viewports: [{ plugin: 'cornerstone' }],
},
activeViewportIndex: 0,
};
Reducer(reducer)
.withState(initialState)
.expect(action)
.toReturnState(expectedState);
});
it('should handle SET_VIEWPORT when we only set one viewport specific data', () => {
const initialState = {
numRows: 1,
numColumns: 2,
viewportSpecificData: {
0: { viewportData0: 'data0' },
1: { viewportData1: 'data1' },
},
layout: {
viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }],
},
activeViewportIndex: 0,
};
const action = {
type: types.SET_VIEWPORT,
viewportIndex: 1,
viewportSpecificData: {
viewportData1: 'NEWdata1',
},
};
const expectedState = {
numRows: 1,
numColumns: 2,
viewportSpecificData: {
0: { viewportData0: 'data0' },
1: { viewportData1: 'NEWdata1' },
},
layout: {
viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }],
},
activeViewportIndex: 0,
};
Reducer(reducer)
.withState(initialState)
.expect(action)
.toReturnState(expectedState);
});
// If there were previous keys, this would have
// "merge" behavior, not a clear & set
// May be worth another test?
it('should handle SET_VIEWPORT', () => {
const viewportToSet = 0;
const setViewportAction = {
@@ -40,19 +40,21 @@ export default class RetrieveMetadataLoader {
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
} catch (e) { }
} catch (e) {
throw e;
}
}
if (loaders.next().done && !result) {
throw 'cant find data';
throw new Error('RetrieveMetadataLoader failed');
}
return result;
}
// Methods to be overwrite
async configLoad() { }
async preLoad() { }
async load(preLoadData) { }
async posLoad(loadData) { }
async configLoad() {}
async preLoad() {}
async load(preLoadData) {}
async posLoad(loadData) {}
}
@@ -1,3 +1,4 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb';
const WADOProxy = {
@@ -21,6 +22,43 @@ function parseFloatArray(obj) {
return result;
}
/**
* Simple cache schema for retrieved color palettes.
*/
const paletteColorCache = {
count: 0,
maxAge: 24 * 60 * 60 * 1000, // 24h cache?
entries: {},
isValidUID: function(paletteUID) {
return typeof paletteUID === 'string' && paletteUID.length > 0;
},
get: function(paletteUID) {
let entry = null;
if (this.entries.hasOwnProperty(paletteUID)) {
entry = this.entries[paletteUID];
// check how the entry is...
if (Date.now() - entry.time > this.maxAge) {
// entry is too old... remove entry.
delete this.entries[paletteUID];
this.count--;
entry = null;
}
}
return entry;
},
add: function(entry) {
if (this.isValidUID(entry.uid)) {
let paletteUID = entry.uid;
if (this.entries.hasOwnProperty(paletteUID) !== true) {
this.count++; // increment cache entry count...
}
entry.time = Date.now();
this.entries[paletteUID] = entry;
// @TODO: Add logic to get rid of old entries and reduce memory usage...
}
},
};
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration parameters
@@ -299,40 +337,41 @@ async function makeSOPInstance(server, study, instance) {
return sopInstance;
}
/**
* Convert String to ArrayBuffer
*
* @param {String} str Input String
* @return {ArrayBuffer} Output converted ArrayBuffer
*/
function str2ab(str) {
const strLen = str.length;
const bytes = new Uint8Array(strLen);
for (let i = 0; i < strLen; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes.buffer;
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0];
const arrayBufferToPaletteColorLUT = arraybuffer => {
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
if (bits === 16) {
for (let i = 0; i < numLutEntries; i++) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
}
} else {
for (let i = 0; i < numLutEntries; i++) {
lut[i] = byteArray[i];
}
}
@@ -340,7 +379,38 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
if (instance[tag].BulkDataURI) {
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
return dicomWeb
.retrieveBulkData(options)
.then(result => result[0])
.then(arrayBufferToPaletteColorLUT);
} else if (instance[tag].InlineBinary) {
const inlineBinaryData = atob(instance[tag].InlineBinary);
const arraybuf = str2ab(inlineBinaryData);
return arrayBufferToPaletteColorLUT(arraybuf);
}
throw new Error(
'Palette Color LUT was not provided as InlineBinary or BulkDataURI'
);
}
/**
-413
View File
@@ -1,413 +0,0 @@
import DICOMWeb from '../DICOMWeb/';
const WADOProxy = {
convertURL: (url, server) => {
// TODO: Remove all WADOProxy stuff from this file
return url;
},
};
function parseFloatArray(obj) {
const result = [];
if (!obj) {
return result;
}
const objs = obj.split('\\');
for (let i = 0; i < objs.length; i++) {
result.push(parseFloat(objs[i]));
}
return result;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration parameters
* @param {Object} aSopInstance a SOP Instance from which study information will be added
*/
function createStudy(server, aSopInstance) {
// TODO: Pass a reference ID to the server instead of including the URLs here
return {
seriesList: [],
seriesMap: Object.create(null),
seriesLoader: null,
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(aSopInstance['00100010']),
patientId: DICOMWeb.getString(aSopInstance['00100020']),
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
modalities: DICOMWeb.getString(aSopInstance['00080061']),
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
};
}
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
* @param seriesInstanceUid
* @param sopInstanceUid
* @returns {string}
*/
function buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frame
) {
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
frame = frame != null || 1;
return `${baseWadoRsUri}/frames/${frame}`;
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime',
};
if (!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if (
radiopharmaceuticalInfo === undefined ||
!radiopharmaceuticalInfo.Value ||
!radiopharmaceuticalInfo.Value.length
) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(
firstPetRadiopharmaceuticalInfo['00181072']
),
radionuclideTotalDose: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181074']
),
radionuclideHalfLife: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181075']
),
};
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length &&
SourceImageSequence.Value[0]['00081155'].Value
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
async function makeSOPInstance(server, study, instance) {
const { studyInstanceUid } = study;
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = study.seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesInstanceUid,
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
study.seriesMap[seriesInstanceUid] = series;
study.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const sopInstance = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
sopInstance.paletteColorLookupTableUID = palettes.uid;
}
sopInstance.redPaletteColorLookupTableData = palettes.red;
sopInstance.greenPaletteColorLookupTableData = palettes.green;
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(sopInstance);
return sopInstance;
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
let entry;
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
const promises = [r, g, b];
Promise.all(promises).then(args => {
entry = {
red: args[0],
green: args[1],
blue: args[2],
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration parameters
* @param {Object} study The study descriptor to which the given SOP instances will be added
* @param {Array} sopInstanceList A list of SOP instance objects
*/
async function addInstancesToStudy(server, study, sopInstanceList) {
return Promise.all(
sopInstanceList.map(function(sopInstance) {
return makeSOPInstance(server, study, sopInstance);
})
);
}
const createStudyFromSOPInstanceList = async (server, sopInstanceList) => {
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
const firstSopInstance = sopInstanceList[0];
const study = createStudy(server, firstSopInstance);
await addInstancesToStudy(server, study, sopInstanceList);
return study;
}
throw new Error('Failed to create study out of provided SOP instance list');
};
export { createStudyFromSOPInstanceList, addInstancesToStudy };
@@ -67,6 +67,12 @@ function processSingleframe(instances) {
if (instances.length > 2) {
const firstIpp = _getImagePositionPatient(firstImage);
const lastIpp = _getImagePositionPatient(instances[instances.length - 1]);
// We can't reconstruct if we are missing imagePositionPatient values
if (!firstIpp || !lastIpp) {
return { value: false };
}
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstIpp, lastIpp) / (instances.length - 1);
@@ -136,8 +142,13 @@ function _getSpacingIssue(spacing, averageSpacing) {
}
function _getImagePositionPatient(instance) {
return instance
.getTagValue('x00200032')
const tagValue = instance
.getTagValue('x00200032');
if (!tagValue) {
return;
}
return tagValue
.split('\\')
.map(element => Number(element));
}
+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.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.4...@ohif/i18n@0.52.5) (2020-01-30)
### Bug Fixes
* download tool fixes & improvements ([#1235](https://github.com/OHIF/Viewers/issues/1235)) ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6))
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.3...@ohif/i18n@0.52.4) (2019-12-16)
**Note:** Version bump only for package @ohif/i18n
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.52.4",
"version": "0.52.5",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
@@ -0,0 +1,14 @@
{
"emptyFilenameError": "The file name cannot be empty.",
"fileType": "File Type",
"filename": "File Name",
"formTitle": "Please specify the dimensions, filename, and desired type for the output image.",
"imageHeight": "Image height (px)",
"imagePreview": "Image Preview",
"imageWidth": "Image width (px)",
"keepAspectRatio": "Keep aspect ratio",
"loadingPreview": "Loading Image Preview...",
"minHeightError": "The minimum valid height is 100px.",
"minWidthError": "The minimum valid width is 100px.",
"showAnnotations": "Show Annotations"
}
+2
View File
@@ -7,6 +7,7 @@ import Header from './Header.json';
import MeasurementTable from './MeasurementTable.json';
import StudyList from './StudyList.json';
import UserPreferencesModal from './UserPreferencesModal.json';
import ViewportDownloadForm from './ViewportDownloadForm.json';
export default {
'en-US': {
@@ -19,5 +20,6 @@ export default {
MeasurementTable,
StudyList,
UserPreferencesModal,
ViewportDownloadForm,
},
};
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.9](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.8...@ohif/ui@1.1.9) (2020-01-30)
### Bug Fixes
* download tool fixes & improvements ([#1235](https://github.com/OHIF/Viewers/issues/1235)) ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6))
## [1.1.8](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.7...@ohif/ui@1.1.8) (2020-01-08)
### Bug Fixes
* measurements panel css and delete button visibility ([#1352](https://github.com/OHIF/Viewers/issues/1352)) ([7ab0bbb](https://github.com/OHIF/Viewers/commit/7ab0bbb32581dcba16ee16b49b92406e2856ac76))
## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.6...@ohif/ui@1.1.7) (2019-12-20)
**Note:** Version bump only for package @ohif/ui
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "1.1.7",
"version": "1.1.9",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -1,9 +1,16 @@
import React, { useEffect, useState, createRef } from 'react';
import React, {
useRef,
useCallback,
useEffect,
useState,
createRef,
} from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import './ViewportDownloadForm.styl';
import { TextInput, Select } from '@ohif/ui';
import { TextInput, Select, Icon } from '@ohif/ui';
import classnames from 'classnames';
const FILE_TYPE_OPTIONS = [
{
@@ -17,6 +24,7 @@ const FILE_TYPE_OPTIONS = [
];
const DEFAULT_FILENAME = 'image';
const REFRESH_VIEWPORT_TIMEOUT = 1000;
const ViewportDownloadForm = ({
activeViewport,
@@ -32,7 +40,7 @@ const ViewportDownloadForm = ({
maximumSize,
canvasClass,
}) => {
const [t] = useTranslation('Buttons');
const [t] = useTranslation('ViewportDownloadForm');
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
@@ -44,6 +52,12 @@ const ViewportDownloadForm = ({
const [showAnnotations, setShowAnnotations] = useState(true);
const [keepAspect, setKeepAspect] = useState(true);
const [aspectMultiplier, setAspectMultiplier] = useState({
width: 1,
height: 1,
});
const [viewportElement, setViewportElement] = useState();
const [viewportElementDimensions, setViewportElementDimensions] = useState({
width: defaultSize,
@@ -62,41 +76,129 @@ const ViewportDownloadForm = ({
height: defaultSize,
});
// Cornerstone's `enable/disable`
useEffect(() => {
enableViewport(viewportElement);
const [error, setError] = useState({
width: false,
height: false,
filename: false,
});
return () => {
disableViewport(viewportElement);
};
}, [disableViewport, enableViewport, viewportElement]);
const hasError = Object.values(error).includes(true);
useEffect(() => {
const { width, height } = viewportElementDimensions;
const validSize = value => (value >= minimumSize ? value : minimumSize);
const loadAndUpdateViewports = async () => {
await loadImage(activeViewport, viewportElement, width, height);
toggleAnnotations(showAnnotations, viewportElement);
const refreshViewport = useRef(null);
const {
dataUrl,
width: viewportElementWidth,
height: viewportElementHeight,
} = await updateViewportPreview(
viewportElement,
downloadCanvas.ref.current,
fileType
const downloadImage = () => {
downloadBlob(
filename || DEFAULT_FILENAME,
fileType,
viewportElement,
downloadCanvas.ref.current
);
};
/**
* @param {object} event - Input change event
* @param {string} dimension - "height" | "width"
*/
const onDimensionsChange = (event, dimension) => {
const oppositeDimension = dimension === 'height' ? 'width' : 'height';
const sanitizedTargetValue = event.target.value.replace(/\D/, '');
const isEmpty = sanitizedTargetValue === '';
const newDimensions = { ...dimensions };
const updatedDimension = isEmpty
? ''
: Math.min(sanitizedTargetValue, maximumSize);
if (updatedDimension === dimensions[dimension]) {
return;
}
newDimensions[dimension] = updatedDimension;
if (keepAspect && newDimensions[oppositeDimension] !== '') {
newDimensions[oppositeDimension] = Math.round(
newDimensions[dimension] * aspectMultiplier[oppositeDimension]
);
}
setViewportPreview(state => ({
// In current code, keepAspect is always `true`
// And we always start w/ a square width/height
setDimensions(newDimensions);
// Only update if value is non-empty
if (!isEmpty) {
setViewportElementDimensions(newDimensions);
setDownloadCanvas(state => ({
...state,
src: dataUrl,
width: validSize(viewportElementWidth),
height: validSize(viewportElementHeight),
...newDimensions,
}));
}
};
const error_messages = {
width: t('minWidthError'),
height: t('minHeightError'),
filename: t('emptyFilenameError'),
};
const renderErrorHandler = errorType => {
if (!error[errorType]) {
return null;
}
return <div className="input-error">{error_messages[errorType]}</div>;
};
const onKeepAspectToggle = () => {
const { width, height } = dimensions;
const aspectMultiplier = { ...aspectMultiplier };
if (!keepAspect) {
const base = Math.min(width, height);
aspectMultiplier.width = width / base;
aspectMultiplier.height = height / base;
setAspectMultiplier(aspectMultiplier);
}
setKeepAspect(!keepAspect);
};
const validSize = value => (value >= minimumSize ? value : minimumSize);
const loadAndUpdateViewports = useCallback(async () => {
const { width: scaledWidth, height: scaledHeight } = await loadImage(
activeViewport,
viewportElement,
dimensions.width,
dimensions.height
);
toggleAnnotations(showAnnotations, viewportElement);
const scaledDimensions = {
height: validSize(scaledHeight),
width: validSize(scaledWidth),
};
loadAndUpdateViewports();
setViewportElementDimensions(scaledDimensions);
setDownloadCanvas(state => ({
...state,
...scaledDimensions,
}));
const {
dataUrl,
width: viewportElementWidth,
height: viewportElementHeight,
} = await updateViewportPreview(
viewportElement,
downloadCanvas.ref.current,
fileType
);
setViewportPreview(state => ({
...state,
src: dataUrl,
width: validSize(viewportElementWidth),
height: validSize(viewportElementHeight),
}));
}, [
activeViewport,
viewportElement,
@@ -111,76 +213,96 @@ const ViewportDownloadForm = ({
viewportElementDimensions,
]);
/**
* @param {object} event - Input change event
* @param {string} dimension - "height" | "width"
*/
const onDimensionsChange = (event, dimension) => {
const sanitizedTargetValue = event.target.value.replace(/\D/, '');
const isEmpty = sanitizedTargetValue === '';
const updatedDimension = isEmpty
? ''
: Math.min(sanitizedTargetValue, maximumSize);
useEffect(() => {
enableViewport(viewportElement);
if (updatedDimension === dimensions.width) {
return;
return () => {
disableViewport(viewportElement);
};
}, [disableViewport, enableViewport, viewportElement]);
useEffect(() => {
if (refreshViewport.current !== null) {
clearTimeout(refreshViewport.current);
}
// In current code, keepAspect is always `true`
// And we always start w/ a square width/height
setDimensions({
width: updatedDimension,
height: updatedDimension,
});
refreshViewport.current = setTimeout(() => {
refreshViewport.current = null;
loadAndUpdateViewports();
}, REFRESH_VIEWPORT_TIMEOUT);
}, [
activeViewport,
viewportElement,
showAnnotations,
dimensions,
loadImage,
toggleAnnotations,
updateViewportPreview,
fileType,
downloadCanvas.ref,
minimumSize,
maximumSize,
]);
// Only update if value is non-empty
if (!isEmpty) {
setViewportElementDimensions({
height: updatedDimension,
width: updatedDimension,
});
setDownloadCanvas(state => ({
...state,
height: updatedDimension,
width: updatedDimension,
}));
}
};
useEffect(() => {
const { width, height } = dimensions;
const hasError = {
width: width < minimumSize,
height: height < minimumSize,
filename: !filename,
};
const downloadImage = () => {
downloadBlob(
filename || DEFAULT_FILENAME,
fileType,
viewportElement,
downloadCanvas.ref.current
);
};
setError({ ...hasError });
}, [dimensions, filename, minimumSize]);
return (
<div className="ViewportDownloadForm">
<div className="title">
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
)}
</div>
<div className="title">{t('formTitle')}</div>
<div className="file-info-container" data-cy="file-info-container">
<div className="col">
<div className="width">
<TextInput
data-cy="image-width"
value={dimensions.width}
label={t('Image width (px)')}
onChange={evt => onDimensionsChange(evt, 'height')}
/>
<div className="dimension-wrapper">
<div className="dimensions">
<div className="width">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={dimensions.width}
label={t('imageWidth')}
onChange={evt => onDimensionsChange(evt, 'width')}
data-cy="image-width"
/>
{renderErrorHandler('width')}
</div>
<div className="height">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={dimensions.height}
label={t('imageHeight')}
onChange={evt => onDimensionsChange(evt, 'height')}
data-cy="image-height"
/>
{renderErrorHandler('height')}
</div>
</div>
<div className="height">
<TextInput
data-cy="image-height"
value={dimensions.height}
label={t('Image height (px)')}
onChange={evt => onDimensionsChange(evt, 'width')}
/>
<div className="keep-aspect-wrapper">
<button
id="keep-aspect"
className={classnames(
'form-button btn',
keepAspect ? 'active' : ''
)}
data-cy="keep-aspect"
alt={t('keepAspectRatio')}
onClick={onKeepAspectToggle}
>
<Icon
name={keepAspect ? 'link' : 'unlink'}
alt={keepAspect ? 'Dismiss Aspect' : 'Keep Aspect'}
/>
</button>
</div>
</div>
@@ -191,9 +313,10 @@ const ViewportDownloadForm = ({
data-cy="file-name"
value={filename}
onChange={event => setFilename(event.target.value)}
label={t('File name')}
label={t('filename')}
id="file-name"
/>
{renderErrorHandler('filename')}
</div>
<div className="file-type">
<Select
@@ -201,7 +324,7 @@ const ViewportDownloadForm = ({
data-cy="file-type"
onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS}
label={t('File type')}
label={t('fileType')}
/>
</div>
</div>
@@ -217,7 +340,7 @@ const ViewportDownloadForm = ({
checked={showAnnotations}
onChange={event => setShowAnnotations(event.target.checked)}
/>
{t('Show Annotations')}
{t('showAnnotations')}
</label>
</div>
</div>
@@ -245,15 +368,23 @@ const ViewportDownloadForm = ({
></canvas>
</div>
<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>
{viewportPreview.src ? (
<div className="preview" data-cy="image-preview">
<div className="preview-header"> {t('imagePreview')}</div>
<img
className="viewport-preview"
src={viewportPreview.src}
alt={t('imagePreview')}
data-cy="image-preview"
data-cy="viewport-preview-img"
/>
</div>
) : (
<div className="loading-image">
<Icon name="circle-notch" className="icon-spin" />
{t('loadingPreview')}
</div>
)}
<div className="actions">
<div className="action-cancel">
@@ -263,16 +394,17 @@ const ViewportDownloadForm = ({
className="btn btn-danger"
onClick={onClose}
>
{t('Cancel')}
{t('Buttons:Cancel')}
</button>
</div>
<div className="action-save">
<button
disabled={hasError}
onClick={downloadImage}
className="btn btn-primary"
data-cy="download-btn"
>
{t('Download')}
{t('Buttons:Download')}
</button>
</div>
</div>
@@ -10,6 +10,9 @@
input, select
max-height: 30px;
#keep-aspect svg
margin-top: 3px;
.title
margin: 0;
font-weight: bold;
@@ -17,85 +20,117 @@
.file-info-container
display: flex;
flex-direction: row;
justify-content: space-around;
margin: 20px 0;
border-radius: 5px;
padding: 20px 10px 0;
background-color: #16202b;
.form-control.input-ohif
padding: 6px 12px;
padding: 20px 10px;
background-color: var(--ui-gray-dark);
@media screen and (max-width: 1023px)
flex-direction: column;
align-items: flex-start;
.col
flex-grow: 1;
.width,
.height,
.file-name,
.file-type
height: 56px;
.input-ohif
margin-left: 15px;
.input-ohif
margin: 0 5px;
.file-name,
.file-type
.select-ohif, .input-ohif
width: 170px;
.input-ohif-label, .select-ohif-label
width: 90px;
display: inline-block;
@media screen and (max-width: 1023px)
margin-left: 0;
margin-top: 5px;
width: 100%;
width: 120px;
.file-type
.select-ohif
margin-left: 17px;
.dimension-wrapper
display: flex;
flex-direction: row;
.dimensions
display: flex;
flex-direction: column;
.input-ohif-label
width: 120px;
display: inline-block;
.input-ohif
@media screen and (max-width: 1023px)
margin-left: 0;
width: 100%;
width: 170px;
.show-annotations
font-weight: bold;
line-height: 30px;
input
margin-right: 7px;
vertical-align: middle;
label
display: flex;
justify-content: center;
align-items: center;
.keep-aspect-wrapper
display: flex;
justify-content: center;
align-items: center;
padding: 0 10px;
height: 86px;
.show-annotations
font-weight: bold;
line-height: 30px;
input
margin-right: 7px;
vertical-align: middle;
label
display: flex;
justify-content: center;
align-items: center;
.loading-image
height: 580px;
display: flex;
justify-content: center;
align-items: center;
color: var(--active-color);
font-size: 20px;
.icon-spin
margin-right: 15px;
.preview
display: flex;
flex-direction column;
height: fit-content;
background-color: #16202b;
width: fit-content;
flex-direction: column;
background-color: var(--ui-gray-dark);
padding: 10px;
border-radius: 5px;
align-self: center;
margin-bottom: 20px;
@media screen and (max-width: 1023px)
width: 100%;
justify-content: center;
align-items: center;
justify-content: flex-start;
align-items: center;
height: 580px;
.viewport-preview
max-height: 512px;
max-width: 512px;
h4
.preview-header
width: 100%;
text-align center;
font-size: 1.3em;
margin: 0 0 10px;
.preview-container
width: auto;
height: 100%;
max-height: 400px;
object-fit contain;
.actions
display: flex;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
.action-cancel
margin: 0 20px;
.actions-save
margin: 0 0 0 10px;
margin-top: 20px;
.btn
margin: 0 10px;
.input-error
font-size: 12px;
color: red;
text-align: center;
margin: 3px 0;
.modal-dialog
height: 100%;
@@ -1,5 +1,6 @@
.measurementItem
.rowActions
margin-left: -1px;
background-color: var(--ui-gray-darker)
height: 0
overflow: hidden
@@ -28,7 +29,6 @@
visibility: visible
.measurementLocation
margin-left: 9px;
margin-top: 9px;
overflow: hidden;
max-width: calc(100% - 50px);
@@ -39,7 +39,6 @@
.measurementDisplayText
display: inline-block;
margin-top: 9px;
margin-left: 9px;
padding-left: 9px;
width: 90px;
border-left: 1px solid var(--text-secondary-color);
@@ -10,7 +10,10 @@ export class TableListItem extends Component {
children: PropTypes.node,
itemClass: PropTypes.string,
itemIndex: PropTypes.number,
itemKey: PropTypes.oneOfType(['number', 'string']),
itemKey: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
onItemClick: PropTypes.func.isRequired,
};
@@ -28,7 +28,7 @@
transition(all 0.3s ease)
font-size: 14px
font-weight: 400
margin-right: 5px
margin-right: 6px
padding: 10px
text-align: center
-webkit-box-sizing: initial;
@@ -38,9 +38,8 @@
.itemContent
flex: 1
color: var(--text-secondary-color)
width: calc(100% - 30px);
width: calc(100% - 51px);
font-weight: 400
margin-left: 9px;
margin-top: 9px;
.warning-icon
+4
View File
@@ -1,6 +1,10 @@
.icon-pulse
fa-spin 1s infinite steps(8)
.icon-spin {
animation: spin 2s linear infinite;
}
@keyframes fa-spin{
0%{ transform:rotate(0deg) }
to{ transform:rotate(1turn) }
+2
View File
@@ -80,6 +80,7 @@ import thLarge from './icons/th-large.svg';
import thList from './icons/th-list.svg';
import times from './icons/times.svg';
import trash from './icons/trash.svg';
import unlink from './icons/unlink.svg';
import user from './icons/user.svg';
import youtube from './icons/youtube.svg';
@@ -158,6 +159,7 @@ const ICONS = {
rotate,
'rotate-right': rotateRight,
trash,
unlink,
'exclamation-circle': exclamationCircle,
link,
'exclamation-triangle': exclamationTriangle,
@@ -0,0 +1,11 @@
<svg
xmlns="http://www.w3.org/2000/svg"
aria-labelledby="unlink"
viewBox="0 0 512 512"
width="1em"
height="1em"
fill="currentColor"
>
<title id="title">Unlink</title>
<path d="M304.083 388.936c4.686 4.686 4.686 12.284 0 16.971l-65.057 65.056c-54.709 54.711-143.27 54.721-197.989 0-54.713-54.713-54.719-143.27 0-197.989l65.056-65.057c4.686-4.686 12.284-4.686 16.971 0l22.627 22.627c4.686 4.686 4.686 12.284 0 16.971L81.386 311.82c-34.341 34.341-33.451 88.269.597 120.866 32.577 31.187 84.788 31.337 117.445-1.32l65.057-65.056c4.686-4.686 12.284-4.686 16.971 0l22.627 22.626zm-56.568-243.245l64.304-64.304c34.346-34.346 88.286-33.453 120.882.612 31.18 32.586 31.309 84.785-1.335 117.43l-65.056 65.057c-4.686 4.686-4.686 12.284 0 16.971l22.627 22.627c4.686 4.686 12.284 4.686 16.971 0l65.056-65.057c54.711-54.709 54.721-143.271 0-197.99-54.71-54.711-143.27-54.72-197.989 0l-65.057 65.057c-4.686 4.686-4.686 12.284 0 16.971l22.627 22.627c4.685 4.685 12.283 4.685 16.97-.001zm238.343 362.794l22.627-22.627c4.686-4.686 4.686-12.284 0-16.971L43.112 3.515c-4.686-4.686-12.284-4.686-16.971 0L3.515 26.142c-4.686 4.686-4.686 12.284 0 16.971l465.373 465.373c4.686 4.686 12.284 4.686 16.97-.001z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+12 -12
View File
@@ -29,18 +29,18 @@ class Select extends Component {
render() {
return (
<div className="select-ohif-container">
<label className="select-ohif-label" htmlFor={this.id}>
{this.props.label}
<select className="form-control select-ohif" {...this.props}>
{this.props.options.map(({ key, value }) => {
return (
<option key={key} value={value}>
{key}
</option>
);
})}
</select>
</label>
{this.props.label && (
<label className="select-ohif-label" htmlFor={this.id}>{this.props.label}</label>
)}
<select className="form-control select-ohif" {...this.props}>
{this.props.options.map(({ key, value }) => {
return (
<option key={key} value={value}>
{key}
</option>
);
})}
</select>
</div>
);
}
+10 -10
View File
@@ -14,7 +14,7 @@ class TextInput extends React.Component {
PropTypes.number
]),
id: PropTypes.string,
label:PropTypes.string,
label: PropTypes.string,
type: PropTypes.string,
};
@@ -28,15 +28,15 @@ class TextInput extends React.Component {
render() {
return (
<div className="input-ohif-container">
<label className="input-ohif-label" htmlFor={this.props.id}>
{this.props.label}
<input
type={this.props.type}
id={this.props.id}
className="form-control input-ohif"
{...this.props}
/>
</label>
{this.props.label && (
<label className="input-ohif-label" htmlFor={this.props.id}>{this.props.label}</label>
)}
<input
type={this.props.type}
id={this.props.id}
className="form-control input-ohif"
{...this.props}
/>
</div>
);
}
+4 -1
View File
@@ -10,7 +10,6 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');
// const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
@@ -27,6 +26,7 @@ const PUBLIC_URL = process.env.PUBLIC_URL || '/';
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
const PROXY_TARGET = process.env.PROXY_TARGET;
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
module.exports = (env, argv) => {
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
@@ -34,6 +34,9 @@ module.exports = (env, argv) => {
const hasProxy = PROXY_TARGET && PROXY_DOMAIN;
const mergedConfig = merge(baseConfig, {
entry: {
app: ENTRY_TARGET,
},
output: {
path: DIST_DIR,
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
+199
View File
@@ -3,6 +3,205 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.5.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.5.0...@ohif/viewer@3.5.1) (2020-02-07)
**Note:** Version bump only for package @ohif/viewer
# [3.5.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.14...@ohif/viewer@3.5.0) (2020-02-06)
### Features
* lesion-tracker extension ([#1420](https://github.com/OHIF/Viewers/issues/1420)) ([73e4409](https://github.com/OHIF/Viewers/commit/73e440968ce4699d081a9c9f2d21dd68095b3056))
## [3.4.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.13...@ohif/viewer@3.4.14) (2020-02-06)
**Note:** Version bump only for package @ohif/viewer
## [3.4.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.12...@ohif/viewer@3.4.13) (2020-01-30)
**Note:** Version bump only for package @ohif/viewer
## [3.4.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.11...@ohif/viewer@3.4.12) (2020-01-30)
**Note:** Version bump only for package @ohif/viewer
## [3.4.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.10...@ohif/viewer@3.4.11) (2020-01-30)
### Bug Fixes
* download tool fixes & improvements ([#1235](https://github.com/OHIF/Viewers/issues/1235)) ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6))
## [3.4.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.9...@ohif/viewer@3.4.10) (2020-01-28)
**Note:** Version bump only for package @ohif/viewer
## [3.4.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.8...@ohif/viewer@3.4.9) (2020-01-28)
**Note:** Version bump only for package @ohif/viewer
## [3.4.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.7...@ohif/viewer@3.4.8) (2020-01-28)
**Note:** Version bump only for package @ohif/viewer
## [3.4.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.6...@ohif/viewer@3.4.7) (2020-01-28)
**Note:** Version bump only for package @ohif/viewer
## [3.4.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.5...@ohif/viewer@3.4.6) (2020-01-28)
**Note:** Version bump only for package @ohif/viewer
## [3.4.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.4...@ohif/viewer@3.4.5) (2020-01-27)
**Note:** Version bump only for package @ohif/viewer
## [3.4.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.3...@ohif/viewer@3.4.4) (2020-01-27)
**Note:** Version bump only for package @ohif/viewer
## [3.4.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.2...@ohif/viewer@3.4.3) (2020-01-24)
**Note:** Version bump only for package @ohif/viewer
## [3.4.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.1...@ohif/viewer@3.4.2) (2020-01-17)
**Note:** Version bump only for package @ohif/viewer
## [3.4.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.0...@ohif/viewer@3.4.1) (2020-01-15)
### Bug Fixes
* 🐛 Metadata is being mistakenly purged ([#1360](https://github.com/OHIF/Viewers/issues/1360)) ([b9a66d4](https://github.com/OHIF/Viewers/commit/b9a66d44241f2896ef184511287fb4984671e16d)), closes [#1326](https://github.com/OHIF/Viewers/issues/1326)
# [3.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.8...@ohif/viewer@3.4.0) (2020-01-14)
### Features
* Custom Healthcare API endpoint ([#1367](https://github.com/OHIF/Viewers/issues/1367)) ([a5d6bc6](https://github.com/OHIF/Viewers/commit/a5d6bc6a51784ed3a8a40d4ae773de9099f116b9))
## [3.3.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.7...@ohif/viewer@3.3.8) (2020-01-10)
**Note:** Version bump only for package @ohif/viewer
## [3.3.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.6...@ohif/viewer@3.3.7) (2020-01-08)
**Note:** Version bump only for package @ohif/viewer
## [3.3.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.5...@ohif/viewer@3.3.6) (2020-01-07)
**Note:** Version bump only for package @ohif/viewer
## [3.3.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.4...@ohif/viewer@3.3.5) (2020-01-06)
**Note:** Version bump only for package @ohif/viewer
## [3.3.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.3...@ohif/viewer@3.3.4) (2019-12-30)
**Note:** Version bump only for package @ohif/viewer
## [3.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.2...@ohif/viewer@3.3.3) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
## [3.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.1...@ohif/viewer@3.3.2) (2019-12-20)
**Note:** Version bump only for package @ohif/viewer
+2 -1
View File
@@ -5,5 +5,6 @@
"defaultCommandTimeout": 10000,
"requestTimeout": 10000,
"responseTimeout": 10000,
"projectId": "4oe38f"
"projectId": "4oe38f",
"video": false
}
Binary file not shown.
@@ -47,7 +47,6 @@ describe('OHIF Cornerstone Hotkeys', () => {
cy.get('body').type('I');
// Visual comparison
cy.screenshot('Hotkey I - Should Invert Image');
cy.percyCanvasSnapshot('Hotkey I - Should Invert Image');
});
it('checks if hotkeys "+", "-" and "=" can zoom in, out and fit to viewport', () => {
@@ -77,7 +76,6 @@ describe('OHIF Cornerstone Hotkeys', () => {
// Visual comparison to make sure the 'inverted' image was reset
cy.screenshot('Hotkey SPACEBAR - Should Reset Image');
cy.percyCanvasSnapshot('Hotkey SPACEBAR - Should Reset Image');
});
it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => {
@@ -119,7 +119,6 @@ describe('OHIF Cornerstone Toolbar', () => {
// Visual comparison
cy.screenshot('Pan tool moved the image inside the viewport');
cy.percyCanvasSnapshot('Pan tool moved the image inside the viewport');
});
it('checks if Length annotation can be added on viewport and on measurements panel', () => {
@@ -471,7 +470,6 @@ describe('OHIF Cornerstone Toolbar', () => {
// Visual comparison
cy.screenshot('Invert tool - Should Invert Canvas');
cy.percyCanvasSnapshot('Invert tool - Should Invert Canvas');
});
it('check if Rotate tool will change the image orientation in the viewport', () => {
@@ -487,7 +485,6 @@ describe('OHIF Cornerstone Toolbar', () => {
// Visual comparison
cy.screenshot('Rotate tool - Should Rotate Image to Right');
cy.percyCanvasSnapshot('Rotate tool - Should Rotate Image to Right');
});
it('check if Flip H tool will flip the image horizontally in the viewport', () => {
@@ -501,7 +498,6 @@ describe('OHIF Cornerstone Toolbar', () => {
// Visual comparison
cy.screenshot('Flip H tool - Should Flip Image on Y axis');
cy.percyCanvasSnapshot('Flip H tool - Should Flip Image on Y axis');
});
it('check if Flip V tool will flip the image vertically in the viewport', () => {
@@ -515,6 +511,5 @@ describe('OHIF Cornerstone Toolbar', () => {
// Visual comparison
cy.screenshot('Flip V tool - Should Flip Image on X axis');
cy.percyCanvasSnapshot('Flip V tool - Should Flip Image on X axis');
});
});
@@ -16,20 +16,18 @@ describe('OHIF Download Snapshot File', () => {
.click();
});
it('checks displayed information for Tablet experience', function() {
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() {
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')
@@ -54,7 +52,7 @@ describe('OHIF Download Snapshot File', () => {
.should('be.visible');
});
it('cancel changes on download modal', function() {
it('cancel changes on download modal', function () {
//Change Image Width, Filename and File Type
cy.get('[data-cy="image-width"]')
.clear()
@@ -93,7 +91,7 @@ describe('OHIF Download Snapshot File', () => {
// //Check error message
// });
it('checks if "Show Annotations" checkbox will display annotations', function() {
it('checks if "Show Annotations" checkbox will display annotations', function () {
// Close modal that is initially opened
cy.get('[data-cy="close-button"]').click();
@@ -109,7 +107,6 @@ describe('OHIF Download Snapshot File', () => {
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')
@@ -0,0 +1,31 @@
describe('OHIF Load Local File', () => {
beforeEach(() => {
cy.visit('/local');
});
it('checks if user can navigate to /local', () => {
cy.get('.drag-drop-contents').should(
'contain',
'Drag and Drop DICOM files here to load them in the Viewer'
);
});
it('loads an invalid file and verify if viewer is empty', () => {
const fileName = 'example.json';
cy.fixture(fileName).then(fileContent => {
cy.get('input[type=file]')
.first()
.upload({
fileContent,
fileName,
mimeType: 'application/json',
});
});
//Verify if there is no thumbnail visible
cy.expectMinimumThumbnails(0);
//Verify if DOC thumnails is displayed and load it on viewport
cy.get('[data-cy="thumbnail-list"]').should('not.exist');
});
});
@@ -6,7 +6,9 @@ describe('OHIF Routes', function() {
});
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.visit(
'/viewer?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/JSON/PTCTStudy.json'
);
cy.server();
cy.route('GET', '**/PTCTStudy/**').as('getPTCTStudy');
@@ -15,8 +17,5 @@ describe('OHIF Routes', function() {
cy.get('@getPTCTStudy').should($route => {
expect($route.status).to.be.eq(200);
});
cy.screenshot();
cy.percyCanvasSnapshot('PT/CT json study route');
});
});
@@ -4,9 +4,6 @@ describe('OHIFStandaloneViewer', () => {
});
it('loads route with at least 2 rows', () => {
cy.screenshot('Study List');
cy.percyCanvasSnapshot('Study List');
cy.get('[data-cy="study-list-results"] tr')
.its('length')
.should('be.gt', 2);
@@ -1,14 +1,14 @@
//We are keeping the hardcoded results values for the study list tests
//this is intended to be running in a controled docker environment with test data.
describe('OHIF Study List', function () {
context('Desktop resolution', function () {
beforeEach(function () {
describe('OHIF Study List', function() {
context('Desktop resolution', function() {
beforeEach(function() {
cy.viewport(1750, 720);
cy.openStudyList();
cy.initStudyListAliasesOnDesktop();
});
it('searches Patient Name with exact string', function () {
it('searches Patient Name with exact string', function() {
cy.get('@patientName').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@@ -18,7 +18,7 @@ describe('OHIF Study List', function () {
});
});
it('searches MRN with exact string', function () {
it('searches MRN with exact string', function() {
cy.get('@MRN').type('ProstateX-0000');
//Wait result list to be displayed
cy.waitStudyList();
@@ -28,7 +28,7 @@ describe('OHIF Study List', function () {
});
});
it('searches Accession with exact string', function () {
it('searches Accession with exact string', function() {
cy.get('@accessionNumber').type('fpcben98890');
//Wait result list to be displayed
cy.waitStudyList();
@@ -38,17 +38,17 @@ describe('OHIF Study List', function () {
});
});
it('searches Modality with camel case', function () {
it('searches Modality with camel case', function() {
cy.get('@modalities').type('Mr');
//Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult').should($list => {
expect($list.length).to.be.eq(18);
expect($list.length).to.be.eq(16);
expect($list).to.contain('MR');
});
});
it('searches Description with exact string', function () {
it('searches Description with exact string', function() {
cy.get('@studyDescription').type('CHEST');
//Wait result list to be displayed
cy.waitStudyList();
@@ -58,7 +58,7 @@ describe('OHIF Study List', function () {
});
});
it('changes rows per page and checks the study count', function () {
it('changes rows per page and checks the study count', function() {
//Show rows per page options
const pageRows = [25, 50, 100];
@@ -100,14 +100,14 @@ describe('OHIF Study List', function () {
// });
});
context('Tablet resolution', function () {
beforeEach(function () {
context('Tablet resolution', function() {
beforeEach(function() {
cy.viewport(1000, 660);
cy.openStudyList();
cy.initStudyListAliasesOnTablet();
});
it('searches Patient Name with exact string', function () {
it('searches Patient Name with exact string', function() {
cy.get('@patientNameOrMRN').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@@ -117,7 +117,7 @@ describe('OHIF Study List', function () {
});
});
it('searches MRN with with exact string', function () {
it('searches MRN with with exact string', function() {
cy.get('@patientNameOrMRN').type('ProstateX-0000');
//Wait result list to be displayed
cy.waitStudyList();
@@ -127,17 +127,17 @@ describe('OHIF Study List', function () {
});
});
it('searches Modality with exact string', function () {
it('searches Modality with exact string', function() {
cy.get('@accessionModalityDescription').type('MR');
//Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult').should($list => {
expect($list.length).to.be.eq(18);
expect($list.length).to.be.eq(16);
expect($list).to.contain('MR');
});
});
it('searches Accession with exact string', function () {
it('searches Accession with exact string', function() {
cy.get('@accessionModalityDescription').type('fpcben98890');
//Wait result list to be displayed
cy.waitStudyList();
@@ -147,7 +147,7 @@ describe('OHIF Study List', function () {
});
});
it('searches Description with exact string', function () {
it('searches Description with exact string', function() {
cy.get('@accessionModalityDescription').type('CHEST');
//Wait result list to be displayed
cy.waitStudyList();
@@ -157,7 +157,7 @@ describe('OHIF Study List', function () {
});
});
it('changes rows per page and checks the study count', function () {
it('changes rows per page and checks the study count', function() {
//Show rows per page options
const pageRows = [25, 50, 100];
@@ -12,7 +12,6 @@ describe('OHIF Study Viewer Page', function() {
it('checks if series thumbnails are being displayed', function() {
cy.screenshot('Series Thumbnails - Should Display Thumbnails');
cy.percyCanvasSnapshot('Series Thumbnails - Should Display Thumbnails');
cy.get('[data-cy="thumbnail-list"]')
.its('length')
@@ -293,7 +292,6 @@ describe('OHIF Study Viewer Page', function() {
// Visual comparison
cy.screenshot('About modal - Should display modal');
cy.percyCanvasSnapshot('About modal - Should display modal');
//close modal
cy.get('[data-cy="close-button"]').click();
@@ -24,10 +24,7 @@ describe('OHIF User Preferences', () => {
// Visual comparison
cy.screenshot(
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
'User Preferences Modal - Hotkeys tab initial state in Study List page'
);
cy.get('[data-cy="close-button"]').click();
});
@@ -42,16 +39,10 @@ describe('OHIF User Preferences', () => {
// Visual comparison
cy.screenshot(
'User Preferences Modal - General tab initial state in Study Viewer page'
'User Preferences Modal - General tab initial state in Study List page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - General tab initial state in Study Viewer page'
);
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Set language to Spanish and save
cy.setLanguage('Spanish');
// Header should be translated to Spanish
cy.get('.research-use')
@@ -161,11 +152,8 @@ describe('OHIF User Preferences', () => {
// Go to General tab
cy.get('@userPreferencesGeneralTab').click();
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Set language to Spanish
cy.setLanguage('Spanish');
// Go to Study Viewer page
cy.openStudy('MISTER^MR');
@@ -191,7 +179,9 @@ describe('OHIF User Preferences', () => {
.should('contain.text', 'Preferencias');
// Check if new hotkey is working on viewport
cy.get('body').type('{shift}Q', { release: false });
cy.get('body').type('{shift}Q', {
release: false,
});
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
});
});
@@ -225,10 +215,7 @@ describe('OHIF User Preferences', () => {
// Visual comparison
cy.screenshot(
'User Preferences Modal - Hotkeys tab initial state in Study List page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in Study List page'
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
);
cy.get('[data-cy="close-button"]').click(); //close User Preferences modal
});
@@ -243,16 +230,10 @@ describe('OHIF User Preferences', () => {
// Visual comparison
cy.screenshot(
'User Preferences Modal - General tab initial state in Study List page'
'User Preferences Modal - General tab initial state in Study Viewer page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - General tab initial state in Study List page'
);
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Set language to Spanish
cy.setLanguage('Spanish');
// Header should be translated to Spanish
cy.get('.research-use')
@@ -310,19 +291,11 @@ describe('OHIF User Preferences', () => {
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Set language to Spanish
cy.setLanguage('Spanish');
//Open Preferences again
cy.get('[data-cy="options-menu"]')
.scrollIntoView()
.click();
cy.get('[data-cy="dd-item-menu"]')
.last()
.click();
// Open User Preferences modal
cy.openPreferences();
// Go to general tab
cy.get('@userPreferencesGeneralTab').click();
@@ -437,9 +410,7 @@ describe('OHIF User Preferences', () => {
// Visual comparison
cy.screenshot('Viewport Navigation - 2nd viewport inverted and rotated');
cy.percyCanvasSnapshot(
'Viewport Navigation - 2nd viewport inverted and rotated'
);
// Set 1 viewport layout
cy.setLayout(1, 1);
});
@@ -23,8 +23,5 @@ describe('OHIF Microscopy Extension', () => {
cy.wait(3000); //Waiting for image to render before taking the snapshot
// Visual comparison
cy.screenshot('Microscopy Extension - Should display loaded canvas');
cy.percyCanvasSnapshot(
'Microscopy Extension - Should display loaded canvas'
);
});
});
@@ -23,6 +23,5 @@ describe('OHIF PDF Extension', () => {
// This won't work unless we switch to an extension that renders using `canvas`
// Currently, we rely on the browser's built-in implementation
cy.screenshot('PDF Extension - Should load PDF file');
cy.percyCanvasSnapshot('PDF Extension - Should load PDF file');
});
});
@@ -24,7 +24,6 @@ describe('OHIF VTK Extension', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('CT WB 5.0 B35f')
.drag('.viewport-drop-target')
.wait(1000); // TODO: Once issue #1167 is fixed, the wait command should be removed. (https://github.com/OHIF/Viewers/issues/1167)
//Select 2D MPR button
cy.get('[data-cy="2d mpr"]').click();
@@ -58,9 +57,6 @@ describe('OHIF VTK Extension', () => {
// Visual comparison
cy.screenshot('VTK initial state - Should display toolbar and 3 viewports');
cy.percyCanvasSnapshot(
'VTK initial state - Should display toolbar and 3 viewports'
);
});
it('checks Crosshairs tool', () => {
@@ -76,9 +72,6 @@ describe('OHIF VTK Extension', () => {
cy.screenshot(
"VTK Crosshairs tool - Should display crosshairs' green lines"
);
cy.percyCanvasSnapshot(
"VTK Crosshairs tool - Should display crosshairs' green lines"
);
});
/* TODO: Non-deterministic behavior (const expectedText = 'W: 350 L: -1044';)
@@ -102,7 +95,6 @@ describe('OHIF VTK Extension', () => {
// Visual comparison
cy.screenshot('VTK WWWC tool - Canvas should be bright');
cy.percyCanvasSnapshot('VTK WWWC tool - Canvas should be bright');
}); */
it('checks Rotate tool', () => {
@@ -118,6 +110,5 @@ describe('OHIF VTK Extension', () => {
// Visual comparison
cy.screenshot('VTK Rotate tool - Should rotate image');
cy.percyCanvasSnapshot('VTK Rotate tool - Should rotate image');
});
});
@@ -0,0 +1,29 @@
describe('OHIF Load Local PDF File', () => {
beforeEach(() => {
cy.visit('/local');
});
it('loads a PDF DICOM file and visualize it', () => {
const fileName = 'PDFDICOMfile.pdf';
cy.fixture(fileName).then(fileContent => {
cy.get('input[type=file]')
.first()
.upload({
fileContent,
fileName,
mimeType: 'application/pdf',
});
});
//Verify if 1 thumbnail is visible
cy.expectMinimumThumbnails(1);
//Verify if DOC thumnails is displayed and load it on viewport
cy.get('[data-cy="thumbnail-list"]')
.contains('DOC')
.click({ force: true });
//Verify if PDF document is displayed on the viewport
cy.get('#pdf-canvas-container').should('be.visible');
});
});
@@ -1,15 +1,11 @@
describe('OHIF Save Measurements', function() {
before(() => {
cy.openStudy('Fall 1');
cy.expectMinimumThumbnails(2);
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(5);
});
beforeEach(() => {
// Drags Study thumbnail into viewport
cy.get('[data-cy="thumbnail-list"]:nth-child(1)')
.scrollIntoView()
.drag('.viewport-drop-target');
// Wait image to load on viewport
cy.wait(2000);
@@ -56,7 +52,9 @@ describe('OHIF Save Measurements', function() {
cy.get('.sb-success').should('be.visible');
// Reload the page
cy.reload();
cy.reload()
.wait(1000) //Wait page to load
.expectMinimumThumbnails(2); //wait all thumbnails to load
//Verify that recently added measurement was retrieved
cy.get('@measurementsBtn').click();
@@ -105,8 +103,7 @@ describe('OHIF Save Measurements', function() {
cy.get('.sb-error')
.should('be.visible')
.and('contains.text', 'Error while saving the measurements');
// Close message overlay
cy.get('.sb-closeIcon').click();
// Close Measurements panel
cy.get('@measurementsBtn').click();
});
@@ -135,7 +132,9 @@ describe('OHIF Save Measurements', function() {
.and('contains.text', 'Measurements were saved with success');
// Reload the page
cy.reload();
cy.reload()
.wait(1000) //Wait page to load
.expectMinimumThumbnails(2); //wait all thumbnails to load
//Verify that measurement for unsupported tool was not saved
cy.get('@measurementsBtn').click();
@@ -0,0 +1,33 @@
describe('Visual Regression - OHIF Cornerstone Hotkeys', () => {
before(() => {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(3);
});
beforeEach(() => {
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
cy.resetViewport();
});
it('checks if hotkey "I" can invert the image', () => {
// Hotkey I
cy.get('body').type('I');
// Visual comparison
cy.screenshot('Hotkey I - Should Invert Image');
cy.percyCanvasSnapshot('Hotkey I - Should Invert Image');
});
it('checks if hotkey "SPACEBAR" can reset the image', () => {
// Press multiples hotkeys
cy.get('body').type('V+++I');
// Hotkey SPACEBAR
cy.get('body').type(' ');
// Visual comparison to make sure the 'inverted' image was reset
cy.screenshot('Hotkey SPACEBAR - Should Reset Image');
cy.percyCanvasSnapshot('Hotkey SPACEBAR - Should Reset Image');
});
});
@@ -0,0 +1,84 @@
describe('Visual Regression - OHIF Cornerstone Toolbar', () => {
before(() => {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(5);
});
beforeEach(() => {
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
cy.resetViewport();
});
it('checks if Pan tool will move the image inside the viewport', () => {
//Click on button and vefiry if icon is active on toolbar
cy.get('@panBtn')
.click()
.then($panBtn => {
cy.wrap($panBtn).should('have.class', 'active');
});
cy.get('@viewport')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'bottom', { which: 1 })
.trigger('mouseup', 'bottom');
// Visual comparison
cy.percyCanvasSnapshot('Pan tool moved the image inside the viewport');
});
it('check if Invert tool will change the colors of the image in the viewport', () => {
// Click on More button
cy.get('@moreBtn').click();
// Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
// Click on Invert button
cy.get('[data-cy="invert"]').click();
// Visual comparison
cy.percyCanvasSnapshot('Invert tool - Should Invert Canvas');
});
it('check if Rotate tool will change the image orientation in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay')
.should('be.visible')
.then(() => {
//Click on Rotate button
cy.get('[data-cy="rotate right"]').click({ force: true });
});
// Visual comparison
cy.percyCanvasSnapshot('Rotate tool - Should Rotate Image to Right');
});
it('check if Flip H tool will flip the image horizontally in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip H button
cy.get('[data-cy="flip h"]').click();
// Visual comparison
cy.percyCanvasSnapshot('Flip H tool - Should Flip Image on Y axis');
});
it('check if Flip V tool will flip the image vertically in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip V button
cy.get('[data-cy="flip v"]').click();
// Visual comparison
cy.percyCanvasSnapshot('Flip V tool - Should Flip Image on X axis');
});
});
@@ -0,0 +1,50 @@
describe('Visual Regression - 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.percyCanvasSnapshot('Download Image Modal - Tablet experience');
});
it('checks displayed information for Desktop experience', function() {
// Set Desktop resolution
cy.viewport(1750, 720);
// Visual comparison
cy.percyCanvasSnapshot('Download Image Modal - Desktop experience');
});
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.percyCanvasSnapshot('Download Image Modal - Show Annotations checked');
});
});
@@ -0,0 +1,22 @@
describe('Visual Regression - OHIF Microscopy Extension', () => {
before(() => {
cy.openStudyModality('SM');
cy.expectMinimumThumbnails(6);
});
it('drags and drop a SM thumbnail into viewport', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('SM')
.drag('.viewport-drop-target');
cy.get('.DicomMicroscopyViewer')
.its('length')
.should('be.eq', 1);
cy.wait(3000); //Waiting for image to render before taking the snapshot
// Visual comparison
cy.percyCanvasSnapshot(
'Microscopy Extension - Should display loaded canvas'
);
});
});
@@ -0,0 +1,20 @@
describe('Visual Regression - OHIF PDF Extension', () => {
before(() => {
cy.openStudy('Dummy');
cy.expectMinimumThumbnails(6);
});
it('drags and drop a PDF thumbnail into viewport', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('DOC')
.drag('.viewport-drop-target');
cy.get('.DicomPDFViewport')
.its('length')
.should('be.eq', 1);
// This won't work unless we switch to an extension that renders using `canvas`
// Currently, we rely on the browser's built-in implementation
cy.percyCanvasSnapshot('PDF Extension - Should load PDF file');
});
});
@@ -0,0 +1,92 @@
describe('Visual Regression - OHIF VTK Extension', () => {
before(() => {
cy.openStudy('Bellona');
cy.waitDicomImage();
cy.expectMinimumThumbnails(5);
//Waiting for the desired thumbnail content to be displayed
cy.get('[data-cy="thumbnail-list"]').should($list => {
expect($list).to.contain('Chest 1x10 Soft');
});
// Drag and drop thumbnail into viewport
cy.get('[data-cy="thumbnail-list"]')
.contains('Chest 1x10 Soft')
.drag('.viewport-drop-target');
//Select 2D MPR button
cy.get('[data-cy="2d mpr"]').click();
//Wait Reformatting Images
cy.waitVTKReformatting();
});
beforeEach(() => {
cy.initVTKToolsAliases();
cy.wait(1000); //Wait toolbar to finish loading
});
afterEach(() => {
cy.wait(5000); //wait screen loads back after screenshot
//Select Exit 2D MPR button
cy.get('[data-cy="exit 2d mpr"]').should($btn => {
expect($btn).to.be.visible;
$btn.click();
});
//Select 2D MPR button
cy.get('[data-cy="2d mpr"]').click();
});
it('checks if VTK buttons are displayed on the toolbar', () => {
// Visual comparison
cy.percyCanvasSnapshot(
'VTK initial state - Should display toolbar and 3 viewports'
);
});
it('checks Crosshairs tool', () => {
cy.get('@crosshairsBtn').click();
// Click and Move the mouse inside the viewport
cy.get('[data-cy="viewport-container-0"]')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup');
// Visual comparison
cy.percyCanvasSnapshot(
"VTK Crosshairs tool - Should display crosshairs' green lines"
);
});
it('checks WWWC tool', () => {
cy.get('@wwwcBtn').click();
// Click and Move the mouse inside the viewport
cy.get('[data-cy="viewport-container-0"]')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup', { which: 1 });
// Visual comparison
cy.percyCanvasSnapshot('VTK WWWC tool - Canvas should be bright');
});
it('checks Rotate tool', () => {
cy.get('@rotateBtn').click();
// Click and Move the mouse inside the viewport
cy.get('[data-cy="viewport-container-0"]')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup', { which: 1 });
// Visual comparison
cy.percyCanvasSnapshot('VTK Rotate tool - Should rotate image');
});
});
@@ -0,0 +1,23 @@
//We excluded the tests for '**/studies/**' because the bulk/all of our other study/viewer tests use this route
describe('Visual Regression - 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.percyCanvasSnapshot('PT/CT json study route');
});
});
@@ -0,0 +1,35 @@
describe('Visual Regression - OHIF Study Viewer Page', function() {
before(function() {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(6);
});
beforeEach(function() {
cy.initCommonElementsAliases();
cy.resetViewport();
});
it('checks if series thumbnails are being displayed', function() {
cy.percyCanvasSnapshot('Series Thumbnails - Should Display Thumbnails');
});
it('opens About modal and verify the displayed information', function() {
cy.get('[data-cy="options-menu"]')
.first()
.click();
cy.get('[data-cy="dd-item-menu"]')
.first()
.click();
cy.get('[data-cy="about-modal"]')
.as('aboutOverlay')
.should('be.visible');
// Visual comparison
cy.percyCanvasSnapshot('About modal - Should display modal');
//close modal
cy.get('[data-cy="close-button"]').click();
cy.get('@aboutOverlay').should('not.be.enabled');
});
});
@@ -0,0 +1,188 @@
describe('Visual Regression - OHIF User Preferences', () => {
context('Study List Page', function() {
before(() => {
cy.visit('/');
});
beforeEach(() => {
// Open User Preferences modal
cy.openPreferences();
cy.initPreferencesModalAliases();
});
it('checks displayed information on User Preferences modal', function() {
cy.get('@restoreBtn').scrollIntoView();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in StudyList page'
);
cy.get('[data-cy="close-button"]').click();
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - General tab initial state in StudyList page'
);
// Set language to Spanish and save
cy.setLanguage('Spanish');
// Options menu should be translated
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Opciones')
.click();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Spanish selected in StudyList page'
);
// Close Options menu
cy.get('[data-cy="options-menu"]').click();
});
});
context('Study Viewer Page', function() {
before(() => {
cy.openStudy('MISTER^MR');
cy.expectMinimumThumbnails(5);
});
beforeEach(() => {
cy.initCommonElementsAliases();
cy.resetViewport();
cy.resetUserHoktkeyPreferences();
// Open User Preferences modal
cy.openPreferences();
});
it('checks displayed information on User Preferences modal', function() {
cy.get('@restoreBtn').scrollIntoView();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in StudyViewer page'
);
cy.get('[data-cy="close-button"]').click(); //close User Preferences modal
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - General tab initial state in StudyViewer page'
);
// Set language to Spanish
cy.setLanguage('Spanish');
// Options menu should be translated
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Opciones')
.click();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Spanish selected in StudyViewer page'
);
// Close Options menu
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can restore to default the language selection and application will be in English', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
// Set language to Spanish
cy.setLanguage('Spanish');
// Open User Preferences modal
cy.openPreferences();
// Go to general tab
cy.get('@userPreferencesGeneralTab').click();
cy.get('@restoreBtn')
.scrollIntoView()
.click();
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Options menu should be in English
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Options')
.click();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - English selected in StudyViewer page'
);
// Close Options menu
cy.get('[data-cy="options-menu"]').click();
});
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
// Set new hotkey for 'Next Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Next Image Viewport',
'{shift}{rightarrow}'
);
// Set new hotkey for 'Previous Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Previous Image Viewport',
'{shift}{leftarrow}'
);
// Save new hotkeys
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Set 3 viewports layout
cy.setLayout(3, 1);
cy.waitViewportImageLoading();
// Rotate Right and Invert colors on Viewport #1
cy.get('body').type('RI');
//Move to Next Viewport
cy.get('body').type('{shift}{rightarrow}');
// Rotate Left and Invert colors on Viewport #2
cy.get('body').type('LI');
//Move to Previous Viewport
cy.get('body').type('{shift}{leftarrow}');
// Reset viewport #1 with spacebar hotkey
cy.get('body').type(' ');
// Visual comparison
cy.percyCanvasSnapshot(
'Viewport Navigation - 2nd viewport inverted and rotated'
);
// Set 1 viewport layout
cy.setLayout(1, 1);
});
});
});
@@ -44,6 +44,7 @@ export function initRouteAliases() {
//Creating aliases for VTK tools buttons
export function initVTKToolsAliases() {
cy.get('[data-cy="exit 2d mpr"]').as('exit2dmprBtn');
cy.get('[data-cy="crosshairs"]').as('crosshairsBtn');
cy.get('[data-cy="wwwc"]').as('wwwcBtn');
cy.get('[data-cy="rotate"]').as('rotateBtn');
+2 -1
View File
@@ -1,4 +1,5 @@
import '@percy/cypress';
import 'cypress-file-upload';
import { DragSimulator } from '../helpers/DragSimulator.js';
import {
initCornerstoneToolsAliases,
@@ -168,7 +169,7 @@ Cypress.Commands.add(
);
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
cy.get('[data-cy=thumbnail-list]', { timeout: 10000 }).should($itemList => {
cy.get('[data-cy=thumbnail-list]', { timeout: 20000 }).should($itemList => {
expect($itemList.length >= seriesToWait).to.be.true;
});
});
+12 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "3.3.2",
"version": "3.5.1",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -19,15 +19,17 @@
"scripts": {
"build:package": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.commonjs.js --progress",
"build:viewer": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
"build:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
"build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/pwa/ APP_CONFIG=config/netlify.js QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js",
"build:viewer:demo": "cross-env NODE_ENV=production APP_CONFIG=config/demo.js HTML_TEMPLATE=rollbar.html QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --progress --config .webpack/webpack.pwa.js",
"build:viewer:package": "yarn run build:package",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"dev:viewer": "yarn run dev",
"dev:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"start": "yarn run dev",
"test:e2e": "cypress open",
"test:e2e:ci": "percy exec -- cypress run --record --browser chrome",
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
"test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci",
"test:e2e:serve": "serve -n -l 3000 -s dist",
"test:unit": "jest --watchAll",
@@ -45,20 +47,21 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^2.3.0",
"@ohif/extension-cornerstone": "^2.2.0",
"@ohif/core": "^2.3.9",
"@ohif/extension-cornerstone": "^2.2.2",
"@ohif/extension-dicom-html": "^1.1.0",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.1",
"@ohif/extension-vtk": "^1.2.2",
"@ohif/i18n": "^0.52.4",
"@ohif/ui": "^1.1.7",
"@ohif/extension-lesion-tracker": "^0.1.0",
"@ohif/extension-vtk": "^1.2.15",
"@ohif/i18n": "^0.52.5",
"@ohif/ui": "^1.1.9",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.9.0",
"cornerstone-tools": "^4.12.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
@@ -88,7 +91,7 @@
"cypress": "^3.8.0",
"gh-pages": "2.0.1",
"identity-obj-proxy": "3.0.x",
"lodash": "4.17.11",
"lodash": "4.17.15",
"lodash.clonedeep": "4.5.0"
}
}
+1
View File
@@ -2,6 +2,7 @@ window.config = {
routerBasename: '/',
whiteLabelling: {},
enableGoogleCloudAdapter: true,
healthcareApiEndpoint: 'https://healthcare.googleapis.com/v1beta1',
servers: {
// This is an array, but we'll only use the first entry for now
dicomWeb: [],
@@ -83,6 +83,44 @@ class OHIFStandaloneViewer extends Component {
path="/callback"
render={() => <CallbackPage userManager={userManager} />}
/>
<Route
path="/login"
component={() => {
const queryParams = new URLSearchParams(
this.props.location.search
);
const iss = queryParams.get('iss');
const loginHint = queryParams.get('login_hint');
const targetLinkUri = queryParams.get('target_link_uri');
const oidcAuthority =
appConfig.oidc !== null && appConfig.oidc[0].authority;
if (iss !== oidcAuthority) {
console.error(
'iss of /login does not match the oidc authority'
);
return null;
}
userManager.removeUser().then(() => {
if (targetLinkUri !== null) {
sessionStorage.setItem(
'ohif-redirect-to',
new URL(targetLinkUri).pathname
);
} else {
sessionStorage.setItem('ohif-redirect-to', '/');
}
if (loginHint !== null) {
userManager.signinRedirect({ login_hint: loginHint });
} else {
userManager.signinRedirect();
}
});
return null;
}}
/>
<Route
component={() => {
userManager.getUser().then(user => {
@@ -1,72 +0,0 @@
import OHIF from '@ohif/core';
import React from 'react';
import PluginSwitch from './PluginSwitch.js';
import { commandsManager } from './../App.js';
import { connect } from 'react-redux';
const { setLayout } = OHIF.redux.actions;
const ConnectedPluginSwitch = props => {
return <PluginSwitch {...props} />;
};
const mapStateToProps = state => {
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
return {
activeViewportIndex,
viewportSpecificData,
layout,
};
};
const mapDispatchToProps = dispatch => {
return {
setLayout: data => {
dispatch(setLayout(data));
},
};
};
/*function setSingleLayoutData(originalArray, viewportIndex, data) {
const viewports = originalArray.slice();
const layoutData = Object.assign({}, viewports[viewportIndex], data);
viewports[viewportIndex] = layoutData;
return viewports;
}*/
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
const { activeViewportIndex, viewportSpecificData } = propsFromState;
const { studies } = ownProps;
const { setLayout } = propsFromDispatch;
const mpr = () => {
commandsManager.runCommand('mpr2d');
};
const exitMpr = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
setLayout(layout);
};
return {
mpr,
exitMpr,
activeViewportIndex,
viewportSpecificData,
studies,
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(ConnectedPluginSwitch);
@@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import StandaloneRouting from '../routes/StandaloneRouting';
const mapDispatchToProps = dispatch => {
return {
activateServer: server => {
const action = {
type: 'ACTIVATE_SERVER',
server,
};
dispatch(action);
},
};
};
const ConnectedStandaloneRouting = connect(
null,
mapDispatchToProps
)(StandaloneRouting);
export default ConnectedStandaloneRouting;
@@ -2,23 +2,21 @@ import { connect } from 'react-redux';
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
import OHIF from "@ohif/core";
const {
clearViewportSpecificData
} = OHIF.redux.actions;
const { clearViewportSpecificData } = OHIF.redux.actions;
const isActive = a => a.active === true;
const mapStateToProps = state => {
const mapStateToProps = (state, ownProps) => {
const activeServer = state.servers.servers.find(isActive);
return {
server: activeServer,
server: ownProps.server || activeServer,
};
};
const mapDispatchToProps = dispatch => {
return {
clearViewportSpecificData: () => {
dispatch(clearViewportSpecificData());
}
},
};
};
@@ -1,3 +0,0 @@
.icon-rotate-120 {
transform: rotate(120deg);
}
@@ -1,106 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ToolbarButton } from '@ohif/ui';
import './PluginSwitch.css';
class PluginSwitch extends Component {
static propTypes = {
mpr: PropTypes.func,
activeViewportIndex: PropTypes.number,
viewportSpecificData: PropTypes.object,
studies: PropTypes.array,
exitMpr: PropTypes.func,
};
static defaultProps = {};
constructor(props) {
super(props);
this.state = {
isPlugSwitchOn: false,
label: '2D MPR',
icon: 'cube',
};
}
handleClick = () => {
if (this.state.isPlugSwitchOn) {
this.setState({
isPlugSwitchOn: false,
label: '2D MPR',
icon: 'cube',
});
this.props.exitMpr();
} else {
this.setState({
isPlugSwitchOn: true,
label: 'Exit 2D MPR',
icon: 'times',
});
this.props.mpr();
}
};
render() {
const { label, icon } = this.state;
// Render exit mpr if switched on, otherwise check if mpr button should be displayed.
const shouldRender =
this.state.isPlugSwitchOn || _shouldRenderMpr2DButton.call(this);
return (
<>
{shouldRender && (
<div className="PluginSwitch">
<ToolbarButton
label={label}
icon={icon}
onClick={this.handleClick}
/>
</div>
)}
</>
);
}
}
function _shouldRenderMpr2DButton() {
const { viewportSpecificData, studies, activeViewportIndex } = this.props;
if (!viewportSpecificData[activeViewportIndex]) {
return;
}
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
activeViewportIndex
];
const displaySet = _getDisplaySet(
studies,
studyInstanceUid,
displaySetInstanceUid
);
if (!displaySet) {
return;
}
return displaySet.isReconstructable;
}
function _getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
);
if (!study) {
return;
}
const displaySet = study.displaySets.find(set => {
return set.displaySetInstanceUid === displaySetInstanceUid;
});
return displaySet;
}
export default PluginSwitch;
@@ -16,7 +16,6 @@ import { commandsManager, extensionManager } from './../App.js';
import ConnectedCineDialog from './ConnectedCineDialog';
import ConnectedLayoutButton from './ConnectedLayoutButton';
import ConnectedPluginSwitch from './ConnectedPluginSwitch.js';
class ToolbarRow extends Component {
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
@@ -124,7 +123,6 @@ class ToolbarRow extends Component {
</div>
{buttonComponents}
<ConnectedLayoutButton />
<ConnectedPluginSwitch studies={this.props.studies} />
<div
className="pull-right m-t-1 rm-x-1"
style={{ marginLeft: 'auto' }}
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useContext } from 'react';
import { metadata, studies, utils, log } from '@ohif/core';
import usePrevious from '../customHooks/usePrevious';
import ConnectedViewer from './ConnectedViewer.js';
import PropTypes from 'prop-types';
@@ -128,9 +129,9 @@ const _addSeriesToStudy = (studyMetadata, series) => {
const _updateMetaDataManager = (study, studyMetadata, series) => {
updateMetaDataManager(study, series);
const { studyInstanceUID } = study;
const { studyInstanceUid } = study;
if (!studyMetadataManager.get(studyInstanceUID)) {
if (!studyMetadataManager.get(studyInstanceUid)) {
studyMetadataManager.add(studyMetadata);
}
};
@@ -252,6 +253,7 @@ function ViewerRetrieveStudyData({
.catch(error => {
if (error && !error.isCanceled) {
setError(true);
log.error(error);
}
});
@@ -289,11 +291,13 @@ function ViewerRetrieveStudyData({
.catch(error => {
if (error && !error.isCanceled) {
setError(true);
log.error(error);
}
});
} catch (error) {
if (error) {
setError(true);
log.error(error);
}
}
};
@@ -314,9 +318,15 @@ function ViewerRetrieveStudyData({
}
};
const prevStudyInstanceUids = usePrevious(studyInstanceUids);
useEffect(() => {
studyMetadataManager.purge();
purgeCancellablePromises();
const hasStudyInstanceUidsChanged = !(prevStudyInstanceUids && prevStudyInstanceUids.every(e => studyInstanceUids.includes(e)));
if (hasStudyInstanceUidsChanged) {
studyMetadataManager.purge();
purgeCancellablePromises();
}
}, [studyInstanceUids]);
useEffect(() => {
@@ -18,6 +18,7 @@ const getActiveServer = servers => {
const getServers = (appConfig, project, location, dataset, dicomStore) => {
let servers = [];
if (appConfig.enableGoogleCloudAdapter) {
GoogleCloudApi.urlBase = appConfig.healthcareApiEndpoint;
const pathUrl = GoogleCloudApi.getUrlBaseDicomWeb(
project,
location,
@@ -15,7 +15,11 @@ class GoogleCloudApi {
}
get urlBase() {
return `https://healthcare.googleapis.com/v1beta1`;
return this.healthcareApiEndpoint || 'https://healthcare.googleapis.com/v1beta1';
}
set urlBase(url) {
this.healthcareApiEndpoint = url;
}
get urlBaseProject() {
@@ -0,0 +1,46 @@
/**
* Entry point for development and production PWA builds.
* Packaged (NPM) builds go through `index-umd.js`
*/
import 'regenerator-runtime/runtime';
import App from './App.js';
import React from 'react';
import ReactDOM from 'react-dom';
/**
* EXTENSIONS
* =================
*
* Importing and modifying the extensions our app uses HERE allows us to leverage
* tree shaking and a few other niceties. However, by including them here they become
* "baked in" to the published application.
*
* Depending on your use case/needs, you may want to consider not adding any extensions
* by default HERE, and instead provide them via the extensions configuration key or
* by using the exported `App` component, and passing in your extensions as props using
* the defaultExtensions property.
*/
import OHIFLesionTrackerExtension from '@ohif/extension-lesion-tracker';
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
/*
* Default Settings
*/
let config = {};
if (window) {
config = window.config || {};
}
const appProps = {
config,
defaultExtensions: [OHIFLesionTrackerExtension, OHIFDicomPDFExtension],
};
/** Create App */
const app = React.createElement(App, appProps, null);
/** Render */
ReactDOM.render(app, document.getElementById('root'));
@@ -19,7 +19,7 @@ const DICOMFileLoader = new (class extends FileLoader {
dicomData.meta
);
} catch (e) {
console.log('Error on getting dicom file dataset. It defaults to empty');
console.error('Error reading dicom file', e);
}
// Set imageId on dataset to be consumed later on
dataset.imageId = imageId;
+7 -5
View File
@@ -2,14 +2,16 @@ import React from 'react';
import './NotFound.css';
import { Link } from 'react-router-dom';
export default function NotFound() {
export default function NotFound({ message = 'Sorry, this page does not exist.', showGoBackButton = true }) {
return (
<div className={'not-found'}>
<div>
<h4>Sorry, this page does not exist.</h4>
<h5>
<Link to={'/'}>Go back to the Study List</Link>
</h5>
<h4>{message}</h4>
{showGoBackButton && (
<h5>
<Link to={'/'}>Go back to the Study List</Link>
</h5>
)}
</div>
</div>
);
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more