Compare commits

..
Author SHA1 Message Date
ohif-bot 4f26940005 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.15
 - @ohif/viewer@1.11.2
2019-10-23 07:56:28 +00:00
James Petts 9d08e81aa5 perf: ⚡️ Throttle UI refresh of synced WL value in vtk port (#1070) 2019-10-23 09:45:26 +02:00
ohif-bot 1a539d04c2 chore(release): publish [skip ci]
- @ohif/viewer@1.11.1
2019-10-23 01:12:25 +00:00
Rodrigo Antinarelli 5cc78c867f test: E2E Intermittent Failures (#1071) 2019-10-22 21:04:07 -04:00
ohif-bot cefa480979 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.14
 - @ohif/ui@0.56.0
 - @ohif/viewer@1.11.0
2019-10-22 20:24:10 +00:00
Danny Brown 66537bf1b9 ci: specify our custom executor
fixes broken build
2019-10-22 16:15:09 -04:00
Danny Brown 862d426c71 ci: first attempt at adding a gated docker PR Publish (#1038) (#1041)
* ci: first attempt at adding a gated docker PR Publish (#1038)

* ci: first attempt at adding a gated docker PR Publish

* See if we can use contexts to inject variables after approval

* fix build command

* try alternative context

* docs: pull request template that spells out PR checklist

* change context back to Docker Hub

* Switch optional approval to another workflow

* Make docker builds faster

* Update .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
2019-10-22 15:53:44 -04:00
ladeirarodolfo e62f403fe9 feat: 🎸 Load spinner when selecting gcloud store. Add key on td (#1034)
* feat: 🎸 Load spinner when selecting gcloud store. Add key on td

* feat: 🎸 Code review. Translate loading study status

* feat: 🎸 Code review. Minor code refactoring

* fix: add missing study and index declerations

* test: Improviment on VTK beforeEach hook (#1057)

* feat: 🎸 Some improvements on table cell and react lifecycle

* feat: 🎸 Prevent component update on a not searchData changing

* feat: 🎸 Some cypress improv to pass ci

* feat: 🎸 Code reviewe and also fixing list for gcloud

* feat: 🎸 Code review. Add spinner on first load of store

* feat: 🎸 Code review. Centering loading list spinner icon
2019-10-22 14:40:23 -04:00
Mirna Silva dea0ceab57 test: Adding Percy Snapshot on VTK, PDF and Microscopy tests (#1059)
* Adding Percy Snapshot on VTK, PDF and Microscopy tests

* Small improvement to always click on first search result

* Wait for search result

* Added timouts all over the code to avoid intermitted failures in CI

* Increasing Default Timeout and other configs

* Cleaning up the code

* try force enabling gpu

* Try custom executor with newer version of chrome

* Fix based on review comments

* Increased minimum thumbnails expected on VTK test

* Fix config validation

* Make sure we're using the chrome browser
2019-10-21 14:30:50 -04:00
Danny Brown a6c1e6c717 Revert "fix: MPR initialization (#1062)" (#1064)
This reverts commit b03739428f.
2019-10-19 17:00:51 -04:00
Rodrigo Antinarelli b03739428f fix: MPR initialization (#1062)
* fix: MPR initialization

* small refactoring
2019-10-18 14:29:00 -04:00
ohif-bot 2a52d9f05d chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.13
 - @ohif/viewer@1.10.3
2019-10-18 14:02:07 +00:00
James Petts d5e21bb5bb * chore: 🤖 Update react-vtkjs-viewport and refactor vtk extension
* chore: 🤖 Update react-vtkjs-viewport and refactor vtk extension
2019-10-18 15:53:43 +02:00
30 changed files with 1120 additions and 838 deletions

No files matched your search

+63 -4
View File
@@ -13,7 +13,7 @@ version: 2.1
## ##
orbs: orbs:
codecov: codecov/codecov@1.0.5 codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@1.10.0 cypress: cypress-io/cypress@1.11.0
defaults: &defaults defaults: &defaults
docker: docker:
@@ -198,7 +198,7 @@ jobs:
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh - run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
- run: ~/repo/.circleci/build-and-publish-docs.sh - run: ~/repo/.circleci/build-and-publish-docs.sh
DOCKER_PUBLISH: DOCKER_MASTER_PUBLISH:
<<: *defaults <<: *defaults
steps: steps:
- attach_workspace: - attach_workspace:
@@ -227,6 +227,43 @@ jobs:
docker push ohif/$IMAGE_NAME:latest docker push ohif/$IMAGE_NAME:latest
fi fi
DOCKER_PR_PUBLISH:
<<: *defaults
steps:
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build and push Docker image
command: |
# Remove npm config
rm -f ./.npmrc
# Set our version number using vars
echo $CIRCLE_BUILD_NUM
# Build our image, auth, and push
docker build --tag ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM
VIEWER_DEMO_BUILD: VIEWER_DEMO_BUILD:
<<: *defaults <<: *defaults
steps: steps:
@@ -288,10 +325,13 @@ workflows:
# E2E: PWA # E2E: PWA
- cypress/run: - cypress/run:
name: 'E2E: PWA' name: 'E2E: PWA'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps: pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest add wait-on' # Use yarn latest
yarn: true yarn: true
record: false
store_artifacts: false store_artifacts: false
working_directory: platform/viewer working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build build: npx cross-env QUICK_BUILD=true yarn run build
@@ -307,10 +347,13 @@ workflows:
# E2E: script-tag # E2E: script-tag
- cypress/run: - cypress/run:
name: 'E2E: Script Tag' name: 'E2E: Script Tag'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps: pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest add wait-on' # Use yarn latest
yarn: true yarn: true
record: false
store_artifacts: false store_artifacts: false
working_directory: platform/viewer working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package build: npx cross-env QUICK_BUILD=true yarn run build:package
@@ -325,6 +368,17 @@ workflows:
requires: requires:
- UNIT_TESTS - UNIT_TESTS
PR_OPTIONAL_DOCKER_PUBLISH:
jobs:
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
- AWAIT_APPROVAL:
type: approval
# Update hub.docker.org
- DOCKER_PR_PUBLISH:
context: Docker Hub
requires:
- AWAIT_APPROVAL
# MERGE TO MASTER # MERGE TO MASTER
RELEASE: RELEASE:
jobs: jobs:
@@ -335,11 +389,13 @@ workflows:
# E2E: PWA + Persist # E2E: PWA + Persist
- cypress/run: - cypress/run:
name: 'E2E: PWA' name: 'E2E: PWA'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps: pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest add wait-on' # Use yarn latest
yarn: true yarn: true
record: true record: false
store_artifacts: true store_artifacts: true
working_directory: platform/viewer working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build build: npx cross-env QUICK_BUILD=true yarn run build
@@ -358,10 +414,13 @@ workflows:
# E2E: script-tag # E2E: script-tag
- cypress/run: - cypress/run:
name: 'E2E: Script Tag' name: 'E2E: Script Tag'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps: pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest add wait-on' # Use yarn latest
yarn: true yarn: true
record: false
store_artifacts: false store_artifacts: false
working_directory: platform/viewer working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package build: npx cross-env QUICK_BUILD=true yarn run build:package
@@ -396,6 +455,6 @@ workflows:
requires: requires:
- VIEWER_DEMO_BUILD - VIEWER_DEMO_BUILD
# Update hub.docker.org # Update hub.docker.org
- DOCKER_PUBLISH: - DOCKER_MASTER_PUBLISH:
requires: requires:
- NPM_PUBLISH - NPM_PUBLISH
@@ -0,0 +1,29 @@
### Request Checklist
- [] Brief description of changes
- [] Links to any relevant issues
- [] Required status checks are passing
- [] `@mention` a maintainer to request a review
#### Special
> If this PR is not on a branch for this repo, it is "untrusted" and does not
> have access to env vars. Reviewers can kick off a trusted docker deploy by:
>
> 1. Verifying there are no malicious changes to CI build
> 2. Using the [git-push-fork-to-upstream-branch][script] script
> 3. Executing the manual approval step in the CI workflow
- [] Request docker publish of PR
_Want to improve this process? Consider PR'ing the ability for reviewers to add
a label to trigger this process._
<!--
Links
-->
<!-- prettier-ignore-start -->
[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/
[script]: https://github.com/jklukas/git-push-fork-to-upstream-branch
<!-- prettier-ignore-end -->
+1
View File
@@ -44,6 +44,7 @@ RUN yarn config set workspaces-experimental true
RUN yarn install RUN yarn install
ENV PATH /usr/src/app/node_modules/.bin:$PATH ENV PATH /usr/src/app/node_modules/.bin:$PATH
ENV QUICK_BUILD true
# ENV GENERATE_SOURCEMAP=false # ENV GENERATE_SOURCEMAP=false
# ENV REACT_APP_CONFIG=config/default.js # ENV REACT_APP_CONFIG=config/default.js
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.52.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.14...@ohif/extension-vtk@0.52.15) (2019-10-23)
### Performance Improvements
* ⚡️ Throttle UI refresh of synced WL value in vtk port ([#1070](https://github.com/OHIF/Viewers/issues/1070)) ([9d08e81](https://github.com/OHIF/Viewers/commit/9d08e81aa5def63a5e34f464aae099f7c29f0ac5))
## [0.52.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.13...@ohif/extension-vtk@0.52.14) (2019-10-22)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.12...@ohif/extension-vtk@0.52.13) (2019-10-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.11...@ohif/extension-vtk@0.52.12) (2019-10-18) ## [0.52.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.11...@ohif/extension-vtk@0.52.12) (2019-10-18)
**Note:** Version bump only for package @ohif/extension-vtk **Note:** Version bump only for package @ohif/extension-vtk
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-vtk", "name": "@ohif/extension-vtk",
"version": "0.52.12", "version": "0.52.15",
"description": "OHIF extension for VTK.js", "description": "OHIF extension for VTK.js",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@@ -48,11 +48,12 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"react-vtkjs-viewport": "^0.3.0" "lodash.throttle": "^4.1.1",
"react-vtkjs-viewport": "^0.3.1"
}, },
"devDependencies": { "devDependencies": {
"@ohif/core": "^1.4.0", "@ohif/core": "^1.4.0",
"@ohif/ui": "^0.55.0", "@ohif/ui": "^0.56.0",
"cornerstone-tools": "^4.0.9", "cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.6.1",
+8 -13
View File
@@ -11,7 +11,6 @@ import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData'; import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
const { StackManager } = OHIF.utils; const { StackManager } = OHIF.utils;
@@ -182,7 +181,6 @@ class OHIFVTKViewport extends Component {
} }
const { vtkImageData, imageMetaData0 } = imageDataObject; const { vtkImageData, imageMetaData0 } = imageDataObject;
const { windowWidth, windowCenter, modality } = imageMetaData0; const { windowWidth, windowCenter, modality } = imageMetaData0;
let lower; let lower;
@@ -208,18 +206,17 @@ class OHIFVTKViewport extends Component {
.getRGBTransferFunction(0) .getRGBTransferFunction(0)
.setRange(lower, upper); .setRange(lower, upper);
// TODO: Should look into implementing autoAdjustSampleDistance in vtk
// Manually changing the sample distance here by a couple orders of
// Magnitude in either direction makes no difference.
// Also setting volumeMapper.setMaximumSamplesPerRay(10000)
// Prevents the warning of too many samples, but the resolution isn't improved.
const spacing = vtkImageData.getSpacing(); const spacing = vtkImageData.getSpacing();
// Set the sample distance to half the mean length of one side. This is where the divide by 6 comes from.
// https://github.com/Kitware/VTK/blob/6b559c65bb90614fb02eb6d1b9e3f0fca3fe4b0b/Rendering/VolumeOpenGL2/vtkSmartVolumeMapper.cxx#L344 // https://github.com/Kitware/VTK/blob/6b559c65bb90614fb02eb6d1b9e3f0fca3fe4b0b/Rendering/VolumeOpenGL2/vtkSmartVolumeMapper.cxx#L344
const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6.0; const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6;
volumeMapper.setSampleDistance(sampleDistance); volumeMapper.setSampleDistance(sampleDistance);
// Be generous to surpress warnings, as the logging really hurts performance.
// TODO: maybe we should auto adjust samples to 1000.
volumeMapper.setMaximumSamplesPerRay(4000);
volumeCache[displaySetInstanceUid] = volumeActor; volumeCache[displaySetInstanceUid] = volumeActor;
return volumeActor; return volumeActor;
@@ -251,6 +248,8 @@ class OHIFVTKViewport extends Component {
frameIndex frameIndex
); );
this.imageDataObject = imageDataObject;
// TODO: Temporarily disabling this since it is not yet // TODO: Temporarily disabling this since it is not yet
// being used and hurts performance significantly. // being used and hurts performance significantly.
/*if (!labelmap) { /*if (!labelmap) {
@@ -294,10 +293,6 @@ class OHIFVTKViewport extends Component {
this.setStateFromProps(); this.setStateFromProps();
} }
componentWillUnmount() {
vtkViewportSubscriptionManager.unsubscribe(this.props.viewportIndex);
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { studies, displaySet } = this.props.viewportData; const { studies, displaySet } = this.props.viewportData;
const prevDisplaySet = prevProps.viewportData.displaySet; const prevDisplaySet = prevProps.viewportData.displaySet;
+86 -198
View File
@@ -4,66 +4,17 @@ import {
vtkInteractorStyleMPRSlice, vtkInteractorStyleMPRSlice,
vtkInteractorStyleMPRRotate, vtkInteractorStyleMPRRotate,
vtkSVGCrosshairsWidget, vtkSVGCrosshairsWidget,
vtkSVGWidgetManager,
} from 'react-vtkjs-viewport'; } from 'react-vtkjs-viewport';
import setMPRLayout from './utils/setMPRLayout.js'; import setMPRLayout from './utils/setMPRLayout.js';
import setViewportToVTK from './utils/setViewportToVTK.js'; import setViewportToVTK from './utils/setViewportToVTK.js';
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js'; import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
import throttle from 'lodash.throttle';
const { BlendMode } = Constants; const { BlendMode } = Constants;
// TODO: Put this somewhere else // TODO: Put this somewhere else
let apis = {}; let apis = {};
let currentSlabThickness = 0.1;
let defaultSlabThickness = 0.1;
function getCrosshairCallbackForIndex(index) {
return ({ worldPos }) => {
// Set camera focal point to world coordinate for linked views
apis.forEach((api, viewportIndex) => {
if (viewportIndex !== index) {
// We are basically doing the same as getSlice but with the world coordinate
// that we want to jump to instead of the camera focal point.
// I would rather do the camera adjustment directly but I keep
// doing it wrong and so this is good enough for now.
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const sliceNormal = istyle.getSliceNormal();
const transform = vtkMatrixBuilder
.buildFromDegree()
.identity()
.rotateFromDirections(sliceNormal, [1, 0, 0]);
const mutatedWorldPos = worldPos.slice();
transform.apply(mutatedWorldPos);
const slice = mutatedWorldPos[0];
istyle.setSlice(slice);
renderWindow.render();
}
const renderer = api.genericRenderWindow.getRenderer();
const wPos = vtkCoordinate.newInstance();
wPos.setCoordinateSystemToWorld();
wPos.setValue(worldPos);
const displayPosition = wPos.getComputedDisplayValue(renderer);
const { svgWidgetManager } = api;
api.svgWidgets.crosshairsWidget.setPoint(
displayPosition[0],
displayPosition[1]
);
svgWidgetManager.render();
});
};
}
async function _getActiveViewportVTKApi(viewports) { async function _getActiveViewportVTKApi(viewports) {
const { const {
@@ -105,8 +56,6 @@ async function _getActiveViewportVTKApi(viewports) {
function _setView(api, sliceNormal, viewUp) { function _setView(api, sliceNormal, viewUp) {
const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const istyle = renderWindow.getInteractor().getInteractorStyle(); const istyle = renderWindow.getInteractor().getInteractorStyle();
istyle.setSliceNormal(...sliceNormal); istyle.setSliceNormal(...sliceNormal);
istyle.setViewUp(...viewUp); istyle.setViewUp(...viewUp);
@@ -114,41 +63,6 @@ function _setView(api, sliceNormal, viewUp) {
renderWindow.render(); renderWindow.render();
} }
function switchMPRInteractors(api, istyle) {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const currentIStyle = renderWindow.getInteractor().getInteractorStyle();
let currentViewport;
if (currentIStyle.getViewport && istyle.getViewport) {
currentViewport = currentIStyle.getViewport();
}
let currentSlabThickness;
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
currentSlabThickness = currentIStyle.getSlabThickness();
defaultSlabThickness = currentSlabThickness;
}
const interactor = renderWindow.getInteractor();
interactor.setInteractorStyle(istyle);
// TODO: Not sure why this is required the second time this function is called
istyle.setInteractor(interactor);
if (currentViewport) {
istyle.setViewport(currentViewport);
}
if (istyle.getVolumeMapper() !== api.volumes[0]) {
if (currentSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
istyle.setVolumeMapper(api.volumes[0]);
}
}
const actions = { const actions = {
axial: async ({ viewports }) => { axial: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports); const api = await _getActiveViewportVTKApi(viewports);
@@ -175,52 +89,56 @@ const actions = {
apis.forEach(api => { apis.forEach(api => {
const istyle = vtkInteractorStyleMPRRotate.newInstance(); const istyle = vtkInteractorStyleMPRRotate.newInstance();
switchMPRInteractors(api, istyle); api.setInteractorStyle({ istyle });
}); });
}, },
enableCrosshairsTool: () => { enableCrosshairsTool: () => {
apis.forEach((api, index) => { apis.forEach((api, apiIndex) => {
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
switchMPRInteractors(api, istyle); api.setInteractorStyle({
istyle,
istyle.setCallback(getCrosshairCallbackForIndex(index)); configuration: { apis, apiIndex },
});
}); });
}, },
enableLevelTool: () => { enableLevelTool: () => {
function updateVOI(apis, windowWidth, windowCenter) {
apis.forEach(api => {
api.updateVOI(windowWidth, windowCenter);
});
}
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
const callbacks = {
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
renderWindow.render();
});
throttledUpdateVOIs(apis, windowWidth, windowCenter);
},
};
apis.forEach(api => { apis.forEach(api => {
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance(); const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
switchMPRInteractors(api, istyle); api.setInteractorStyle({ istyle, callbacks });
}); });
}, },
setSlabThickness: ({ slabThickness }) => { setSlabThickness: ({ slabThickness }) => {
currentSlabThickness = slabThickness;
apis.forEach(api => { apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow(); api.setSlabThickness(slabThickness);
const istyle = renderWindow.getInteractor().getInteractorStyle();
if (istyle.setSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
renderWindow.render();
}); });
}, },
changeSlabThickness: ({ change }) => { changeSlabThickness: ({ change }) => {
currentSlabThickness += change;
currentSlabThickness = Math.max(currentSlabThickness, 0.1);
apis.forEach(api => { apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow(); const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
const istyle = renderWindow.getInteractor().getInteractorStyle();
if (istyle.setSlabThickness) { api.setSlabThickness(slabThickness);
istyle.setSlabThickness(currentSlabThickness);
}
renderWindow.render();
}); });
}, },
setBlendModeToComposite: () => { setBlendModeToComposite: () => {
@@ -228,13 +146,15 @@ const actions = {
const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle(); const istyle = renderWindow.getInteractor().getInteractorStyle();
const slabThickness = api.getSlabThickness();
const mapper = api.volumes[0].getMapper(); const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToComposite) { if (mapper.setBlendModeToComposite) {
mapper.setBlendModeToComposite(); mapper.setBlendModeToComposite();
} }
if (istyle.setSlabThickness) { if (istyle.setSlabThickness) {
istyle.setSlabThickness(defaultSlabThickness); istyle.setSlabThickness(slabThickness);
} }
renderWindow.render(); renderWindow.render();
}); });
@@ -252,7 +172,6 @@ const actions = {
setBlendMode: ({ blendMode }) => { setBlendMode: ({ blendMode }) => {
apis.forEach(api => { apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
api.volumes[0].getMapper().setBlendMode(blendMode); api.volumes[0].getMapper().setBlendMode(blendMode);
@@ -264,28 +183,8 @@ const actions = {
const displaySet = const displaySet =
viewports.viewportSpecificData[viewports.activeViewportIndex]; viewports.viewportSpecificData[viewports.activeViewportIndex];
// TODO -> Clean this logic up a bit. // Get current VOI if cornerstone viewport.
const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom); const cornerstoneVOI = getVOIFromCornerstoneViewport(displaySet);
let cornerstoneVOI;
if (cornerstoneElement) {
const imageId = cornerstoneElement.image.imageId;
const { modality } = cornerstone.metaData.get(
'generalSeriesModule',
imageId
);
if (modality !== 'PT') {
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
cornerstoneVOI = {
windowWidth,
windowCenter,
};
}
}
const viewportProps = [ const viewportProps = [
{ {
@@ -295,7 +194,6 @@ const actions = {
viewUp: [0, -1, 0], viewUp: [0, -1, 0],
}, },
}, },
{ {
// Sagital // Sagital
orientation: { orientation: {
@@ -312,77 +210,29 @@ const actions = {
}, },
]; ];
let apiByViewport;
try { try {
apiByViewport = await setMPRLayout(displaySet, viewportProps); apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
} catch (error) { } catch (error) {
throw new Error(error); throw new Error(error);
} }
apis = apiByViewport;
const rgbTransferFunction = apiByViewport[0].volumes[0]
.getProperty()
.getRGBTransferFunction(0);
if (cornerstoneVOI) { if (cornerstoneVOI) {
const { windowWidth, windowCenter } = cornerstoneVOI; setVOI(cornerstoneVOI);
const lower = windowCenter - windowWidth / 2.0;
const upper = windowCenter + windowWidth / 2.0;
rgbTransferFunction.setRange(lower, upper);
apiByViewport.forEach(api => {
api.updateVOI(windowWidth, windowCenter);
});
} }
const onModifiedSubscription = rgbTransferFunction.onModified(() => { // Add widgets and set default interactorStyle of each viewport.
const range = rgbTransferFunction.getMappingRange(); apis.forEach((api, apiIndex) => {
const windowWidth = Math.abs(range[1] - range[0]); api.addSVGWidget(
const windowCenter = range[0] + windowWidth / 2; vtkSVGCrosshairsWidget.newInstance(),
'crosshairsWidget'
apiByViewport.forEach(api => { );
const renderWindow = api.genericRenderWindow.getRenderWindow();
renderWindow.render();
api.updateVOI(windowWidth, windowCenter);
});
});
vtkViewportSubscriptionManager.pushSubscription(0, onModifiedSubscription);
apiByViewport.forEach((api, index) => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
renderWindow.getInteractor().setInteractorStyle(istyle);
istyle.setVolumeMapper(api.volumes[0]); api.setInteractorStyle({
istyle.setCallback(getCrosshairCallbackForIndex(index)); istyle,
configuration: { apis, apiIndex },
const svgWidgetManager = vtkSVGWidgetManager.newInstance(); });
svgWidgetManager.setRenderer(renderer);
svgWidgetManager.setScale(1);
const crosshairsWidget = vtkSVGCrosshairsWidget.newInstance();
svgWidgetManager.addWidget(crosshairsWidget);
svgWidgetManager.render();
api.svgWidgetManager = svgWidgetManager;
api.svgWidgets = {
crosshairsWidget,
};
const orientation = api.getOrientation();
istyle.setViewUp(...orientation.viewUp);
istyle.setSliceNormal(...orientation.sliceNormal);
renderWindow.render();
}); });
}, },
}; };
@@ -472,3 +322,41 @@ export default {
definitions, definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK', defaultContext: 'ACTIVE_VIEWPORT::VTK',
}; };
function getVOIFromCornerstoneViewport(displaySet) {
const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom);
if (cornerstoneElement) {
const imageId = cornerstoneElement.image.imageId;
const { modality } = cornerstone.metaData.get(
'generalSeriesModule',
imageId
);
if (modality !== 'PT') {
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
return {
windowWidth,
windowCenter,
};
}
}
}
function setVOI(voi) {
const { windowWidth, windowCenter } = voi;
const lower = windowCenter - windowWidth / 2.0;
const upper = windowCenter + windowWidth / 2.0;
const rgbTransferFunction = apis[0].volumes[0]
.getProperty()
.getRGBTransferFunction(0);
rgbTransferFunction.setRange(lower, upper);
apis.forEach(api => {
api.updateVOI(windowWidth, windowCenter);
});
}
+1 -1
View File
@@ -4,7 +4,7 @@ export default function setMPRLayout(
displaySet, displaySet,
viewportPropsArray, viewportPropsArray,
numRows = 1, numRows = 1,
numColumns = 3 numColumns = 1
) { ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const viewports = []; const viewports = [];
@@ -1,28 +0,0 @@
const subscriptions = [];
// This is pretty hacky right now, but it makes sure we don't keep adding subscriptions.
// TODO -> Nuke this and move it up a layer once we have more vigorous layout support.
const vtkViewportSubscriptionManager = {
subscriptions,
pushSubscription(viewportIndex, subscription) {
if (!Array.isArray(subscriptions[viewportIndex])) {
subscriptions[viewportIndex] = [];
}
subscriptions[viewportIndex].push(subscription);
},
unsubscribe(viewportIndex) {
if (!subscriptions[viewportIndex]) {
return;
}
while (subscriptions[viewportIndex].length) {
subscriptions[viewportIndex].pop().unsubscribe();
}
subscriptions[viewportIndex] = null;
},
};
export default vtkViewportSubscriptionManager;
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.56.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.55.0...@ohif/ui@0.56.0) (2019-10-22)
### Features
* 🎸 Load spinner when selecting gcloud store. Add key on td ([#1034](https://github.com/OHIF/Viewers/issues/1034)) ([e62f403](https://github.com/OHIF/Viewers/commit/e62f403fe9e3df56713128e3d59045824b086d8d)), closes [#1057](https://github.com/OHIF/Viewers/issues/1057)
# [0.55.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.54.0...@ohif/ui@0.55.0) (2019-10-15) # [0.55.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.54.0...@ohif/ui@0.55.0) (2019-10-15)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/ui", "name": "@ohif/ui",
"version": "0.55.0", "version": "0.56.0",
"description": "A set of React components for Medical Imaging Viewers", "description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors", "author": "OHIF Contributors",
"license": "MIT", "license": "MIT",
+165 -109
View File
@@ -11,11 +11,79 @@ import { StudylistToolbar } from './StudyListToolbar.js';
import { isInclusivelyBeforeDay } from 'react-dates'; import { isInclusivelyBeforeDay } from 'react-dates';
import moment from 'moment'; import moment from 'moment';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import isEqual from 'lodash.isequal';
import { withTranslation } from '../../utils/LanguageProvider'; import { withTranslation } from '../../utils/LanguageProvider';
const today = moment(); const today = moment();
const lastWeek = moment().subtract(7, 'day'); const lastWeek = moment().subtract(7, 'day');
const lastMonth = moment().subtract(1, 'month'); const lastMonth = moment().subtract(1, 'month');
function getPaginationFragment(
props,
searchData,
nextPageCb,
prevPageCb,
changeRowsPerPageCb
) {
return (
<PaginationArea
pageOptions={props.pageOptions}
currentPage={searchData.currentPage}
nextPageFunc={nextPageCb}
prevPageFunc={prevPageCb}
onRowsPerPageChange={changeRowsPerPageCb}
rowsPerPage={searchData.rowsPerPage}
recordCount={props.studies.length}
/>
);
}
function getTableMeta(translate) {
return {
patientName: {
displayText: translate('PatientName'),
sort: 0,
},
patientId: {
displayText: translate('MRN'),
sort: 0,
},
accessionNumber: {
displayText: translate('AccessionNumber'),
sort: 0,
},
studyDate: {
displayText: translate('StudyDate'),
inputType: 'date-range',
sort: 0,
},
modalities: {
displayText: translate('Modality'),
sort: 0,
},
studyDescription: {
displayText: translate('StudyDescription'),
sort: 0,
},
};
}
function getNoListFragment(translate, studies, error, loading) {
if (loading) {
return (
<div className="loading">
<StudyListLoadingText />
</div>
);
} else if (error) {
return (
<div className="notFound">
{translate('There was an error fetching studies')}
</div>
);
} else if (!studies.length) {
return <div className="notFound">{translate('No matching results')}</div>;
}
}
class StudyList extends Component { class StudyList extends Component {
static propTypes = { static propTypes = {
@@ -111,24 +179,27 @@ class StudyList extends Component {
getBlurHandler(key) { getBlurHandler(key) {
return event => { return event => {
this.delayedSearch.cancel(); this.delayedSearch.cancel();
this.setSearchData(key, event.target.value, this.search); this.setSearchData(key, event.target.value);
}; };
} }
setSearchData(key, value, callback) { setSearchData(key, value) {
const searchData = this.state.searchData; const searchData = { ...this.state.searchData };
searchData[key] = value; searchData[key] = value;
this.setState({ searchData }, callback);
if (!isEqual(searchData[key], this.state.searchData[key])) {
this.setState({ ...this.state, searchData });
}
} }
setSearchDataBatch(keyValues, callback) { setSearchDataBatch(keyValues) {
const searchData = this.state.searchData; const searchData = { ...this.state.searchData };
Object.keys(keyValues).forEach(key => { Object.keys(keyValues).forEach(key => {
searchData[key] = keyValues[key]; searchData[key] = keyValues[key];
}); });
this.setState({ searchData }, callback); this.setState({ searchData });
} }
async onInputKeydown(event) { async onInputKeydown(event) {
@@ -138,7 +209,7 @@ class StudyList extends Component {
this.delayedSearch.cancel(); this.delayedSearch.cancel();
// reset the page because user is doing a new search // reset the page because user is doing a new search
this.setSearchData('currentPage', 0, this.search); this.setSearchData('currentPage', 0);
} }
} }
@@ -154,45 +225,21 @@ class StudyList extends Component {
} }
} }
renderNoMachingResults() {
if (!this.props.studies.length && !this.state.error) {
return <div className="notFound">No matching results</div>;
}
}
renderHasError() {
if (this.state.error) {
return (
<div className="notFound">There was an error fetching studies</div>
);
}
}
renderIsLoading() {
if (this.state.loading) {
return (
<div className="loading">
<StudyListLoadingText />
</div>
);
}
}
nextPage(currentPage) { nextPage(currentPage) {
currentPage = currentPage + 1; currentPage = currentPage + 1;
this.delayedSearch.cancel(); this.delayedSearch.cancel();
this.setSearchData('currentPage', currentPage, this.search); this.setSearchData('currentPage', currentPage);
} }
prevPage(currentPage) { prevPage(currentPage) {
currentPage = currentPage - 1; currentPage = currentPage - 1;
this.delayedSearch.cancel(); this.delayedSearch.cancel();
this.setSearchData('currentPage', currentPage, this.search); this.setSearchData('currentPage', currentPage);
} }
onRowsPerPageChange(rowsPerPage) { onRowsPerPageChange(rowsPerPage) {
this.delayedSearch.cancel(); this.delayedSearch.cancel();
this.setSearchDataBatch({ rowsPerPage, currentPage: 0 }, this.search); this.setSearchDataBatch({ rowsPerPage, currentPage: 0 });
} }
onSortClick(field) { onSortClick(field) {
@@ -213,7 +260,7 @@ class StudyList extends Component {
} }
this.delayedSearch.cancel(); this.delayedSearch.cancel();
this.setSearchData('sortData', { field, order }, this.search); this.setSearchData('sortData', { field, order });
}; };
} }
@@ -221,10 +268,39 @@ class StudyList extends Component {
this.setState({ highlightedItem: studyItemUid }); this.setState({ highlightedItem: studyItemUid });
} }
renderTableRow(study) { getTableRow(study, index) {
const trKey = `trStudy${index}${study.studyInstanceUid}`;
if (!study) {
return;
}
const getTableCell = (
study,
studyKey,
emptyValue = '',
emptyClass = ''
) => {
const componentKey = `td${studyKey}`;
const isValidValue = study && typeof study[studyKey] === 'string';
let className = emptyClass;
let value = emptyValue;
if (isValidValue) {
className = studyKey;
value = study[studyKey];
}
return (
<td key={componentKey} className={className}>
{value}
</td>
);
};
return ( return (
<tr <tr
key={study.studyInstanceUid} key={trKey}
className={ className={
this.state.highlightedItem === study.studyInstanceUid this.state.highlightedItem === study.studyInstanceUid
? 'studylistStudy noselect active' ? 'studylistStudy noselect active'
@@ -241,47 +317,35 @@ class StudyList extends Component {
this.props.onSelectItem(study.studyInstanceUid); this.props.onSelectItem(study.studyInstanceUid);
}} }}
> >
<td className={study.patientName ? 'patientName' : 'emptyCell'}> {getTableCell(
{study.patientName || `(${this.props.t('Empty')})`} study,
</td> 'patientName',
`(${this.props.t('Empty')})`,
<td className="patientId">{study.patientId}</td> 'emptyCell'
<td className="accessionNumber">{study.accessionNumber}</td> )}
<td className="studyDate">{study.studyDate}</td> {getTableCell(study, 'patientId')}
<td className="modalities">{study.modalities}</td> {getTableCell(study, 'accessionNumber')}
<td className="studyDescription">{study.studyDescription}</td> {getTableCell(study, 'studyDate')}
{getTableCell(study, 'modalities')}
{getTableCell(study, 'studyDescription')}
</tr> </tr>
); );
} }
componentDidUpdate(previousProps, previousState) {
if (!isEqual(previousState.searchData, this.state.searchData)) {
this.search();
}
}
renderTableBody(noListFragment) {
return !noListFragment && this.props.studies
? this.props.studies.map(this.getTableRow.bind(this))
: null;
}
render() { render() {
const tableMeta = { const tableMeta = getTableMeta(this.props.t);
patientName: {
displayText: this.props.t('PatientName'),
sort: 0,
},
patientId: {
displayText: this.props.t('MRN'),
sort: 0,
},
accessionNumber: {
displayText: this.props.t('AccessionNumber'),
sort: 0,
},
studyDate: {
displayText: this.props.t('StudyDate'),
inputType: 'date-range',
sort: 0,
},
modalities: {
displayText: this.props.t('Modality'),
sort: 0,
},
studyDescription: {
displayText: this.props.t('StudyDescription'),
sort: 0,
},
};
// Apply sort // Apply sort
const sortedFieldName = this.state.searchData.sortData.field; const sortedFieldName = this.state.searchData.sortData.field;
@@ -294,14 +358,20 @@ class StudyList extends Component {
// Sort Icons // Sort Icons
const sortIcons = ['sort', 'sort-up', 'sort-down']; const sortIcons = ['sort', 'sort-up', 'sort-down'];
const noListFragment = getNoListFragment(
this.props.t,
this.props.studies,
this.state.error,
this.props.loading || this.state.loading
);
const tableBody = this.renderTableBody(noListFragment);
const studiesNum = (this.props.studies && this.props.studies.length) || 0;
return ( return (
<div className="StudyList"> <div className="StudyList">
<div className="studyListToolbar clearfix"> <div className="studyListToolbar clearfix">
<div className="header pull-left">{this.props.t('StudyList')}</div> <div className="header pull-left">{this.props.t('StudyList')}</div>
<div className="studyCount pull-right"> <div className="studyCount pull-right">{studiesNum}</div>
{this.props.studies.length}
</div>
<div className="pull-right"> <div className="pull-right">
{this.props.studyListFunctionsEnabled ? ( {this.props.studyListFunctionsEnabled ? (
<StudylistToolbar onImport={this.props.onImport} /> <StudylistToolbar onImport={this.props.onImport} />
@@ -367,22 +437,16 @@ class StudyList extends Component {
(this.state.focusedInput === 'endDate' || (this.state.focusedInput === 'endDate' ||
preset) preset)
) { ) {
this.setSearchDataBatch( this.setSearchDataBatch({
{ studyDateFrom: startDate.toDate(),
studyDateFrom: startDate.toDate(), studyDateTo: endDate.toDate(),
studyDateTo: endDate.toDate(), });
},
this.search
);
this.setState({ focusedInput: false }); this.setState({ focusedInput: false });
} else if (!startDate && !endDate) { } else if (!startDate && !endDate) {
this.setSearchDataBatch( this.setSearchDataBatch({
{ studyDateFrom: null,
studyDateFrom: null, studyDateTo: null,
studyDateTo: null, });
},
this.search
);
} }
}} }}
focusedInput={this.state.focusedInput} focusedInput={this.state.focusedInput}
@@ -398,26 +462,18 @@ class StudyList extends Component {
})} })}
</tr> </tr>
</thead> </thead>
<tbody id="studyListData"> <tbody id="studyListData">{tableBody}</tbody>
{this.props.studies.map(study => {
return this.renderTableRow(study);
})}
</tbody>
</table> </table>
{this.renderIsLoading()} {noListFragment
{this.renderHasError()} ? noListFragment
{this.renderNoMachingResults()} : getPaginationFragment(
this.props,
<PaginationArea this.state.searchData,
pageOptions={this.props.pageOptions} this.nextPage,
currentPage={this.state.searchData.currentPage} this.prevPage,
nextPageFunc={this.nextPage} this.onRowsPerPageChange
prevPageFunc={this.prevPage} )}
onRowsPerPageChange={this.onRowsPerPageChange}
rowsPerPage={this.state.searchData.rowsPerPage}
recordCount={this.props.studies.length}
/>
</div> </div>
</div> </div>
); );
@@ -82,9 +82,14 @@ placeholder-color(c)
position: absolute position: absolute
z-index: 2 z-index: 2
.loading-text .loading
color: var(--table-text-secondary-color) display: flex;
font-size: 30px justify-content: center;
.loading-text
color: var(--table-text-secondary-color)
font-size: 30px
width: fit-content;
.notFound .notFound
color: var(--table-text-secondary-color) color: var(--table-text-secondary-color)
@@ -1,12 +1,17 @@
import { Icon } from './../../elements/Icon'; import { Icon } from './../../elements/Icon';
import React from 'react'; import React from 'react';
import { withTranslation } from '../../utils/LanguageProvider';
function StudyListLoadingText() { function StudyListLoadingText({ t: translate }) {
return ( return (
<div className="loading-text"> <div className="loading-text">
Loading... <Icon name="circle-notch" animation="pulse" /> {translate('Loading')}... <Icon name="circle-notch" animation="pulse" />
</div> </div>
); );
} }
export { StudyListLoadingText }; const connectedComponent = withTranslation('StudyListLoadingText')(
StudyListLoadingText
);
export { connectedComponent as StudyListLoadingText };
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.1...@ohif/viewer@1.11.2) (2019-10-23)
**Note:** Version bump only for package @ohif/viewer
## [1.11.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.0...@ohif/viewer@1.11.1) (2019-10-23)
**Note:** Version bump only for package @ohif/viewer
# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.3...@ohif/viewer@1.11.0) (2019-10-22)
### Bug Fixes
* MPR initialization ([#1062](https://github.com/OHIF/Viewers/issues/1062)) ([b037394](https://github.com/OHIF/Viewers/commit/b03739428f72bb50bdabdd6f83b7af885057da69))
### Features
* 🎸 Load spinner when selecting gcloud store. Add key on td ([#1034](https://github.com/OHIF/Viewers/issues/1034)) ([e62f403](https://github.com/OHIF/Viewers/commit/e62f403fe9e3df56713128e3d59045824b086d8d)), closes [#1057](https://github.com/OHIF/Viewers/issues/1057)
## [1.10.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.2...@ohif/viewer@1.10.3) (2019-10-18)
**Note:** Version bump only for package @ohif/viewer
## [1.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.1...@ohif/viewer@1.10.2) (2019-10-18) ## [1.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.1...@ohif/viewer@1.10.2) (2019-10-18)
**Note:** Version bump only for package @ohif/viewer **Note:** Version bump only for package @ohif/viewer
+4
View File
@@ -2,5 +2,9 @@
"baseUrl": "http://localhost:3000", "baseUrl": "http://localhost:3000",
"video": false, "video": false,
"chromeWebSecurity": false, "chromeWebSecurity": false,
"waitForAnimations": true,
"defaultCommandTimeout": 10000,
"requestTimeout": 10000,
"responseTimeout": 10000,
"projectId": "4oe38f" "projectId": "4oe38f"
} }
@@ -2,7 +2,7 @@ describe('OHIF Cornerstone Toolbar', () => {
before(() => { before(() => {
cy.openStudy('MISTER^MR'); cy.openStudy('MISTER^MR');
cy.waitDicomImage(); cy.waitDicomImage();
cy.expectMinimumThumbnails(1); cy.expectMinimumThumbnails(3);
}); });
beforeEach(() => { beforeEach(() => {
@@ -57,12 +57,11 @@ describe('OHIF Cornerstone Toolbar', () => {
//drags the mouse inside the viewport to be able to interact with series //drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport') cy.get('@viewport')
.trigger('mousedown', 'top', { which: 1 }) .trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'center', { which: 1 }) .trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup'); .trigger('mouseup');
const expectedText = const expectedText =
'Ser: 1Img: 14 14/26256 x 256Loc: 0.00 mm Thick: 5.00 mm'; 'Ser: 1Img: 1 1/26256 x 256Loc: -30.00 mm Thick: 5.00 mm';
cy.get('@viewportInfoBottomLeft').should('have.text', expectedText); cy.get('@viewportInfoBottomLeft').should('have.text', expectedText);
}); });
@@ -76,11 +75,11 @@ describe('OHIF Cornerstone Toolbar', () => {
//drags the mouse inside the viewport to be able to interact with series //drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport') cy.get('@viewport')
.trigger('mousedown', 'top', { which: 1 }) .trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'center', { which: 1 }) .trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup'); .trigger('mouseup');
const expectedText = 'Zoom: 884%W: 820 L: 410Lossless / Uncompressed'; const expectedText = 'Zoom: 50%W: 958 L: 479Lossless / Uncompressed';
cy.get('@viewportInfoBottomRight').should('have.text', expectedText); cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
}); });
@@ -94,14 +93,14 @@ describe('OHIF Cornerstone Toolbar', () => {
//drags the mouse inside the viewport to be able to interact with series //drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport') cy.get('@viewport')
.trigger('mousedown', 'top', { which: 1 }) .trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'center', { which: 1 }) .trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup') .trigger('mouseup')
.trigger('mousedown', 'center', { which: 1 }) .trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'left', { which: 1 }) .trigger('mousemove', 'left', { which: 1 })
.trigger('mouseup'); .trigger('mouseup');
const expectedText = 'Zoom: 211%W: 544 L: 626Lossless / Uncompressed'; const expectedText = 'Zoom: 211%W: 635 L: 226Lossless / Uncompressed';
cy.get('@viewportInfoBottomRight').should('have.text', expectedText); cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
}); });
@@ -197,11 +196,14 @@ describe('OHIF Cornerstone Toolbar', () => {
//Click on reset button //Click on reset button
cy.get('@resetBtn').click(); cy.get('@resetBtn').click();
const expectedText = 'Zoom: 211%W: 820 L: 410Lossless / Uncompressed'; const expectedText = 'Zoom: 211%W: 958 L: 479Lossless / Uncompressed';
cy.get('@viewportInfoBottomRight').should('have.text', expectedText); cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
}); });
it('checks if CINE tool will prompt a modal with working controls', () => { it('checks if CINE tool will prompt a modal with working controls', () => {
cy.server();
cy.route('GET', '/**/studies/**/').as('studies');
//Click on button //Click on button
cy.get('@cineBtn').click(); cy.get('@cineBtn').click();
//Vefiry if cine control overlay is being displayed //Vefiry if cine control overlay is being displayed
@@ -216,26 +218,49 @@ describe('OHIF Cornerstone Toolbar', () => {
.click(); .click();
let expectedText = 'Img: 1 1/26'; let expectedText = 'Img: 1 1/26';
cy.get('@viewportInfoBottomLeft').should('not.have.text', expectedText); cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
'not.have.text',
expectedText
);
//Test SKIP TO FIRST IMAGE button //Test SKIP TO FIRST IMAGE button
cy.get('[title="Skip to first Image"]').click(); cy.get('[title="Skip to first Image"]')
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText); .click()
.wait(1000);
cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
'contain.text',
expectedText
);
//Test NEXT IMAGE button //Test NEXT IMAGE button
cy.get('[title="Next Image"]').click(); cy.get('[title="Next Image"]')
.click()
.wait(1000);
expectedText = 'Img: 2 2/26'; expectedText = 'Img: 2 2/26';
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText); cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
'contain.text',
expectedText
);
//Test SKIP TO LAST IMAGE button //Test SKIP TO LAST IMAGE button
cy.get('[title="Skip to last Image"]').click(); cy.get('[title="Skip to last Image"]')
.click()
.wait(2000);
expectedText = 'Img: 27 26/26'; expectedText = 'Img: 27 26/26';
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText); cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
'contain.text',
expectedText
);
//Test PREVIOUS IMAGE button //Test PREVIOUS IMAGE button
cy.get('[title="Previous Image"]').click(); cy.get('[title="Previous Image"]')
.click()
.wait(1000);
expectedText = 'Img: 26 25/26'; expectedText = 'Img: 26 25/26';
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText); cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
'contain.text',
expectedText
);
//Click on Cine button //Click on Cine button
cy.get('@cineBtn').click(); cy.get('@cineBtn').click();
@@ -1,6 +1,6 @@
describe('OHIFStandaloneViewer', () => { describe('OHIFStandaloneViewer', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/'); cy.openStudyList();
}); });
it('loads route with at least 2 rows', () => { it('loads route with at least 2 rows', () => {
@@ -13,7 +13,11 @@ describe('OHIFStandaloneViewer', () => {
}); });
it('first 2 rows has values', () => { it('first 2 rows has values', () => {
cy.get('#studyListData > :nth-child(1) > .patientId').should('be.visible'); cy.get('#studyListData > :nth-child(1) > .patientId', {
cy.get('#studyListData > :nth-child(2) > .patientId').should('be.visible'); timeout: 15000,
}).should('be.visible');
cy.get('#studyListData > :nth-child(2) > .patientId', {
timeout: 15000,
}).should('be.visible');
}); });
}); });
@@ -2,7 +2,7 @@ describe('OHIF Study Viewer Page', () => {
before(() => { before(() => {
cy.openStudy('MISTER^MR'); cy.openStudy('MISTER^MR');
cy.waitDicomImage(); cy.waitDicomImage();
cy.expectMinimumThumbnails(1); cy.expectMinimumThumbnails(6);
}); });
beforeEach(() => { beforeEach(() => {
@@ -14,13 +14,13 @@ describe('OHIF Study Viewer Page', () => {
cy.screenshot(); cy.screenshot();
cy.percySnapshot(); cy.percySnapshot();
cy.get('.ThumbnailEntryContainer') cy.get('[data-cy="thumbnail-list"]')
.its('length') .its('length')
.should('be.gt', 1); .should('be.gt', 1);
}); });
it('drags and drop a series thumbnail into viewport', () => { it('drags and drop a series thumbnail into viewport', () => {
cy.get('.ThumbnailEntryContainer:nth-child(2)') //element to be dragged cy.get('[data-cy="thumbnail-list"]:nth-child(2)') //element to be dragged
.drag('.cornerstone-canvas'); //dropzone element .drag('.cornerstone-canvas'); //dropzone element
const expectedText = const expectedText =
@@ -1,7 +1,7 @@
describe('OHIF HTML Extension', () => { describe('OHIF HTML Extension', () => {
before(() => { before(() => {
cy.openStudy('Dummy'); cy.openStudy('Dummy');
cy.expectMinimumThumbnails(1); cy.expectMinimumThumbnails(5);
}); });
it('checks if series thumbnails are being displayed', () => { it('checks if series thumbnails are being displayed', () => {
@@ -14,6 +14,7 @@ describe('OHIF HTML Extension', () => {
it('drags and drop a SR thumbnail into viewport', () => { it('drags and drop a SR thumbnail into viewport', () => {
cy.get('[data-cy="thumbnail-list"]') cy.get('[data-cy="thumbnail-list"]')
.contains('SR') .contains('SR')
.first()
.drag('.viewport-drop-target'); .drag('.viewport-drop-target');
cy.get(':nth-child(2) > h1').should( cy.get(':nth-child(2) > h1').should(
@@ -0,0 +1,27 @@
describe('OHIF Microscopy Extension', () => {
before(() => {
cy.openStudyModality('SM');
cy.expectMinimumThumbnails(6);
});
it('checks if series thumbnails are being displayed', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('SM')
.its('length')
.should('to.be.at.least', 1);
});
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
cy.screenshot();
cy.percySnapshot();
});
});
@@ -1,7 +1,7 @@
describe('OHIF PDF Extension', () => { describe('OHIF PDF Extension', () => {
before(() => { before(() => {
cy.openStudy('Dummy'); cy.openStudy('Dummy');
cy.expectMinimumThumbnails(1); cy.expectMinimumThumbnails(6);
}); });
it('checks if series thumbnails are being displayed', () => { it('checks if series thumbnails are being displayed', () => {
@@ -19,5 +19,8 @@ describe('OHIF PDF Extension', () => {
cy.get('.DicomPDFViewport') cy.get('.DicomPDFViewport')
.its('length') .its('length')
.should('be.eq', 1); .should('be.eq', 1);
cy.screenshot();
cy.percySnapshot();
}); });
}); });
@@ -2,7 +2,7 @@ describe('OHIF VTK Extension', () => {
before(() => { before(() => {
cy.openStudy('Juno'); cy.openStudy('Juno');
cy.waitDicomImage(); cy.waitDicomImage();
cy.expectMinimumThumbnails(1); cy.expectMinimumThumbnails(7);
}); });
beforeEach(() => { beforeEach(() => {
@@ -11,7 +11,7 @@ describe('OHIF VTK Extension', () => {
// has data from a drag-n-drop // has data from a drag-n-drop
// Drag and drop first thumbnail into first viewport // Drag and drop first thumbnail into first viewport
cy.get('[data-cy="thumbnail-list"]:nth-child(3)').drag( cy.get('[data-cy="thumbnail-list"]:nth-child(3)').drag(
'.cornerstone-canvas' '.viewport-drop-target'
); );
cy.get('.PluginSwitch > .toolbar-button') cy.get('.PluginSwitch > .toolbar-button')
@@ -22,12 +22,15 @@ describe('OHIF VTK Extension', () => {
btn.click(); btn.click();
} }
}); });
//wait VTK toolbar and images to be loaded
cy.wait(3000);
cy.initVTKToolsAliases(); cy.initVTKToolsAliases();
cy.wait(1000);
}); });
it('checks if VTK buttons are displayed on the toolbar', () => { it('checks if VTK buttons are displayed on the toolbar', () => {
cy.screenshot();
cy.percySnapshot();
cy.get('@crosshairsBtn') cy.get('@crosshairsBtn')
.should('be.visible') .should('be.visible')
.contains('Crosshairs'); .contains('Crosshairs');
+6
View File
@@ -23,6 +23,12 @@ module.exports = (on, config) => {
// whatever you return here becomes the new args // whatever you return here becomes the new args
return args; return args;
} }
if (browser.name === 'chromium') {
const newArgs = args.filter(arg => arg !== '--disable-gpu');
newArgs.push('--ignore-gpu-blacklist');
return newArgs;
}
}); });
on('task', percyHealthCheck); on('task', percyHealthCheck);
+2 -1
View File
@@ -35,7 +35,8 @@ export function initCommonElementsAliases() {
//Creating aliases for Routes //Creating aliases for Routes
export function initRouteAliases() { export function initRouteAliases() {
cy.server(); cy.server();
cy.route('GET', '/dcm4chee-arc/**/series').as('getStudySeries'); cy.route('GET', '**/series**').as('getStudySeries');
cy.route('GET', '**/studies**').as('getStudies');
} }
//Creating aliases for VTK tools buttons //Creating aliases for VTK tools buttons
+61 -30
View File
@@ -39,19 +39,46 @@ import {
* @param {string} PatientName - Patient name that we would like to search for * @param {string} PatientName - Patient name that we would like to search for
*/ */
Cypress.Commands.add('openStudy', patientName => { Cypress.Commands.add('openStudy', patientName => {
cy.openStudyList();
cy.get('#patientName').type(patientName);
cy.wait('@getStudies');
cy.get('#studyListData .studylistStudy', { timeout: 5000 })
.contains(patientName)
.first()
.click({ force: true });
});
/**
* Command to search for a modality and open the study.
*
* @param {string} modality - Modality type that we would like to search for
*/
Cypress.Commands.add('openStudyModality', modality => {
cy.initRouteAliases(); cy.initRouteAliases();
cy.visit('/'); cy.visit('/');
cy.get('#patientName').type(patientName); cy.get('#modalities')
.type(modality)
.wait(2000);
cy.get('.studylistStudy > .patientName', { timeout: 5000 }) cy.get('#studyListData')
.contains(patientName) .contains(modality)
.first()
.click(); .click();
});
// cy.get('.studylistStudy > .patientName') /**
// .as('patientResult') * Command to wait and check if a new page was loaded
// .should({ timeout: 3000 }, () => { *
// cy.contains(patientName).click(); * @param {string} url - part of the expected url. Default value is /viewer/
// }); */
Cypress.Commands.add('isPageLoaded', (url = '/viewer/') => {
return cy.location('pathname', { timeout: 60000 }).should('include', url);
});
Cypress.Commands.add('openStudyList', patientName => {
cy.initRouteAliases();
cy.visit('/');
cy.wait('@getStudies');
}); });
/** /**
@@ -119,31 +146,35 @@ Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
//Command to wait DICOM image to load into the viewport //Command to wait DICOM image to load into the viewport
Cypress.Commands.add('waitDicomImage', (timeout = 20000) => { Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
cy.window() const loaded = cy.isPageLoaded();
.its('cornerstone')
.then({ timeout }, $cornerstone => {
return new Cypress.Promise(resolve => {
const onEvent = renderedEvt => {
const element = renderedEvt.detail.element;
element.removeEventListener('cornerstoneimagerendered', onEvent); if (loaded) {
$cornerstone.events.removeEventListener( cy.window()
'cornerstoneimagerendered', .its('cornerstone')
onEvent .then({ timeout }, $cornerstone => {
return new Cypress.Promise(resolve => {
const onEvent = renderedEvt => {
const element = renderedEvt.detail.element;
element.removeEventListener('cornerstoneimagerendered', onEvent);
$cornerstone.events.removeEventListener(
'cornerstoneimagerendered',
onEvent
);
resolve();
};
const onEnabled = enabledEvt => {
const element = enabledEvt.detail.element;
element.addEventListener('cornerstoneimagerendered', onEvent);
};
$cornerstone.events.addEventListener(
'cornerstoneelementenabled',
onEnabled
); );
resolve(); });
};
const onEnabled = enabledEvt => {
const element = enabledEvt.detail.element;
element.addEventListener('cornerstoneimagerendered', onEvent);
};
$cornerstone.events.addEventListener(
'cornerstoneelementenabled',
onEnabled
);
}); });
}); }
}); });
//Command to reset and clear all the changes made to the viewport //Command to reset and clear all the changes made to the viewport
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ohif/viewer", "name": "@ohif/viewer",
"version": "1.10.2", "version": "1.11.2",
"description": "OHIF Viewer", "description": "OHIF Viewer",
"author": "OHIF Contributors", "author": "OHIF Contributors",
"license": "MIT", "license": "MIT",
@@ -27,7 +27,7 @@
"dev:viewer": "yarn run dev", "dev:viewer": "yarn run dev",
"start": "yarn run dev", "start": "yarn run dev",
"test:e2e": "cypress open", "test:e2e": "cypress open",
"test:e2e:ci": "percy exec -- cypress run", "test:e2e:ci": "percy exec -- cypress run --browser chrome",
"test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci", "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:e2e:serve": "serve -n -l 3000 -s dist",
"test:unit": "jest --watchAll", "test:unit": "jest --watchAll",
@@ -50,9 +50,9 @@
"@ohif/extension-dicom-html": "^1.0.0", "@ohif/extension-dicom-html": "^1.0.0",
"@ohif/extension-dicom-microscopy": "^0.50.6", "@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.0", "@ohif/extension-dicom-pdf": "^1.0.0",
"@ohif/extension-vtk": "^0.52.12", "@ohif/extension-vtk": "^0.52.15",
"@ohif/i18n": "^0.51.0", "@ohif/i18n": "^0.51.0",
"@ohif/ui": "^0.55.0", "@ohif/ui": "^0.56.0",
"@tanem/react-nprogress": "^1.1.25", "@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"core-js": "^3.2.1", "core-js": "^3.2.1",
@@ -22,6 +22,8 @@ class DicomStorePickerModal extends Component {
handleEvent = data => { handleEvent = data => {
const servers = GoogleCloudUtilServers.getServers(data, data.dicomstore); const servers = GoogleCloudUtilServers.getServers(data, data.dicomstore);
this.props.setServers(servers); this.props.setServers(servers);
// Force auto close
this.props.onClose();
}; };
render() { render() {
@@ -8,6 +8,7 @@ import { StudyList } from '@ohif/ui';
import ConnectedHeader from '../connectedComponents/ConnectedHeader.js'; import ConnectedHeader from '../connectedComponents/ConnectedHeader.js';
import * as RoutesUtil from '../routes/routesUtil'; import * as RoutesUtil from '../routes/routesUtil';
import moment from 'moment'; import moment from 'moment';
import isEqual from 'lodash.isequal';
import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader'; import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader';
import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker'; import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker';
import filesToStudies from '../lib/filesToStudies.js'; import filesToStudies from '../lib/filesToStudies.js';
@@ -20,8 +21,9 @@ import AppContext from '../context/AppContext';
class StudyListWithData extends Component { class StudyListWithData extends Component {
static contextType = AppContext; static contextType = AppContext;
state = { state = {
searchData: {}, searchOutdated: true,
studies: [], studies: [],
searchingStudies: false,
error: null, error: null,
modalComponentId: null, modalComponentId: null,
}; };
@@ -62,6 +64,7 @@ class StudyListWithData extends Component {
if (!this.props.server && appConfig.enableGoogleCloudAdapter) { if (!this.props.server && appConfig.enableGoogleCloudAdapter) {
this.setState({ this.setState({
modalComponentId: 'DicomStorePicker', modalComponentId: 'DicomStorePicker',
searchOutdated: false,
}); });
} else { } else {
this.searchForStudies({ this.searchForStudies({
@@ -71,21 +74,33 @@ class StudyListWithData extends Component {
} }
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps, prevState) {
if (!this.state.searchData && !this.state.studies) { const hasNewServer = !isEqual(this.props.server, prevProps.server);
this.searchForStudies();
} const { searchOutdated, searchingStudies } = this.state;
if (this.props.server !== prevProps.server) {
this.setState({ if (!searchingStudies) {
modalComponentId: null, if (hasNewServer) {
searchData: null, const { appConfig = {} } = this.context;
studies: null,
}); const newState = {
searchOutdated: true,
studies: null,
};
if (appConfig.enableGoogleCloudAdapter) {
newState.modalComponentId = null;
}
this.setState(newState);
}
if (searchOutdated) {
this.searchForStudies();
}
} }
} }
searchForStudies = (searchData = StudyListWithData.defaultSearchData) => { searchForStudies = (searchData = StudyListWithData.defaultSearchData) => {
const { server } = this.props; const { server = {} } = this.props;
const filter = { const filter = {
patientId: searchData.patientId, patientId: searchData.patientId,
patientName: searchData.patientName, patientName: searchData.patientName,
@@ -105,7 +120,9 @@ class StudyListWithData extends Component {
// TODO: add sorting // TODO: add sorting
const promise = OHIF.studies.searchStudies(server, filter); const promise = OHIF.studies.searchStudies(server, filter);
// Render the viewer when the data is ready this.setState({
searchingStudies: true,
});
promise promise
.then(studies => { .then(studies => {
if (!studies) { if (!studies) {
@@ -150,11 +167,15 @@ class StudyListWithData extends Component {
this.setState({ this.setState({
studies: sortedStudies, studies: sortedStudies,
searchingStudies: false,
searchOutdated: false,
}); });
}) })
.catch(error => { .catch(error => {
this.setState({ this.setState({
error: true, error: true,
searchingStudies: false,
searchOutdated: false,
}); });
throw new Error(error); throw new Error(error);
@@ -208,8 +229,6 @@ class StudyListWithData extends Component {
if (this.state.error) { if (this.state.error) {
return <div>Error: {JSON.stringify(this.state.error)}</div>; return <div>Error: {JSON.stringify(this.state.error)}</div>;
} else if (this.state.studies === null && !this.state.modalComponentId) {
return <div>Loading...</div>;
} }
let healthCareApiButtons = null; let healthCareApiButtons = null;
@@ -240,8 +259,9 @@ class StudyListWithData extends Component {
const studyList = ( const studyList = (
<div className="paginationArea"> <div className="paginationArea">
{this.state.studies ? ( {this.state.studies || this.state.searchingStudies ? (
<StudyList <StudyList
loading={this.state.searchingStudies}
studies={this.state.studies} studies={this.state.studies}
studyListFunctionsEnabled={this.props.studyListFunctionsEnabled} studyListFunctionsEnabled={this.props.studyListFunctionsEnabled}
onImport={this.onImport} onImport={this.onImport}
+457 -391
View File
File diff suppressed because it is too large. Load diff