Compare commits

...
Author SHA1 Message Date
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
ohif-bot 04ffe3ec76 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.12
 - @ohif/viewer@1.10.2
2019-10-18 10:16:06 +00:00
James Petts 9cdf8b690b * feat: 🎸 Progressive volume loading for vtk viewport (#1055)
* feat: 🎸 Progressive volume loading for vtk viewport

Adds progressive volume loading for vtkjs viewport whilst the frames are
streamed from PACs and rebuilt in the volume.

Closes: closes #1051

* Update to react-vtkjs-viewport 0.3.0
2019-10-18 11:43:23 +02:00
Mirna Silva 8d49b31d03 test: Improviment on VTK beforeEach hook (#1057) 2019-10-16 15:59:50 -04:00
Danny Brown e4cfae7ad9 docs: add percy badge to readme 2019-10-16 11:51:45 -04:00
ohif-bot e53041f630 chore(release): publish [skip ci]
- @ohif/viewer@1.10.1
2019-10-16 15:46:46 +00:00
Mirna Silva 35c54eac56 test: Test cases for extensions: VTK, HTML and PDF (#1036)
* Test cases for extensions: VTK, HTML and PDF

* Test Cases: Delete measurement, Relabel and Description through Contextmenu

* Fix: PDF, HTML and VTK extensions

* CI: running into different folders for PWA and Script-tag tests

* CI: running into different folders for PWA and Script-tag tests

* CI: running into different folders for PWA and Script-tag tests

* Fix for VTK tests and Cornerstone Eraser tool

* Removed VTK alias from Cornerstone aliases initialization

* Temporary fix for Eraser tool

* Test workaround

* Use juno for vtk

* Shift VTK tests to PWA folder
2019-10-16 11:39:41 -04:00
ohif-bot e7be573328 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.11
 - @ohif/i18n@0.51.0
 - @ohif/ui@0.55.0
 - @ohif/viewer@1.10.0
2019-10-15 18:59:20 +00:00
Danny Brown 8a7706b286 test: percy visual testing (#1053)
* test: percy visual testing init

* Try PR checks with custom command

* Only run percy visual tests for PWA on merge for now
2019-10-15 14:52:23 -04:00
Rodrigo Antinarelli c217b8bbde feat: Add browser info and app version (#1046)
* feat: Add browser info and app version

* quick fix

* lock file

* add build_num

* add screenshot e2e test

* quick test build_num

* revert header

* include i18n texts
2019-10-15 13:09:14 -04:00
ohif-bot 74f3cbc4b2 chore(release): publish [skip ci]
- @ohif/viewer@1.9.1
2019-10-15 16:10:17 +00:00
James Petts d091cd604c fix: 🐛 Remove debugger statement left in from last PR (#1052) 2019-10-15 18:03:29 +02:00
ohif-bot 3a4280163d chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.10
 - @ohif/core@1.4.0
 - @ohif/viewer@1.9.0
2019-10-15 15:18:15 +00:00
James Petts 2d75e01ea0 feat: 🎸 Only allow reconstruction of datasets that make sense (#1010)
* feat: 🎸 Only allow reconstruction of datasets that make sense

Only allow reconstruction of datasets which are imaging data, that have
frames in the same orientation, with the same size and make sense to be
reconstructed in 3D.

Closes: #561
2019-10-15 17:08:53 +02:00
Danny Brown 5c5a49486d docs: more specific location for build output 2019-10-15 08:56:54 -04:00
ohif-bot a8a57132a7 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.9
 - @ohif/ui@0.54.0
 - @ohif/viewer@1.8.0
2019-10-14 15:46:13 +00:00
Rodrigo Antinarelli 92c8996ed4 feat: Notification Service (#1011)
* WIP - Notification Service

* add snackbar provider into the main App component

* feat: Notification Service

* revert unmodified file

* Update DicomHtmlViewport.js

* update css classnames

* revert toolbarRow

* fix revert toolbarRow

* styles adjustments and hide method

* remove unecessary default options

* minor fixes

* fix long truncated messages

* move SnackbarContext to ui project, rename, export
2019-10-14 11:39:31 -04:00
ohif-bot f07dca5fa4 chore(release): publish [skip ci]
- @ohif/viewer@1.7.0
2019-10-14 12:50:19 +00:00
Karthik Rao c99e0d8a23 feat: Implement a 'Exit 2D MPR' button in the toolbar
* feat: Implement a 'Exit 2D MPR' button in the toolbar to act as a toggle when '2D MPR' button is clicked

* Move logic to exit MPR and toggling into ConnectedPluginSwitch and PluginSwitch. Also, discard the ConnectedExitPluginSwitch and ExitPluginSwitch components.

* Update ToolbarRow.js
2019-10-14 08:41:29 -04:00
ohif-bot 84144bc3ef chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.8
 - @ohif/core@1.3.2
 - @ohif/viewer@1.6.3
2019-10-14 11:15:08 +00:00
Erik Ziegler 5cbdd0c616 ci: Skip copying URL to clipboard on headless CI servers (#1043) 2019-10-14 13:08:59 +02:00
Chris McGee fc015326c7 fix: Return display sets in StudyMetadata._createDisplaySetsForSeries (#1042) 2019-10-13 20:53:01 -04:00
ohif-bot 01860b6094 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.7
 - @ohif/core@1.3.1
 - @ohif/ui@0.53.4
 - @ohif/viewer@1.6.2
2019-10-11 18:56:39 +00:00
Danny Brown cfdf827ff0 ci: fix broken build (#1033) 2019-10-11 14:50:12 -04:00
Danny Brown ceb6837153 ci: speed up script tag e2e tests (#1032)
* show package bundle progress

* Ability to run "quick builds" for faster feedback on status checks (e2e)

* ci: speed up PR checks

* specify quick build value
2019-10-11 14:26:24 -04:00
Danny Brown b01b0108b0 refactor: layout manager to viewer (#1026)
* docs: formatting

* Prefer numRows and numColumns to percentage width/height for layout

* fix: LayoutManager --> ViewportGrid (get out of UI component library)

* docs: remove outdated docs

* Don't expose ExampleDropTarget

* Revert thumnail entry drag source removal

* Update screaming tests

* fix drag-n-drop

* fix vtk mpr2d CC: @jamesapetts

* remove setSingleLayoutData

* remove vtk qualifier
2019-10-11 12:03:59 -04:00
Erik Ziegler d6862e7418 ci: Set cache key to only use yarn.lock checksum (#1031)
* ci: Set cache key to only use yarn.lock checksum

* Use increasingly general pattern for restoring the cache

* Avoid running script tag e2e tests twice

* Performance improvements for CI

* Valid YAML

* Valid YAML?

* Valid YAML!!!!

* Update Node image, update yarn version

* circle

* serve
2019-10-11 11:40:21 -04:00
ohif-bot beb7083e55 chore(release): publish [skip ci]
- @ohif/viewer@1.6.1
2019-10-11 13:13:57 +00:00
Erik Ziegler 412fe4e23f fix: Switch token storage back to localStorage because in-memory was annoying for end users (#1030) 2019-10-11 14:53:49 +02:00
87 changed files with 3245 additions and 1505 deletions

No files matched your search

+95 -37
View File
@@ -17,46 +17,107 @@ orbs:
defaults: &defaults
docker:
- image: circleci/node:10.16.0
- image: circleci/node:12.9.1
environment:
TERM: xterm # Enable colors in term
working_directory: ~/repo
jobs:
CHECKOUT:
UNIT_TESTS:
<<: *defaults
steps:
# Update yarn
- run: yarn -v
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
# 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-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-packages-v1-{{ .Branch }}-
- yarn-packages-v1-
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
# Persist :+1:
- persist_to_workspace:
root: ~/repo
paths: .
key: yarn-packages-{{ checksum "yarn.lock" }}
UNIT_TESTS:
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: yarn run test:unit:ci
# PLATFORM/VIEWER
- run:
name: 'VIEWER: Combine report output'
command: |
viewerCov="/home/circleci/repo/platform/viewer/coverage"
touch "${viewerCov}/reports"
cat "${viewerCov}/clover.xml" >> "${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
cat "${viewerCov}/lcov.info" >>"${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/viewer/coverage/reports'
flags: 'viewer'
# PLATFORM/CORE
- run:
name: 'CORE: Combine report output'
command: |
coreCov="/home/circleci/repo/platform/core/coverage"
touch "${coreCov}/reports"
cat "${coreCov}/clover.xml" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
cat "${coreCov}/lcov.info" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
UNIT_TESTS_PERSIST_WORKSPACE:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
# Update yarn
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
# 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
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# RUN TESTS
- run:
@@ -92,7 +153,7 @@ jobs:
flags: 'core'
# Persist :+1:
- persist_to_workspace:
- persist_to_workspace: &persist_to_workspace
root: ~/repo
paths: .
@@ -143,7 +204,7 @@ jobs:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
docker_layer_caching: false
- run:
name: Build and push Docker image
command: |
@@ -217,16 +278,13 @@ workflows:
# PULL REQUESTS
PR_CHECKS:
jobs:
- CHECKOUT:
- UNIT_TESTS:
filters:
branches:
ignore:
- master
- feature/*
- hotfix/*
- UNIT_TESTS:
requires:
- CHECKOUT
# E2E: PWA
- cypress/run:
name: 'E2E: PWA'
@@ -236,10 +294,10 @@ workflows:
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: yarn run build:e2e
start: yarn run test:e2e:dist
build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
@@ -255,10 +313,11 @@ workflows:
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: yarn run build:package
start: yarn run test:e2e:dist
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
@@ -269,13 +328,10 @@ workflows:
# MERGE TO MASTER
RELEASE:
jobs:
- CHECKOUT:
- UNIT_TESTS_PERSIST_WORKSPACE:
filters:
branches:
only: master
- UNIT_TESTS:
requires:
- CHECKOUT
# E2E: PWA + Persist
- cypress/run:
name: 'E2E: PWA'
@@ -286,10 +342,11 @@ workflows:
record: true
store_artifacts: true
working_directory: platform/viewer
build: yarn run build:e2e
start: yarn run test:e2e:dist
build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
command: 'yarn run test:e2e:ci'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
@@ -297,7 +354,7 @@ workflows:
- store_test_results:
path: cypress/results
requires:
- UNIT_TESTS
- UNIT_TESTS_PERSIST_WORKSPACE
# E2E: script-tag
- cypress/run:
name: 'E2E: Script Tag'
@@ -307,10 +364,11 @@ workflows:
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: yarn run build:package
start: yarn run test:e2e:dist
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
@@ -318,7 +376,7 @@ workflows:
- store_test_results:
path: cypress/results
requires:
- UNIT_TESTS
- UNIT_TESTS_PERSIST_WORKSPACE
# Update NPM
- NPM_PUBLISH:
requires:
+1
View File
@@ -0,0 +1 @@
PERCY_TOKEN=<your token here>
+1
View File
@@ -21,6 +21,7 @@ npm-debug.log
package-lock.json
yarn-error.log
.DS_Store
.env
# Common Example Data Directories
sampledata/
+56 -4
View File
@@ -1,23 +1,54 @@
// ~~ ENV
const dotenv = require('dotenv');
//
const path = require('path');
const webpack = require('webpack');
const PACKAGE = require('../platform/viewer/package.json');
// ~~ RULES
const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
// ~~ PLUGINS
const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
const QUICK_BUILD = process.env.QUICK_BUILD;
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
//
dotenv.config();
module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
if (!process.env.NODE_ENV) {
throw new Error('process.env.NODE_ENV not set');
}
const mode =
process.env.NODE_ENV === 'production' ? 'production' : 'development';
const mode = NODE_ENV === 'production' ? 'production' : 'development';
const isProdBuild = NODE_ENV === 'production';
const isQuickBuild = QUICK_BUILD === 'true';
return {
mode,
const config = {
mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
entry: {
app: `${SRC_DIR}/index.js`,
},
optimization: {
minimize: isProdBuild,
sideEffects: true,
},
context: SRC_DIR,
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
module: {
rules: [
transpileJavaScriptRule(mode),
@@ -45,6 +76,8 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
}),
],
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
@@ -53,4 +86,23 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
fs: 'empty',
},
};
if (isProdBuild) {
config.optimization.minimizer = [
new TerserJSPlugin({
// Supports:
// source-map and inline-source-map
sourceMap: isProdBuild && !isQuickBuild,
parallel: true,
terserOptions: {},
}),
];
}
if (isQuickBuild) {
config.optimization.minimize = false;
config.devtool = false;
}
return config;
};
+3
View File
@@ -26,6 +26,7 @@
[![Netlify Status][netlify-image]][netlify-url]
[![CircleCI][circleci-image]][circleci-url]
[![codecov][codecov-image]][codecov-url]
[![This project is using Percy.io for visual regression testing.][percy-image]](percy-url)
[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors)
<!-- prettier-ignore-end -->
@@ -288,6 +289,8 @@ MIT © [OHIF](https://github.com/OHIF)
[docker-image-url]: https://hub.docker.com/r/ohif/viewer
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
[license-url]: LICENSE
[percy-image]: https://percy.io/static/images/percy-badge.svg
[percy-url]: https://percy.io/Open-Health-Imaging-Foundation/OHIF-Viewer
<!-- Links -->
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
+13 -6
View File
@@ -140,8 +140,8 @@ children: PropTypes.node,
customProps: PropTypes.object
```
Viewport components are managed by the `LayoutManager`. Which Viewport component
is used depends on:
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
component is used depends on:
- The Layout Configuration
- Registered SopClassHandlers
@@ -159,8 +159,8 @@ For a complete example implementation,
An extension can register a Toolbar Module by providing a `getToolbarModule()`
method that returns a React Component. The component does not receive any props.
If you want to modify or react to state, you will need to connect to the redux
store.
The given toolbar must determine its set of elements and the context of them. The set of elements will be listed on toolbar `definitions`.
store. The given toolbar must determine its set of elements and the context of
them. The set of elements will be listed on toolbar `definitions`.
![Toolbar Extension](../assets/img/extensions-toolbar.gif)
@@ -173,7 +173,11 @@ For a complete example implementation,
##### Toolbar Custom Component
Toolbar elements can define its own custom react component to be consumed when rendering it. So far, it accepts `Functional` and `Class` Components. For that, you just need to expose your `CustomToolbarComponent` as the value of key `CustomComponent`. In case the property `CustomComponent` is not present, a default toolbar component will be used to render it. See bellow
Toolbar elements can define its own custom react component to be consumed when
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
you just need to expose your `CustomToolbarComponent` as the value of key
`CustomComponent`. In case the property `CustomComponent` is not present, a
default toolbar component will be used to render it. See bellow
```js
definitions: [
@@ -188,8 +192,11 @@ definitions: [
]
```
`CustomComponent` components will receive the following props:
- parentContext: parent context. (In most of the cases it will be a ToolbarRow instance)
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
instance)
- toolbarClickCallback: callback method when clicking on toolbar
- button: its own definition object
- key: react key prop
@@ -48,7 +48,7 @@ If everything worked as expected, you should have a new `dist/` directory in the
project's folder. It should roughly resemble the following:
```bash
dist/
<root>platform/viewer/dist/
├── app-config.js
├── app.bundle.js
├── app.css
+56
View File
@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.10...@ohif/extension-vtk@0.52.11) (2019-10-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.9...@ohif/extension-vtk@0.52.10) (2019-10-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.8...@ohif/extension-vtk@0.52.9) (2019-10-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.7...@ohif/extension-vtk@0.52.8) (2019-10-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.6...@ohif/extension-vtk@0.52.7) (2019-10-11)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.5...@ohif/extension-vtk@0.52.6) (2019-10-11)
**Note:** Version bump only for package @ohif/extension-vtk
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.52.6",
"version": "0.52.13",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -48,11 +48,11 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"react-vtkjs-viewport": "^0.1.6"
"react-vtkjs-viewport": "^0.3.1"
},
"devDependencies": {
"@ohif/core": "^1.3.0",
"@ohif/ui": "^0.53.3",
"@ohif/core": "^1.4.0",
"@ohif/ui": "^0.55.0",
"cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
+7 -7
View File
@@ -1,6 +1,6 @@
import OHIF from "@ohif/core";
import { View2D } from "react-vtkjs-viewport";
import { connect } from "react-redux";
import OHIF from '@ohif/core';
import { View2D } from 'react-vtkjs-viewport';
import { connect } from 'react-redux';
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
@@ -24,7 +24,7 @@ const mapStateToProps = (state, ownProps) => {
// Hopefully this doesn't break anything under the hood for this one
// activeTool: activeButton && activeButton.command,
...dataFromStore,
enableStackPrefetch: isActive
enableStackPrefetch: isActive,
};
};
@@ -38,7 +38,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
setViewportSpecificData: data => {
dispatch(setViewportSpecificData(viewportIndex, data));
}
},
};
};
@@ -63,10 +63,10 @@ const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
// Store the API details for later
//setViewportSpecificData({ vtkApi: api });
if (afterCreation && typeof afterCreation === "function") {
if (afterCreation && typeof afterCreation === 'function') {
afterCreation(api);
}
}
},
};
return props;
};
+4 -4
View File
@@ -3,15 +3,15 @@
}
.loadingIndicator {
background-color: rgba(0, 0, 0, 0.75);
font-size: 18px;
height: 100%;
background-color: rgba(0, 0, 0, 0);
font-size: 8px;
height: 20%;
overflow: hidden;
position: absolute;
top: 0;
width: 100%;
z-index: 1;
/* Necessary for click-through to cornerstone element below */
/* Necessary for click-through to vtkjs element below */
pointer-events: none;
}
+4 -3
View File
@@ -1,7 +1,7 @@
import './LoadingIndicator.css';
import React, { PureComponent } from 'react';
import { withTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
class LoadingIndicator extends PureComponent {
@@ -35,7 +35,8 @@ class LoadingIndicator extends PureComponent {
<div className="imageViewerLoadingIndicator loadingIndicator">
<div className="indicatorContents">
<p>
Loading... <i className="fa fa-spin fa-circle-o-notch fa-fw" />{' '}
{this.props.t('Reformatting')}...
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
{percComplete}
</p>
</div>
@@ -46,4 +47,4 @@ class LoadingIndicator extends PureComponent {
}
}
export default LoadingIndicator;
export default withTranslation('Common')(LoadingIndicator);
+103 -52
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 vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
const { StackManager } = OHIF.utils;
@@ -172,47 +171,45 @@ class OHIFVTKViewport extends Component {
default:
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
const loadImageDataPromise = loadImageData(imageDataObject);
return loadImageDataPromise.then(() => {
return {
data: imageDataObject.vtkImageData,
};
});
return imageDataObject;
}
};
getOrCreateVolume(data, displaySetInstanceUid) {
getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
if (volumeCache[displaySetInstanceUid]) {
return volumeCache[displaySetInstanceUid];
}
const { vtkImageData, imageMetaData0 } = imageDataObject;
const { windowWidth, windowCenter, modality } = imageMetaData0;
let lower;
let upper;
if (modality === 'PT') {
// For PET just set the range to 0-5 SUV
lower = 0;
upper = 5;
} else {
lower = windowCenter - windowWidth / 2.0;
upper = windowCenter + windowWidth / 2.0;
}
const volumeActor = vtkVolume.newInstance();
const volumeMapper = vtkVolumeMapper.newInstance();
volumeActor.setMapper(volumeMapper);
volumeMapper.setInputData(data);
volumeMapper.setInputData(vtkImageData);
const range = data
.getPointData()
.getScalars()
.getRange();
// TODO: For PET we might want to just set this to 0-5 SUV
volumeActor
.getProperty()
.getRGBTransferFunction(0)
.setRange(range[0], range[1]);
.setRange(lower, upper);
// TODO: Should look into implementing autoAdjustSampleDistance in vtk
const sampleDistance =
1.2 *
Math.sqrt(
data
.getSpacing()
.map(v => v * v)
.reduce((a, b) => a + b, 0)
);
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
const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6;
volumeMapper.setSampleDistance(sampleDistance);
@@ -221,7 +218,7 @@ class OHIFVTKViewport extends Component {
return volumeActor;
}
async setStateFromProps() {
setStateFromProps() {
const { studies, displaySet } = this.props.viewportData;
const {
studyInstanceUid,
@@ -238,8 +235,7 @@ class OHIFVTKViewport extends Component {
}
const sopClassUid = sopClassUids[0];
let { data, labelmap } = await this.getViewportData(
const imageDataObject = this.getViewportData(
studies,
studyInstanceUid,
displaySetInstanceUid,
@@ -248,29 +244,51 @@ class OHIFVTKViewport extends Component {
frameIndex
);
this.imageDataObject = imageDataObject;
// TODO: Temporarily disabling this since it is not yet
// being used and hurts performance significantly.
/*if (!labelmap) {
labelmap = createLabelMapImageData(data);
}*/
const volumeActor = this.getOrCreateVolume(data, displaySetInstanceUid);
const volumeActor = this.getOrCreateVolume(
imageDataObject,
displaySetInstanceUid
);
this.setState({
volumes: [volumeActor],
paintFilterBackgroundImageData: data,
paintFilterLabelMapImageData: labelmap,
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
paintFilterLabelMapImageData: null, // TODO
percentComplete: 0,
});
this.setState(
{
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
paintFilterLabelMapImageData: null, // TODO
percentComplete: 0,
},
() => {
this.loadProgressively(imageDataObject);
// TODO: There must be a better way to do this.
// We do this so that if all the data is available the react-vtkjs-viewport
// Will render _something_ before the volumes are set and the volume
// Construction that happens in react-vtkjs-viewport locks up the CPU.
setTimeout(() => {
this.setState({
volumes: [volumeActor],
});
}, 200);
}
);
}
componentDidMount() {
this.setStateFromProps();
}
componentWillUnmount() {
vtkViewportSubscriptionManager.unsubscribe(this.props.viewportIndex);
}
componentDidUpdate(prevProps) {
const { studies, displaySet } = this.props.viewportData;
const prevDisplaySet = prevProps.viewportData.displaySet;
@@ -285,6 +303,37 @@ class OHIFVTKViewport extends Component {
}
}
loadProgressively(imageDataObject) {
loadImageData(imageDataObject);
const { isLoading, insertPixelDataPromises } = imageDataObject;
const numberOfFrames = insertPixelDataPromises.length;
if (!isLoading) {
this.setState({ isLoaded: true });
return;
}
insertPixelDataPromises.forEach(promise => {
promise.then(numberProcessed => {
const percentComplete = Math.floor(
(numberProcessed * 100) / numberOfFrames
);
if (percentComplete !== this.state.percentComplete) {
this.setState({
percentComplete,
});
}
});
});
Promise.all(insertPixelDataPromises).then(() => {
this.setState({ isLoaded: true });
});
}
render() {
let childrenWithProps = null;
@@ -302,21 +351,23 @@ class OHIFVTKViewport extends Component {
return (
<>
{this.state.volumes ? (
<ConnectedVTKViewport
volumes={this.state.volumes}
paintFilterLabelMapImageData={
this.state.paintFilterLabelMapImageData
}
paintFilterBackgroundImageData={
this.state.paintFilterBackgroundImageData
}
viewportIndex={this.props.viewportIndex}
/>
) : (
<div style={style}>
<LoadingIndicator />
</div>
<div style={style}>
{!this.state.isLoaded && (
<LoadingIndicator percentComplete={this.state.percentComplete} />
)}
{this.state.volumes && (
<ConnectedVTKViewport
volumes={this.state.volumes}
paintFilterLabelMapImageData={
this.state.paintFilterLabelMapImageData
}
paintFilterBackgroundImageData={
this.state.paintFilterBackgroundImageData
}
viewportIndex={this.props.viewportIndex}
/>
)}
</div>
)}
{childrenWithProps}
</>
+114 -176
View File
@@ -4,69 +4,25 @@ import {
vtkInteractorStyleMPRSlice,
vtkInteractorStyleMPRRotate,
vtkSVGCrosshairsWidget,
vtkSVGWidgetManager,
} from 'react-vtkjs-viewport';
import setMPRLayout from './utils/setMPRLayout.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';
const { BlendMode } = Constants;
// TODO: Put this somewhere else
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) {
const { layout, viewportSpecificData, activeViewportIndex } = viewports;
const {
numRows,
numColumns,
layout,
viewportSpecificData,
activeViewportIndex,
} = viewports;
const currentData = layout.viewports[activeViewportIndex];
if (currentData && currentData.plugin === 'vtk') {
@@ -84,6 +40,8 @@ async function _getActiveViewportVTKApi(viewports) {
api = await setViewportToVTK(
displaySet,
activeViewportIndex,
numRows,
numColumns,
layout,
viewportSpecificData
);
@@ -97,8 +55,6 @@ async function _getActiveViewportVTKApi(viewports) {
function _setView(api, sliceNormal, viewUp) {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const istyle = renderWindow.getInteractor().getInteractorStyle();
istyle.setSliceNormal(...sliceNormal);
istyle.setViewUp(...viewUp);
@@ -106,41 +62,6 @@ function _setView(api, sliceNormal, viewUp) {
renderWindow.render();
}
function switchMPRInteractors(api, istyle) {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
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;
}
renderWindow.getInteractor().setInteractorStyle(istyle);
// TODO: Not sure why this is required the second time this function is called
istyle.setInteractor(renderWindow.getInteractor());
if (currentViewport) {
istyle.setViewport(currentViewport);
}
if (istyle.getVolumeMapper() !== api.volumes[0]) {
if (currentSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
istyle.setVolumeMapper(api.volumes[0]);
}
}
const actions = {
axial: async ({ viewports }) => {
const api = await _getActiveViewportVTKApi(viewports);
@@ -167,52 +88,48 @@ const actions = {
apis.forEach(api => {
const istyle = vtkInteractorStyleMPRRotate.newInstance();
switchMPRInteractors(api, istyle);
api.setInteractorStyle({ istyle });
});
},
enableCrosshairsTool: () => {
apis.forEach((api, index) => {
apis.forEach((api, apiIndex) => {
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
switchMPRInteractors(api, istyle);
istyle.setCallback(getCrosshairCallbackForIndex(index));
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
});
},
enableLevelTool: () => {
const callbacks = {
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
renderWindow.render();
api.updateVOI(windowWidth, windowCenter);
});
},
};
apis.forEach(api => {
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
switchMPRInteractors(api, istyle);
api.setInteractorStyle({ istyle, callbacks });
});
},
setSlabThickness: ({ slabThickness }) => {
currentSlabThickness = slabThickness;
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
if (istyle.setSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
renderWindow.render();
api.setSlabThickness(slabThickness);
});
},
changeSlabThickness: ({ change }) => {
currentSlabThickness += change;
currentSlabThickness = Math.max(currentSlabThickness, 0.1);
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
if (istyle.setSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
renderWindow.render();
api.setSlabThickness(slabThickness);
});
},
setBlendModeToComposite: () => {
@@ -220,13 +137,15 @@ const actions = {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const slabThickness = api.getSlabThickness();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToComposite) {
mapper.setBlendModeToComposite();
}
if (istyle.setSlabThickness) {
istyle.setSlabThickness(defaultSlabThickness);
istyle.setSlabThickness(slabThickness);
}
renderWindow.render();
});
@@ -244,7 +163,6 @@ const actions = {
setBlendMode: ({ blendMode }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
api.volumes[0].getMapper().setBlendMode(blendMode);
@@ -252,78 +170,60 @@ const actions = {
});
},
mpr2d: async ({ viewports }) => {
// TODO push a lot of this backdoor logic lower down to the library level.
const displaySet =
viewports.viewportSpecificData[viewports.activeViewportIndex];
let apiByViewport;
// Get current VOI if cornerstone viewport.
const cornerstoneVOI = getVOIFromCornerstoneViewport(displaySet);
const viewportProps = [
{
//Axial
orientation: {
sliceNormal: [0, 0, 1],
viewUp: [0, -1, 0],
},
},
{
// Sagital
orientation: {
sliceNormal: [1, 0, 0],
viewUp: [0, 0, 1],
},
},
{
// Coronal
orientation: {
sliceNormal: [0, 1, 0],
viewUp: [0, 0, 1],
},
},
];
try {
apiByViewport = await setMPRLayout(displaySet);
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
} catch (error) {
throw new Error(error);
}
apis = apiByViewport;
if (cornerstoneVOI) {
setVOI(cornerstoneVOI);
}
const rgbTransferFunction = apiByViewport[0].volumes[0]
.getProperty()
.getRGBTransferFunction(0);
const onModifiedSubscription = rgbTransferFunction.onModified(() => {
apiByViewport.forEach(a => {
const renderWindow = a.genericRenderWindow.getRenderWindow();
renderWindow.render();
});
});
vtkViewportSubscriptionManager.pushSubscription(0, onModifiedSubscription);
apiByViewport.forEach((api, index) => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
// Add widgets and set default interactorStyle of each viewport.
apis.forEach((api, apiIndex) => {
api.addSVGWidget(
vtkSVGCrosshairsWidget.newInstance(),
'crosshairsWidget'
);
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
renderWindow.getInteractor().setInteractorStyle(istyle);
istyle.setVolumeMapper(api.volumes[0]);
istyle.setCallback(getCrosshairCallbackForIndex(index));
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,
};
switch (index) {
default:
case 0:
//Axial
istyle.setSliceNormal(0, 0, 1);
istyle.setViewUp(0, -1, 0);
break;
case 1:
// sagittal
istyle.setSliceNormal(1, 0, 0);
istyle.setViewUp(0, 0, 1);
break;
case 2:
// Coronal
istyle.setSliceNormal(0, 1, 0);
istyle.setViewUp(0, 0, 1);
break;
}
renderWindow.render();
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
});
},
};
@@ -413,3 +313,41 @@ export default {
definitions,
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);
});
}
+32 -36
View File
@@ -1,60 +1,56 @@
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
import setSingleLayoutData from './setSingleLayoutData.js';
export default function setMPRLayout(displaySet) {
export default function setMPRLayout(
displaySet,
viewportPropsArray,
numRows = 1,
numColumns = 1
) {
return new Promise((resolve, reject) => {
let viewports = [];
const rows = 1;
const columns = 3;
const numViewports = rows * columns;
const viewportSpecificData = {};
for (let i = 0; i < numViewports; i++) {
viewports.push({
height: `${100 / rows}%`,
width: `${100 / columns}%`,
});
const viewports = [];
const numViewports = numRows * numColumns;
if (viewportPropsArray && viewportPropsArray.length !== numViewports) {
reject(
new Error(
'viewportProps is supplied but its length is not equal to numViewports'
)
);
}
const viewportSpecificData = {};
for (let i = 0; i < numViewports; i++) {
viewports.push({});
viewportSpecificData[i] = displaySet;
viewportSpecificData[i].plugin = 'vtk';
}
const layout = {
viewports,
};
const viewportIndices = [0, 1, 2];
let updatedViewports = layout.viewports;
const apis = [];
viewportIndices.forEach(viewportIndex => {
apis[viewportIndex] = null;
/*const currentData = layout.viewports[viewportIndex];
if (currentData && currentData.plugin === 'vtk') {
reject(new Error('Should not have reached this point??'));
}*/
const data = {
// plugin: 'vtk',
viewports.forEach((viewport, index) => {
apis[index] = null;
const viewportProps = viewportPropsArray[index];
viewports[index] = Object.assign({}, viewports[index], {
vtk: {
mode: 'mpr', // TODO: not used
afterCreation: api => {
apis[viewportIndex] = api;
apis[index] = api;
if (apis.every(a => !!a)) {
resolve(apis);
}
},
...viewportProps,
},
};
updatedViewports = setSingleLayoutData(
updatedViewports,
viewportIndex,
data
);
});
});
setLayoutAndViewportData(
{ viewports: updatedViewports },
{
numRows,
numColumns,
viewports,
},
viewportSpecificData
);
});
@@ -1,12 +0,0 @@
export default function setSingleLayoutData(
originalArray,
viewportIndex,
data
) {
const viewports = originalArray.slice();
const layoutData = Object.assign({}, viewports[viewportIndex], data);
viewports[viewportIndex] = layoutData;
return viewports;
}
+11 -10
View File
@@ -1,9 +1,10 @@
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
import setSingleLayoutData from './setSingleLayoutData.js';
export default function setViewportToVTK(
displaySet,
viewportIndex,
numRows,
numColumns,
layout,
viewportSpecificData
) {
@@ -13,7 +14,9 @@ export default function setViewportToVTK(
reject(new Error('Should not have reached this point??'));
}*/
const data = {
const viewports = layout.viewports.slice();
viewports[viewportIndex] = Object.assign({}, viewports[viewportIndex], {
// plugin: 'vtk',
vtk: {
mode: 'mpr', // TODO: not used
@@ -21,18 +24,16 @@ export default function setViewportToVTK(
resolve(api);
},
},
};
const updatedViewports = setSingleLayoutData(
layout.viewports,
viewportIndex,
data
);
});
const updatedViewportData = viewportSpecificData;
setLayoutAndViewportData(
{ viewports: updatedViewports },
{
numRows,
numColumns,
viewports,
},
updatedViewportData
);
});
@@ -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;
+4 -1
View File
@@ -13,7 +13,6 @@
"scripts": {
"cm": "npx git-cz",
"build": "lerna run build:viewer --stream",
"build:e2e": "lerna run build:viewer:e2e --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",
@@ -27,7 +26,9 @@
"test:unit": "jest --collectCoverage",
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
"test:e2e": "lerna run test:e2e --stream",
"test:e2e:ci": "lerna run test:e2e:ci --stream",
"test:e2e:dist": "lerna run test:e2e:dist --stream",
"test:e2e:serve": "lerna run test:e2e:serve --stream",
"see-changed": "lerna changed",
"docs:publish": "chmod +x ./build-and-publish-docs.sh && ./build-and-publish-docs.sh",
"release": "yarn run lerna:version && yarn run lerna:publish",
@@ -49,6 +50,7 @@
"@babel/plugin-transform-runtime": "^7.5.0",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@percy/cypress": "^2.2.0",
"babel-eslint": "9.x",
"babel-loader": "^8.0.6",
"babel-plugin-inline-react-svg": "1.1.0",
@@ -57,6 +59,7 @@
"copy-webpack-plugin": "^5.0.3",
"cross-env": "^5.2.0",
"css-loader": "^3.2.0",
"dotenv": "^8.1.0",
"eslint": "5.16.0",
"eslint-config-react-app": "^4.0.1",
"eslint-plugin-flowtype": "2.x",
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.2...@ohif/core@1.4.0) (2019-10-15)
### Features
* 🎸 Only allow reconstruction of datasets that make sense ([#1010](https://github.com/OHIF/Viewers/issues/1010)) ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes [#561](https://github.com/OHIF/Viewers/issues/561)
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.1...@ohif/core@1.3.2) (2019-10-14)
### Bug Fixes
* Return display sets in StudyMetadata._createDisplaySetsForSeries ([#1042](https://github.com/OHIF/Viewers/issues/1042)) ([fc01532](https://github.com/OHIF/Viewers/commit/fc01532))
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.0...@ohif/core@1.3.1) (2019-10-11)
**Note:** Version bump only for package @ohif/core
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.2.0...@ohif/core@1.3.0) (2019-10-11)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.3.0",
"version": "1.4.0",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
-167
View File
@@ -1,167 +0,0 @@
# Table of contents
In this document, some important objects are described. In the files there are comments that can help better undestand their methods and properties.
- [ResizeViewportManager object](#the-resize-viewport-manager-object)
- [ImageSet object](#the-image-set-object)
- [Layout Manager](#the-layout-manager-object)
- [Type Safe Collections](#the-type-safe-collections)
# The Resize Viewport Manager object
This object has multiple functions to manage window resize event. It relocates Dialogs, resizes viewport elements and scrollbars and some other UI components such as Study and Series Quick Switch, when available.
## Usage
It's only necessary to bind **handleResize** function to the window resize event as follows. The **ohif:viewerbase** package needs to be imported by the referring code as well.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
const ResizeViewportManager = new Viewerbase.ResizeViewportManager();
window.addEventListener('resize', ResizeViewportManager.getResizeHandler());
```
An example os its usage can be found in **ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js**.
# The Image Set object
An object that represents a list of images that are associated by any arbitrary criteria being thus content agnostic. Besides the main attributes (**images** and **uid**) it allows additional attributes to be appended to it (currently indiscriminately, but this should be changed).
## Usage
ImageSet constructor requires an array of SOP instances like in the example below. It's necessary to import **ohif:viewerbase**.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
const imageSet = new Viewerbase.ImageSet(sopInstances);
imageSet.setAttributes({
displaySetInstanceUid: imageSet.uid,
seriesInstanceUid: seriesData.seriesInstanceUid,
seriesNumber: seriesData.seriesNumber,
seriesDescription: seriesData.seriesDescription,
numImageFrames: instances.length,
frameRate: instance.getRawValue('x00181063'),
modality: seriesData.modality,
isMultiFrame: isMultiFrame(instance)
});
// Sort instances by InstanceNumber (0020,0013)
imageSet.sortBy((a, b) => {
return (parseInt(a.getRawValue('x00200013', 0)) || 0) - (parseInt(b.getRawValue('x00200013', 0)) || 0);
});
```
Each SOP instance in this example is an instance of **OHIFInstanceMetadata** object, which is a specialization of **InstanceMetadata**. To read more about the **Metadata API** click [here](metadata/).
# The Layout Manager object
Objects of this class are responsible for creating, organizing and maintaining (manage) viewport rendering. It creates a grid, positioning viewports accordingly to it's configuration keeping all viewports data (in **viewportData** property) for easy access from other components. It support many layout configurations and some of them were fully tested: 1x1, 1x2, 1x3, 2x1, 2x2, 2x3, 3x1, 3x2, 3x3. Other configurations may work as well.
Finally it provides some useful functions to move through viewports and zoom it.
## Usage
In order to use _LayoutManager_ the **ohif:viewerbase** package needs to be imported by the referring code and instantiated as follows. An example os its usage is in **ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js**.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Get an array of studies object. This function needs to be implemented, it does not exist.
const studies = getArrayOfStudiesObjects();
const parentElement = document.getElementById('layoutManagerTarget');
const LayoutManager = new Viewerbase.LayoutManager(parentElement, studies);
```
The default configuration is 1x1, and to change it just set **layoutProps** and call **updateViewports** to update the layout as follows.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Get an array of studies object. This function needs to be implemented, it does not exist.
const studies = getArrayOfStudiesObjects();
const parentElement = document.getElementById('layoutManagerTarget');
const LayoutManager = new LayoutManager(parentElement, studies);
// Set the layout proprerties to 2x2 layout
LayoutManager.layoutProps = {
rows: 2,
columns: 2
};
// It will render four viewports: two in each row.
LayoutManager.updateViewports();
```
The layoutManagerTarget element will have a new class **layout-2-2** (to allow further styling) and it's inner content will a new div#imageViewerViewports that has four inner elements like the following (some elements and attributes were removed for example purpose):
```html
<div class="viewportContainer active" style="height:50%; width:50%;">
<div class="removable">
<div class="imageViewerViewport">
<canvas></canvas>
</div>
<div class="imageViewerViewportOverlay"></div>
<div class="imageViewerLoadingIndicator"></div>
<div class="imageViewerErrorLoadingIndicator"></div>
<div class="viewportOrientationMarkers"></div>
</div>
</div>
```
Each of this _div.viewportContainer_ will have some classes to help CSS specific styling accordingly to the element's position in the grid: **top**, **middle** and **bottom**. This classes are added by **viewer/components/gridLayout/** component in ohif-viewerbase package.
# The Type Safe Collections
With the introduction of the new _Study Metadata API_ in which study metadata is represented by class hierarchies (using prototype-based inheritance), the usage of standard _Minimongo_ collections as a central client-side storage for this data became no longer an option. Standard _Mongo_ and _Minimongo_ collections internally _flatten_ data (in other words, data gets serialized) before storage hence no functions or prototype chains are preserved. In that scenario, when an object is restored (fetched), what is returned is actually a flattened copy of the original object with no functions or prototype (it's no longer an instance of it's original class). As an attempt to overcome this limitation a new type of collection was intruduced: the *TypeSafeCollection*.
The `TypeSafeCollection` is a simple list-like collection which tries to implement an API _similar_ but not compatible with _Mongo_'s API. It supports basic features like search by attribute map and ID, retrieval by index, sorting of result sets, insertion, removal and reactive operations but, unlike _Mongo_'s API, it (still) lacks support to advanced functionality like complex search criterea or flexible sorting options.
## Implementation
The `TypeSafeCollection` is implemented on top of the _JavaScript_ `Array` object. Each element inserted in the collection is appended to the end of its internal array as a _key-value pair (KVP)_ object where the _key_ is a unique randomly generated ID string and the _value_ is the element itself. Once the object has been successfully stored, the generated ID (its ID) is returned to the client code and can later be used to access that specific element. At this point, an important difference to the _Minimongo_ API can be highlighted: a _TypeSafeCollection_ instance will never make any changes to the stored element (e.g., no "\_id" property will ever be assigned to the original object). Another relevant feature that is supported by this design decision is that _not only objects_ can be stored in this collections, but literally _anything_.
Inside the codebase, the _value_ attribute of each _KVP_ entry in the collection is refered to as _the **payload** of the entry_ since it's what really matters to the user. Hence, this term will also be used here to refer to the _value that has been stored in the collection_. That being said, we can approach another important feature of these collections: A single _payload_ cannot be stored more than once in a given collection. When an attempt of inserting a _payload_ which is already present in the collection is detected, the insert operation will fail and `null` will be returned. In that regard, the collection behaves like `Set` object not permitting a payload to be stored more than once. Strict equality is used when comparing payloads, thus cloned objects are not considered the same. This feature adds an additional garantee that a given study/series/instance will not be listed more than once (it was designed as a replacement for central study collections which were always checked for duplicates).
Please refer to the codebase for the full `TypeSafeCollection` API.
## Usage
In order to use the `TypeSafeCollection` class, the **ohif:viewerbase** package needs to be imported by the referring code and instantiated as follows:
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase'; // i.e., Viewerbase.TypeSafeCollection
OR
import 'meteor/ohif:viewerbase'; // i.e., OHIF.viewerbase.TypeSafeCollection
// The later is preferred when the client code already makes use of the "OHIF" namespace making the second
// "import" a garantee that the ".viewerbase" namespace has been properly loaded.
```
A few usage examples:
```javascript
const Users = new OHIF.viewerbase.TypeSafeCollection();
[[ ... ]]
// Insert a User object...
let userId = Users.insert({
data: {
firstName: 'John',
lastName: 'Doe',
age: 45
},
getFullName() {
return `${this.data.firstName} ${this.data.lastName}`;
},
getAge() {
return this.data.age;
}
});
[[ ... ]]
let theUserWeJustStored = Users.findById(userId); // ;-)
[[ ... ]]
// Retrieve a single user with "Doe" as `lastName`...
let myUser = Users.findBy({ 'data.lastName': 'Doe' });
// Or all users with "Doe" as `lastName`, sorted by `firstName` in ascending
// order and using the `age` attribute to break ties in descending order...
let myUsers = Users.findAllBy({ 'data.lastName': 'Doe' }, {
sort: [ [ 'data.firstName', 'asc' ], [ 'data.age', 'desc' ] ]
});
```
@@ -9,6 +9,7 @@ import { SeriesMetadata } from './SeriesMetadata';
import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from '../../utils/isImage';
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
export class StudyMetadata extends Metadata {
@@ -121,7 +122,7 @@ export class StudyMetadata extends Metadata {
if (displaySet) {
displaySet.sopClassModule = true;
displaySets.push(displaySet);
return;
return displaySets;
}
}
@@ -620,6 +621,16 @@ const makeDisplaySet = (series, instances) => {
imageSet.getImage(0).getRawValue('x00200013')
);
const isReconstructable = isDisplaySetReconstructable(series, instances);
imageSet.isReconstructable = isReconstructable.value;
if (isReconstructable.missingFrames) {
// TODO -> This is currently unused, but may be used for reconstructing
// Volumes with gaps later on.
imageSet.missingFrames = isReconstructable.missingFrames;
}
return imageSet;
};
@@ -504,12 +504,12 @@ export default class ProtocolEngine {
/**
* Sets the current layout
*
* @param rows
* @param columns
* @param {number} numRows
* @param {number} numColumns
*/
setLayout(rows, columns) {
if (rows < 1 && columns < 1) {
log.error(`Invalid layout ${rows} x ${columns}`);
setLayout(numRows, numColumns) {
if (numRows < 1 && numColumns < 1) {
log.error(`Invalid layout ${numRows} x ${numColumns}`);
return;
}
@@ -519,16 +519,13 @@ export default class ProtocolEngine {
}
let viewports = [];
const numViewports = rows * columns;
const numViewports = numRows * numColumns;
for (let i = 0; i < numViewports; i++) {
viewports.push({
height: `${100 / rows}%`,
width: `${100 / columns}%`,
});
viewports.push({});
}
this.options.setLayout({ viewports });
this.options.setLayout({ numRows, numColumns, viewports });
}
/**
@@ -634,14 +631,10 @@ export default class ProtocolEngine {
//console.log('renderedCallback for ' + element.id);
customSettings.forEach(customSetting => {
log.trace(
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${
customSetting.id
}`
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
);
log.trace(
`ProtocolEngine::currentViewportData.renderedCallback with value: ${
customSetting.value
}`
`ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
);
const setting = CustomViewportSettings[customSetting.id];
+27 -8
View File
@@ -26,9 +26,34 @@ export const setViewportActive = viewportIndex => ({
viewportIndex,
});
export const setLayout = layout => ({
/**
* @param {object} layout
* @param {number} layout.numRows
* @param {number} layout.numColumns
* @param {array} layout.viewports
*/
export const setLayout = ({ numRows, numColumns, viewports }) => ({
type: SET_VIEWPORT_LAYOUT,
layout,
numRows,
numColumns,
viewports,
});
/**
* @param {object} layout
* @param {number} layout.numRows
* @param {number} layout.numColumns
* @param {array} layout.viewports
*/
export const setViewportLayoutAndData = (
{ numRows, numColumns, viewports },
viewportSpecificData
) => ({
type: SET_VIEWPORT_LAYOUT_AND_DATA,
numRows,
numColumns,
viewports,
viewportSpecificData,
});
export const clearViewportSpecificData = viewportIndex => ({
@@ -87,12 +112,6 @@ export const setServers = servers => ({
servers,
});
export const setViewportLayoutAndData = (layout, viewportSpecificData) => ({
type: SET_VIEWPORT_LAYOUT_AND_DATA,
layout,
viewportSpecificData,
});
const actions = {
// VIEWPORT
setViewportActive,
+11 -10
View File
@@ -94,19 +94,20 @@ describe('actions', () => {
});
it('should create an action to set the viewport layout', () => {
const layout = {
viewports: [
{
height: '100%',
width: '100%',
},
],
};
const numRows = 1;
const numColumns = 2;
const viewports = [{ plugin: 'vtk' }, { plugin: 'vtk' }];
const expectedAction = {
type: types.SET_VIEWPORT_LAYOUT,
layout,
numRows,
numColumns,
viewports,
};
expect(actions.setLayout(layout)).toEqual(expectedAction);
expect(actions.setLayout({ numRows, numColumns, viewports })).toEqual(
expectedAction
);
});
});
});
+23 -9
View File
@@ -12,13 +12,13 @@ import cloneDeep from 'lodash.clonedeep';
import merge from 'lodash.merge';
const defaultState = {
numRows: 1,
numColumns: 1,
activeViewportIndex: 0,
layout: {
viewports: [
{
// plugin: 'cornerstone',
height: '100%',
width: '100%',
},
],
},
@@ -38,17 +38,31 @@ const viewports = (state = defaultState, action) => {
let viewportSpecificData;
let useActiveViewport = false;
switch (action.type) {
case SET_VIEWPORT_LAYOUT_AND_DATA:
return Object.assign({}, state, {
viewportSpecificData: action.viewportSpecificData,
layout: action.layout,
});
case SET_VIEWPORT_ACTIVE:
return Object.assign({}, state, {
activeViewportIndex: action.viewportIndex,
});
case SET_VIEWPORT_LAYOUT:
return Object.assign({}, state, { layout: action.layout });
case SET_VIEWPORT_LAYOUT: {
const { numRows, numColumns, viewports } = action;
const layout = {
viewports: [...viewports],
};
return Object.assign({}, state, { numRows, numColumns, layout });
}
case SET_VIEWPORT_LAYOUT_AND_DATA: {
const { numRows, numColumns, viewports, viewportSpecificData } = action;
const layout = {
viewports: [...viewports],
};
return Object.assign({}, state, {
numRows,
numColumns,
layout,
viewportSpecificData: cloneDeep(viewportSpecificData),
});
}
case SET_VIEWPORT: {
const layout = cloneDeep(state.layout);
const hasPlugin = action.data && action.data.plugin;
@@ -6,13 +6,10 @@ describe('viewports reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual({
activeViewportIndex: 0,
numRows: 1,
numColumns: 1,
layout: {
viewports: [
{
height: '100%',
width: '100%',
},
],
viewports: [{}],
},
viewportSpecificData: {},
});
@@ -34,23 +31,25 @@ describe('viewports reducer', () => {
it('should handle SET_VIEWPORT_LAYOUT', () => {
const setViewportLayoutAction = {
type: types.SET_VIEWPORT_LAYOUT,
layout: {
viewports: [
{
height: '100%',
width: '50%',
},
{
height: '100%',
width: '50%',
},
],
},
numRows: 1,
numColumns: 2,
viewports: [
{
plugin: 'cornerstone',
},
{
plugin: 'vtk',
},
],
};
const updatedState = reducer({}, setViewportLayoutAction);
expect(updatedState.layout).toEqual(setViewportLayoutAction.layout);
expect(updatedState.numRows).toEqual(setViewportLayoutAction.numRows);
expect(updatedState.numColumns).toEqual(setViewportLayoutAction.numColumns);
expect(updatedState.layout.viewports).toEqual(
setViewportLayoutAction.viewports
);
});
// If there were previous keys, this would have
@@ -0,0 +1,157 @@
/**
* Checks if a series is reconstructable to a 3D volume.
*
* @param {Object} series The `OHIFSeriesMetadata` object.
* @param {Object[]} instances The `OHIFInstanceMetadata` object
*/
export default function isDisplaySetReconstructable(series, instances) {
// Can't reconstruct if we only have one image.
const modality = series._data.modality; // TODO -> Is there a better way to get this?
const isMultiframe = instances[0].getRawValue('x00280008') > 1;
if (!constructableModalities.includes(modality)) {
return { value: false };
}
if (!isMultiframe && instances.length === 1) {
return { values: false };
}
if (isMultiframe) {
return processMultiframe(instances[0]);
} else {
return processSingleframe(instances);
}
}
function processMultiframe(instance) {
//TODO: deal with multriframe checks! return true for now.
return { value: true };
}
function processSingleframe(instances) {
const firstImage = instances[0];
const firstImageRows = firstImage.getTagValue('x00280010');
const firstImageColumns = firstImage.getTagValue('x00280011');
const firstImageSamplesPerPixel = firstImage.getTagValue('x00280002');
// Note: No need to unpack iop, can compare string form.
const firstImageOrientationPatient = firstImage.getTagValue('x00200037');
// Can't reconstruct if we:
// -- Have a different dimensions within a displaySet.
// -- Have a different number of components within a displaySet.
// -- Have different orientations within a displaySet.
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const rows = instance.getTagValue('x00280010');
const columns = instance.getTagValue('x00280011');
const samplesPerPixel = instance.getTagValue('x00280002');
const imageOrientationPatient = instance.getTagValue('x00200037');
if (
rows !== firstImageRows ||
columns !== firstImageColumns ||
samplesPerPixel !== firstImageSamplesPerPixel ||
imageOrientationPatient !== firstImageOrientationPatient
) {
return { value: false };
}
}
let missingFrames = 0;
// Check if frame spacing is approximately equal within a tolerance.
// If spacing is on a uniform grid but we are missing frames,
// Allow reconstruction, but pass back the number of missing frames.
if (instances.length > 2) {
const firstIpp = _getImagePositionPatient(firstImage);
const lastIpp = _getImagePositionPatient(instances[instances.length - 1]);
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstIpp, lastIpp) / (instances.length - 1);
let previousIpp = firstIpp;
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const ipp = _getImagePositionPatient(instance);
const spacingBetweenFrames = _getPerpendicularDistance(ipp, previousIpp);
const spacingIssue = _getSpacingIssue(
spacingBetweenFrames,
averageSpacingBetweenFrames
);
if (spacingIssue) {
const issue = spacingIssue.issue;
if (issue === reconstructionIssues.MISSING_FRAMES) {
missingFrames += spacingIssue.missingFrames;
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
return { value: false };
}
}
previousIpp = ipp;
}
}
return { value: true, missingFrames };
}
// TODO: Is 10% a reasonable tolerance for spacing?
const tolerance = 0.1;
/**
* Checks for spacing issues.
*
* @param {number} spacing The spacing between two frames.
* @param {number} averageSpacing The average spacing between all frames.
*
* @returns {Object} An object containing the issue and extra information if necessary.
*/
function _getSpacingIssue(spacing, averageSpacing) {
const equalWithinTolerance =
Math.abs(spacing - averageSpacing) < averageSpacing * tolerance;
if (equalWithinTolerance) {
return;
}
const multipleOfAverageSpacing = spacing / averageSpacing;
const numberOfSpacings = Math.round(multipleOfAverageSpacing);
const errorForEachSpacing =
Math.abs(spacing - numberOfSpacings * averageSpacing) / numberOfSpacings;
if (errorForEachSpacing < tolerance * averageSpacing) {
return {
issue: reconstructionIssues.MISSING_FRAMES,
missingFrames: numberOfSpacings - 1,
};
}
return { issue: reconstructionIssues.IRREGULAR_SPACING };
}
function _getImagePositionPatient(instance) {
return instance
.getTagValue('x00200032')
.split('\\')
.map(element => Number(element));
}
function _getPerpendicularDistance(a, b) {
return Math.sqrt(
Math.pow(a[0] - b[0], 2) +
Math.pow(a[1] - b[1], 2) +
Math.pow(a[2] - b[2], 2)
);
}
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
const reconstructionIssues = {
MISSING_FRAMES: 'missingframes',
IRREGULAR_SPACING: 'irregularspacing',
};
+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.51.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.5...@ohif/i18n@0.51.0) (2019-10-15)
### Features
* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b))
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.4...@ohif/i18n@0.50.5) (2019-10-04)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.50.5",
"version": "0.51.0",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
@@ -0,0 +1,14 @@
{
"Browser": "Browser",
"Build Number": "Build Number",
"Latest Master Commits": "Latest Master Commits",
"More details": "More details",
"Name": "Name",
"OHIF Viewer - About": "OHIF Viewer - About",
"OS": "OS",
"Report an issue": "Report an issue",
"Repository URL": "Repository URL",
"Value": "Value",
"Version Information": "Version Information",
"Visit the forum": "Visit the forum"
}
+2
View File
@@ -1,3 +1,4 @@
import AboutModal from "./AboutModal.json";
import Buttons from "./Buttons.json";
import CineDialog from "./CineDialog.json";
import Common from "./Common.json";
@@ -8,6 +9,7 @@ import UserPreferencesModal from "./UserPreferencesModal.json";
export default {
'en-US': {
AboutModal,
Buttons,
CineDialog,
Common,
@@ -0,0 +1,14 @@
{
"Browser": "Navegador",
"Build Number": "Número da compilação",
"Latest Master Commits": "Últimos Commits na Master",
"More details": "Mais detalhes",
"Name": "Nome",
"OHIF Viewer - About": "OHIF Viewer - Sobre",
"OS": "SO",
"Report an issue": "Informar um problema",
"Repository URL": "URL do Repositório",
"Value": "Valor",
"Version Information": "Informação da Versão",
"Visit the forum": "Visite o fórum"
}
+2
View File
@@ -1,3 +1,4 @@
import AboutModal from "./AboutModal.json";
import Buttons from "./Buttons.json";
import CineDialog from "./CineDialog.json";
import Common from "./Common.json";
@@ -6,6 +7,7 @@ import UserPreferencesModal from "./UserPreferencesModal.json";
export default {
'pt-BR': {
AboutModal,
Buttons,
CineDialog,
Common,
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.55.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.54.0...@ohif/ui@0.55.0) (2019-10-15)
### Features
* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b))
# [0.54.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.4...@ohif/ui@0.54.0) (2019-10-14)
### Features
* Notification Service ([#1011](https://github.com/OHIF/Viewers/issues/1011)) ([92c8996](https://github.com/OHIF/Viewers/commit/92c8996))
## [0.53.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.3...@ohif/ui@0.53.4) (2019-10-11)
**Note:** Version bump only for package @ohif/ui
## [0.53.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.2...@ohif/ui@0.53.3) (2019-10-10)
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.53.3",
"version": "0.55.0",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -39,6 +39,7 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/i18n": "^0.2.3",
"browser-detect": "^0.2.28",
"classnames": "2.2.6",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
@@ -1,162 +0,0 @@
import './LayoutManager.css';
import React, { Component } from 'react';
import LayoutPanelDropTarget from './LayoutPanelDropTarget.js';
import PropTypes from 'prop-types';
function defaultViewportPlugin(props) {
return <div>{JSON.stringify(props)}</div>;
}
function EmptyViewport() {
return (
<div className="EmptyViewport">
<p>Please drag a stack here to view images.</p>
</div>
);
}
export class LayoutManager extends Component {
static className = 'LayoutManager';
static defaultProps = {
viewportData: [],
layout: {
viewports: [
{
top: 0,
left: 0,
height: '100%',
width: '100%',
},
],
},
activeViewportIndex: 0,
supportsDragAndDrop: true,
availablePlugins: {
defaultViewportPlugin,
},
defaultPlugin: 'defaultViewportPlugin',
};
static propTypes = {
viewportData: PropTypes.array.isRequired,
supportsDragAndDrop: PropTypes.bool.isRequired,
activeViewportIndex: PropTypes.number.isRequired,
layout: PropTypes.object.isRequired,
availablePlugins: PropTypes.object.isRequired,
setViewportData: PropTypes.func,
studies: PropTypes.array,
children: PropTypes.node,
};
onDrop = ({ viewportIndex, item }) => {
if (this.props.setViewportData) {
this.props.setViewportData({ viewportIndex, item });
}
};
getPluginComponent = plugin => {
const pluginComponent = this.props.availablePlugins[
plugin || this.props.defaultPlugin
];
if (!pluginComponent) {
throw new Error(
`No Viewport Plugin available for plugin ${plugin}. Available plugins: ${JSON.stringify(
this.props.availablePlugins
)}`
);
}
return pluginComponent;
};
getChildComponent(plugin, data, viewportIndex, children) {
if (data.displaySet) {
const PluginComponent = this.getPluginComponent(plugin);
return (
<PluginComponent
viewportData={data}
viewportIndex={viewportIndex}
children={[children]}
/>
);
}
return <EmptyViewport />;
}
getContent(childComponent, supportsDragAndDrop, viewportIndex) {
if (supportsDragAndDrop) {
return (
<LayoutPanelDropTarget
onDrop={this.onDrop}
viewportIndex={viewportIndex}
>
{childComponent}
</LayoutPanelDropTarget>
);
}
return <div className="LayoutPanel">{childComponent}</div>;
}
render() {
if (!this.props.viewportData.length) {
return '';
}
const { supportsDragAndDrop, studies, viewportData } = this.props;
const viewports = this.props.layout.viewports;
const viewportElements = viewports.map((layout, viewportIndex) => {
const displaySet = viewportData[viewportIndex];
const data = {
displaySet,
studies,
};
// Use whichever plugin is currently in use in the panel
// unless nothing is specified. If nothing is specified
// and the display set has a plugin specified, use that.
//
// TODO: Change this logic to:
// - Plugins define how capable they are of displaying a SopClass
// - When updating a panel, ensure that the currently enabled plugin
// in the viewport is capable of rendering this display set. If not
// then use the most capable available plugin
let plugin = layout.plugin;
if (!layout.plugin && displaySet && displaySet.plugin) {
plugin = displaySet.plugin;
}
const childComponent = this.getChildComponent(
plugin,
data,
viewportIndex,
this.props.children
);
const content = this.getContent(
childComponent,
supportsDragAndDrop,
viewportIndex
);
let className = 'viewport-container';
if (this.props.activeViewportIndex === viewportIndex) {
className += ' active';
}
return (
<div key={viewportIndex} className={className} style={{ ...layout }}>
{content}
</div>
);
});
return <div className={LayoutManager.className}>{viewportElements}</div>;
}
}
export default LayoutManager;
@@ -1,12 +0,0 @@
.LayoutPanelDropTarget {
width: 100%;
height: 100%;
opacity: 1;
transition: 0.3s all ease;
}
.LayoutPanelDropTarget.hovered {
opacity: 0.5;
cursor: copy;
}
@@ -1,74 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { DropTarget } from 'react-dnd';
import './LayoutPanelDropTarget.css';
// Drag sources and drop targets only interact
// if they have the same string type.
const Types = {
THUMBNAIL: 'thumbnail',
};
const divTarget = {
drop(props, monitor, component) {
const item = monitor.getItem();
if (props.onDrop) {
props.onDrop({
viewportIndex: props.viewportIndex,
item,
});
}
return {
id: `LayoutPanelDropTarget-${props.viewportIndex}`,
viewportIndex: props.viewportIndex,
item,
};
},
};
// TODO: Find out why we can't move this into the Example app instead.
// It looks like the context isn't properly shared.
class LayoutPanelDropTarget extends Component {
static className = 'LayoutPanelDropTarget';
static defaultProps = {
isOver: false,
canDrop: false,
};
static propTypes = {
connectDropTarget: PropTypes.func.isRequired,
canDrop: PropTypes.bool.isRequired,
isOver: PropTypes.bool.isRequired,
viewportComponent: PropTypes.object,
};
render() {
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
let className = LayoutPanelDropTarget.className;
if (isActive) {
className += ' hovered';
} else if (canDrop) {
className += ' can-drop';
}
return connectDropTarget(
<div className={className}>{this.props.children}</div>
);
}
}
const collect = (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
canDrop: monitor.canDrop(),
isOver: monitor.isOver(),
});
export default DropTarget(Types.THUMBNAIL, divTarget, collect)(
LayoutPanelDropTarget
);
+1 -6
View File
@@ -1,8 +1,4 @@
import {
ExampleDropTarget,
StudyBrowser,
ThumbnailEntry,
} from './studyBrowser';
import { StudyBrowser, ThumbnailEntry } from './studyBrowser';
import { LayoutButton, LayoutChooser } from './layoutButton';
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
import { Overlay, OverlayTrigger } from './overlayTrigger';
@@ -26,7 +22,6 @@ import { Tooltip } from './tooltip';
export {
Checkbox,
CineDialog,
ExampleDropTarget,
LayoutButton,
LayoutChooser,
MeasurementTable,
@@ -0,0 +1,196 @@
.sb-container {
position: fixed;
width: var(--snackbar-size);
padding: 20px;
z-index: var(--snackbar-zIndex);
box-sizing: border-box;
height: auto;
}
.sb-topLeft {
top: 0;
bottom: auto;
left: 0;
right: auto;
}
.sb-topCenter {
left: 50%;
top: 0;
bottom: auto;
transform: translateX(-50%);
}
.sb-topRight {
top: 0;
bottom: auto;
left: auto;
right: 0;
}
.sb-bottomLeft {
top: auto;
bottom: 0px;
left: 0px;
right: auto;
}
.sb-bottomCenter {
left: 50%;
bottom: 0;
top: auto;
transform: translateX(-50%);
}
.sb-bottomRight {
top: auto;
bottom: 0px;
left: auto;
right: 0px;
margin: 10px 0 0;
}
.sb-topLeft .sb-item,
.sb-topCenter .sb-item,
.sb-topRight .sb-item {
margin: 10px 0 0;
}
.sb-bottomLeft .sb-item,
.sb-bottomCenter .sb-item,
.sb-bottomRight .sb-item {
margin: 0 0 10px;
}
.sb-closeBtn {
height: 20px;
opacity: 1;
overflow: hidden;
padding: 2px;
text-align: center;
text-shadow: none;
width: 20px;
cursor: pointer;
position: absolute;
right: 5px;
top: 5px;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.6);
border-radius: 100%;
}
.sb-closeBtn:hover {
background: #fff;
}
.sb-closeIcon {
display: block;
font-size: 0;
height: 100%;
line-height: 0;
overflow: hidden;
position: relative;
width: 100%;
}
.sb-closeIcon:after,
.sb-closeIcon:before {
content: ' ';
display: block;
height: 2px;
transition: all 0.3s ease;
width: 12px;
background-color: #222;
opacity: 1;
position: absolute;
}
.sb-closeIcon:before {
left: 4px;
top: 3px;
transform: rotate(45deg);
transform-origin: 0px 50%;
}
.sb-closeIcon:after {
right: 3px;
top: 5px;
transform: rotate(-45deg);
transform-origin: calc(100% - 3px) 50%;
}
.sb-title {
font-size: 16px;
font-weight: bold;
}
.sb-message {
font-size: 14px;
word-break: break-all;
}
.sb-item {
position: relative;
transition: height 300ms ease;
animation: fadein 1s;
padding: 20px;
color: white;
overflow: hidden;
border-radius: 4px;
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12),
0 3px 5px -1px rgba(0, 0, 0, 0.14);
}
.sb-item a {
color: white;
text-decoration: underline;
}
.sb-container .sb-hidden {
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
height: 0;
opacity: 0;
transition: all 300ms ease;
}
.sb-success {
background-color: var(--snackbar-success);
}
.sb-error {
background-color: var(--snackbar-error);
}
.sb-warning {
background-color: var(--snackbar-warning);
}
.sb-info {
background-color: var(--snackbar-info);
}
@keyframes fadein {
from {
opacity: 0;
top: 30px;
}
to {
opacity: 1;
top: 0;
}
}
/* Internet Explorer */
@-ms-keyframes fadein {
from {
opacity: 0;
top: 30px;
}
to {
opacity: 1;
top: 0;
}
}
@@ -0,0 +1,53 @@
import React from 'react';
import SnackbarItem from './SnackbarItem';
import './Snackbar.css';
import { useSnackbarContext } from '../../utils/SnackbarProvider';
const SnackbarContainer = () => {
const { snackbarItems, hide } = useSnackbarContext();
const renderItem = item => {
return <SnackbarItem key={item.itemId} options={item} onClose={hide} />;
};
if (!snackbarItems) {
return null;
}
const renderItems = () => {
const items = {
topLeft: [],
topCenter: [],
topRight: [],
bottomLeft: [],
bottomCenter: [],
bottomRight: [],
};
snackbarItems.map(item => {
items[item.position].push(item);
});
return (
<div>
{Object.keys(items).map(pos => {
if (!items[pos].length) {
return null;
}
return (
<div key={pos} className={`sb-container sb-${pos}`}>
{items[pos].map(item => (
<div key={item.id}>{renderItem(item)}</div>
))}
</div>
);
})}
</div>
);
};
return <>{renderItems()}</>;
};
export default SnackbarContainer;
@@ -0,0 +1,31 @@
import React, { useState, useEffect } from 'react';
const SnackbarItem = ({ options, onClose }) => {
const handleClose = () => {
onClose(options.id);
};
useEffect(() => {
if (options.autoClose) {
setTimeout(() => {
handleClose();
}, options.duration);
}
}, []);
return (
<div
className={`${options.visible ? '' : 'sb-hidden'} sb-${
options.type
} sb-item`}
>
<span className="sb-closeBtn" onClick={handleClose}>
<span className="sb-closeIcon">x</span>
</span>
{options.title && <div className="sb-title">{options.title}</div>}
{options.message && <div className="sb-message">{options.message}</div>}
</div>
);
};
export default SnackbarItem;
@@ -0,0 +1,6 @@
export default {
INFO: 'info',
WARNING: 'warning',
SUCCESS: 'success',
ERROR: 'error',
};
@@ -8,7 +8,8 @@ import {
onThumbnailClick,
onThumbnailDoubleClick,
} from './exampleStudies.js';
import { ExampleDropTarget, StudyBrowser } from './../index.js';
import ExampleDropTarget from './ExampleDropTarget.js';
import { StudyBrowser } from './../index.js';
class StudyBrowserContainer extends Component {
render() {
@@ -1,3 +1,2 @@
export { ExampleDropTarget } from './ExampleDropTarget.js';
export { StudyBrowser } from './StudyBrowser.js';
export { ThumbnailEntry } from './ThumbnailEntry.js';
@@ -1,14 +1,16 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-bootstrap-modal';
import detect from 'browser-detect';
import './AboutModal.styl';
import { withTranslation } from '../../utils/LanguageProvider';
import 'react-bootstrap-modal/lib/css/rbm-patch.css';
// TODO: Is this the only component importing these?
import './../../design/styles/common/modal.styl';
export class AboutModal extends Component {
class AboutModal extends Component {
constructor(props) {
super(props);
}
@@ -18,16 +20,41 @@ export class AboutModal extends Component {
onCancel: PropTypes.func,
};
static itemsPreset = [
{
name: 'Repository URL',
value: 'https://github.com/OHIF/Viewers/',
},
{
name: 'Latest Master Commits',
value: 'https://github.com/OHIF/Viewers/commits/master',
},
];
itemsPreset() {
const { t } = this.props;
const browser = detect();
const capitalize = s =>
s.substr(0, 1).toUpperCase() + s.substr(1).toLowerCase();
return [
{
name: t('Repository URL'),
value: 'https://github.com/OHIF/Viewers/',
link: 'https://github.com/OHIF/Viewers/',
},
{
name: t('Latest Master Commits'),
value: 'https://github.com/OHIF/Viewers/commits/master',
link: 'https://github.com/OHIF/Viewers/commits/master',
},
{
name: 'Version Number',
value: process.env.VERSION_NUMBER,
},
{
name: t('Build Number'),
value: process.env.BUILD_NUM,
},
{
name: t('Browser'),
value: `${capitalize(browser.name)} ${browser.version}`,
},
{
name: t('OS'),
value: browser.os,
},
];
}
static defaultProps = {
isOpen: false,
@@ -38,15 +65,20 @@ export class AboutModal extends Component {
<tr key={item.name}>
<td>{item.name}</td>
<td>
<a target="_blank" href={item.value}>
{item.value}
</a>
{item.link ? (
<a target="_blank" href={item.link}>
{item.value}
</a>
) : (
item.value
)}
</td>
</tr>
);
}
render() {
const { t } = this.props;
return (
<Modal
show={this.props.isOpen}
@@ -58,7 +90,7 @@ export class AboutModal extends Component {
keyboard={false}
>
<Modal.Header closeButton>
<Modal.Title>About</Modal.Title>
<Modal.Title>{t('OHIF Viewer - About')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="btn-group">
@@ -67,34 +99,36 @@ export class AboutModal extends Component {
target="_blank"
href="https://groups.google.com/forum/#!forum/cornerstone-platform"
>
Visit the forum
{t('Visit the forum')}
</a>
{` `}
<a
className="btn btn-default"
target="_blank"
href="https://github.com/OHIF/Viewers/issues"
href="https://github.com/OHIF/Viewers/issues/new/choose"
>
Report an issue
{t('Report an issue')}
</a>
{` `}
<a
className="btn btn-default"
target="_blank"
href="http://ohif.org"
>
More details
{t('More details')}
</a>
</div>
<div>
<h3>Version Information</h3>
<h3>{t('Version Information')}</h3>
<table className="table table-responsive">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>{t('Name')}</th>
<th>{t('Value')}</th>
</tr>
</thead>
<tbody>
{AboutModal.itemsPreset.map(item => this.renderTableRow(item))}
{this.itemsPreset().map(item => this.renderTableRow(item))}
</tbody>
</table>
</div>
@@ -103,3 +137,7 @@ export class AboutModal extends Component {
);
}
}
const connectedComponent = withTranslation('AboutModal')(AboutModal);
export { connectedComponent as AboutModal };
export default connectedComponent;
@@ -12,6 +12,7 @@
z-index: 1050;
-webkit-overflow-scrolling: touch;
outline: 0;
background: rgba(0,0,0,0.5);
.modal-dialog
+7 -6
View File
@@ -1,7 +1,6 @@
import {
Checkbox,
CineDialog,
ExampleDropTarget,
LayoutButton,
LayoutChooser,
MeasurementTable,
@@ -28,13 +27,15 @@ import { ICONS, Icon } from './elements';
// Alias this for now as not all dependents are using strict versioning
import { DropdownMenu as Dropdown, Range, Select } from './elements/form';
import ExpandableToolMenu from './viewer/ExpandableToolMenu.js';
import LayoutManager from './LayoutChooser/LayoutManager.js';
import LayoutPanelDropTarget from './LayoutChooser/LayoutPanelDropTarget.js';
import PlayClipButton from './viewer/PlayClipButton.js';
import { ScrollableArea } from './ScrollableArea/ScrollableArea.js';
import Toolbar from './viewer/Toolbar.js';
import ToolbarButton from './viewer/ToolbarButton.js';
import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
import SnackbarProvider, {
useSnackbarContext,
withSnackbar,
} from './utils/SnackbarProvider';
export {
ICONS,
@@ -43,12 +44,9 @@ export {
CineDialog,
Dropdown,
ExpandableToolMenu,
ExampleDropTarget,
Icon,
LayoutButton,
LayoutChooser,
LayoutManager,
LayoutPanelDropTarget,
MeasurementTable,
MeasurementTableItem,
Overlay,
@@ -74,4 +72,7 @@ export {
UserPreferences,
UserPreferencesModal,
ViewerbaseDragDropContext,
SnackbarProvider,
useSnackbarContext,
withSnackbar,
};
+102
View File
@@ -0,0 +1,102 @@
import React, { useState, createContext, useContext } from 'react';
import SnackbarContainer from '../components/snackbar/SnackbarContainer';
import SnackbarTypes from '../components/snackbar/SnackbarTypes';
const SnackbarContext = createContext(null);
export const useSnackbarContext = () => useContext(SnackbarContext);
const SnackbarProvider = ({ children }) => {
const DEFAULT_OPTIONS = {
title: '',
message: '',
duration: 5000,
autoClose: true,
position: 'bottomRight',
type: SnackbarTypes.INFO,
};
const [count, setCount] = useState(1);
const [snackbarItems, setSnackbarItems] = useState([]);
const show = options => {
if (!options || (!options.title && !options.message)) {
console.warn(
'Snackbar cannot be rendered without required parameters: title | message'
);
return null;
}
const newItem = {
...DEFAULT_OPTIONS,
...options,
id: count,
visible: true,
};
setSnackbarItems(state => [...state, newItem]);
setCount(count + 1);
};
const hide = id => {
const hideItem = items => {
const newItems = items.map(item => {
if (item.id === id) {
item.visible = false;
}
return item;
});
return newItems;
};
setSnackbarItems(state => hideItem(state));
setTimeout(() => {
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
}, 1000);
};
const hideAll = () => {
// reset count
setCount(1);
// remove all items from array
setSnackbarItems(() => []);
};
/**
* expose snackbar methods to window for debug purposes
* TODO: Check if it's really necessary
*/
window.snackbar = {
show,
hide,
hideAll,
};
return (
<SnackbarContext.Provider value={{ show, hide, hideAll, snackbarItems }}>
{!!snackbarItems && <SnackbarContainer />}
{children}
</SnackbarContext.Provider>
);
};
/**
*
* High Order Component to use the snackbar methods through a Class Component
*
*/
export const withSnackbar = Component => {
return function WrappedComponent(props) {
const snackbarContext = {
...useSnackbarContext(),
};
return <Component {...props} snackbarContext={snackbarContext} />;
};
};
export default SnackbarProvider;
+8 -21
View File
@@ -1,8 +1,9 @@
// ~~ WebPack
const webpack = require('webpack');
const path = require('path');
const merge = require('webpack-merge');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
//
// ~~ Plugins
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const fontsToJavaScriptRule = require('./rules/fontsToJavaScript.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -12,31 +13,14 @@ const DIST_DIR = path.join(__dirname, '../dist');
const PUBLIC_DIR = path.join(__dirname, '../public');
// ~~ Env Vars
const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'script-tag.html';
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
const mergedConfig = merge(commonConfig, {
entry: {
app: `${SRC_DIR}/index-umd.js`,
},
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: DIST_DIR,
library: 'OHIFViewer',
@@ -50,14 +34,17 @@ module.exports = (env, argv) => {
// Clean output.path
new CleanWebpackPlugin(),
// Generate "index.html" w/ correct includes/imports
// NOTE: We use this for E2E Tests
new HtmlWebpackPlugin({
inject: false,
template: `${PUBLIC_DIR}/html-templates/${HTML_TEMPLATE}`,
filename: 'index.html',
}),
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
maxChunks: 1,
}),
],
});
return mergedConfig;
};
-27
View File
@@ -25,36 +25,18 @@ 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 SKIP_MINIMIZE = process.env.SKIP_MINIMIZE;
module.exports = (env, argv) => {
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
const isProdBuild = process.env.NODE_ENV === 'production';
const hasProxy = PROXY_TARGET && PROXY_DOMAIN;
const skipMinimize = SKIP_MINIMIZE === 'true';
const mergedConfig = merge(baseConfig, {
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
output: {
path: DIST_DIR,
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix
},
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: isProdBuild && !skipMinimize,
sideEffects: true,
},
module: {
rules: [...extractStyleChunksRule(isProdBuild)],
},
@@ -131,15 +113,6 @@ module.exports = (env, argv) => {
if (!isProdBuild) {
mergedConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
} else {
mergedConfig.optimization.minimizer = [
new TerserJSPlugin({
sourceMap: true,
parallel: true,
}),
// No bueno
// new OptimizeCSSAssetsPlugin({}),
];
}
return mergedConfig;
+106
View File
@@ -3,6 +3,112 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @ohif/viewer
## [1.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.0...@ohif/viewer@1.10.1) (2019-10-16)
**Note:** Version bump only for package @ohif/viewer
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.9.1...@ohif/viewer@1.10.0) (2019-10-15)
### Features
* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b))
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.9.0...@ohif/viewer@1.9.1) (2019-10-15)
### Bug Fixes
* 🐛 Remove debugger statement left in from last PR ([#1052](https://github.com/OHIF/Viewers/issues/1052)) ([d091cd6](https://github.com/OHIF/Viewers/commit/d091cd6))
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.8.0...@ohif/viewer@1.9.0) (2019-10-15)
### Features
* 🎸 Only allow reconstruction of datasets that make sense ([#1010](https://github.com/OHIF/Viewers/issues/1010)) ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes [#561](https://github.com/OHIF/Viewers/issues/561)
# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.7.0...@ohif/viewer@1.8.0) (2019-10-14)
### Features
* Notification Service ([#1011](https://github.com/OHIF/Viewers/issues/1011)) ([92c8996](https://github.com/OHIF/Viewers/commit/92c8996))
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.3...@ohif/viewer@1.7.0) (2019-10-14)
### Features
* Implement a 'Exit 2D MPR' button in the toolbar ([c99e0d8](https://github.com/OHIF/Viewers/commit/c99e0d8))
## [1.6.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.2...@ohif/viewer@1.6.3) (2019-10-14)
**Note:** Version bump only for package @ohif/viewer
## [1.6.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.1...@ohif/viewer@1.6.2) (2019-10-11)
**Note:** Version bump only for package @ohif/viewer
## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.0...@ohif/viewer@1.6.1) (2019-10-11)
### Bug Fixes
* Switch token storage back to localStorage because in-memory was annoying for end users ([#1030](https://github.com/OHIF/Viewers/issues/1030)) ([412fe4e](https://github.com/OHIF/Viewers/commit/412fe4e))
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.4...@ohif/viewer@1.6.0) (2019-10-11)
@@ -2,12 +2,12 @@ describe('OHIF Cornerstone Toolbar', () => {
before(() => {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(1);
});
beforeEach(() => {
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
//Following best practices, reset should be done before each test
cy.resetViewport();
});
@@ -42,9 +42,6 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('@moreBtn')
.should('be.visible')
.contains('More');
cy.get('@twodmprBtn')
.should('be.visible')
.contains('2D MPR');
cy.get('@layoutBtn')
.should('be.visible')
.contains('Layout');
@@ -148,7 +145,7 @@ describe('OHIF Cornerstone Toolbar', () => {
//Add annotation on the viewport
const firstClick = [150, 100];
const secondClick = [130, 170];
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
cy.addLine('@viewport', firstClick, secondClick);
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn')
@@ -176,7 +173,7 @@ describe('OHIF Cornerstone Toolbar', () => {
const initPos = [180, 390];
const midPos = [300, 410];
const finalPos = [180, 450];
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
cy.addAngle('@viewport', initPos, midPos, finalPos);
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn')
@@ -361,33 +358,20 @@ describe('OHIF Cornerstone Toolbar', () => {
.should('be.eq', 9);
});
//Commented this to avoid throwing an wrong exception at the end of "Layout button" test
//this commented section should be uncommented once issue #999 is fixed. (https://github.com/OHIF/Viewers/issues/999)
//verify if layout has changed to 1 viewport
// cy.get('@layoutBtn').click();
// cy.get('tbody > :nth-child(1) > :nth-child(1)').click();
// cy.get('.viewport-container').then(($viewport) =>{
// cy.wrap($viewport)
// .its('length')
// .should('be.eq', 1);
// })
cy.reload();
cy.waitDicomImage();
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(1) > :nth-child(1)').click();
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 1);
});
});
it('checks if Clear tool will delete all measurements added in the viewport', () => {
//Add measurements in the viewport
cy.get('@lengthBtn').click();
const firstClick = [150, 100];
const secondClick = [130, 170];
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
cy.get('@angleBtn').click();
const initPos = [180, 390];
const midPos = [300, 410];
const finalPos = [180, 450];
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
cy.addLengthMeasurement();
cy.addAngleMeasurement();
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
@@ -419,23 +403,18 @@ describe('OHIF Cornerstone Toolbar', () => {
it('checks if Eraser tool will remove the measurements added in the viewport', () => {
//Add measurements in the viewport
cy.get('@lengthBtn').click();
const firstClick = [150, 100];
const secondClick = [130, 170];
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
cy.get('@angleBtn').click();
const initPos = [180, 390];
const midPos = [300, 410];
const finalPos = [180, 450];
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
cy.addLengthMeasurement();
cy.addAngleMeasurement();
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.eq', 2);
cy.get('@measurementsBtn').click();
cy.get('@measurementsBtn')
.click()
.wait(2000);
//cy.isNotInViewport('@measurementsPanel'); //TO DO: check this intermittent behaviour
//Click More button
cy.get('@moreBtn').click();
@@ -443,14 +422,14 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-inner > :nth-child(12)').click();
//Erase measurement #1 and Verify if it was removed from the measurements panel
const [x1, y1] = firstClick;
const [x1, y1] = [150, 100];
cy.get('@viewport').click(x1, y1, { force: true });
cy.get('.measurementItem')
.its('length')
.should('be.eq', 1);
//Erase measurement #2 and Verify if it was removed from the measurements panel
const [x2, y2] = initPos;
const [x2, y2] = [180, 390];
cy.get('@viewport').click(x2, y2, { force: true });
cy.get('.measurementItem').should('not.exist');
});
@@ -4,6 +4,9 @@ describe('OHIFStandaloneViewer', () => {
});
it('loads route with at least 2 rows', () => {
cy.screenshot();
cy.percySnapshot();
cy.get('#studyListData tr')
.its('length')
.should('be.gt', 2);
@@ -2,15 +2,18 @@ describe('OHIF Study Viewer Page', () => {
before(() => {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(1);
});
beforeEach(() => {
cy.initCommonElementsAliases();
//Following best practices, reset should be done before each test
cy.resetViewport();
});
it('checks if series thumbnails are being displayed', () => {
cy.screenshot();
cy.percySnapshot();
cy.get('.ThumbnailEntryContainer')
.its('length')
.should('be.gt', 1);
@@ -62,8 +65,11 @@ describe('OHIF Study Viewer Page', () => {
// Confirm Selection
cy.get('.checkIconWrapper').click();
//Verify if 'Bone' label was added
// Verify if 'Bone' label was added
cy.get('.measurementLocation').should('contain.text', 'Bone');
// Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.be.enabled');
});
//TO-DO: Test case will fail due to issue #1013: https://github.com/OHIF/Viewers/issues/1013
@@ -90,4 +96,114 @@ describe('OHIF Study Viewer Page', () => {
// cy.get('.measurementLocation')
// .should('contain.text', descriptionText);
// });
it('checks if measurement item can be deleted through the context menu on the viewport', () => {
cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport
//Right click on measurement annotation
const [x1, y1] = [150, 100];
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
});
//Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
//Click "Delete measurement"
cy.get('.form-action')
.contains('Delete measurement')
.click();
//Open measurements menu
cy.get('@measurementsBtn').click();
//Verify measurements was removed from panel
cy.get('.measurementItem')
.should('not.exist')
.log('Annotation removed with success');
//Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.be.enabled');
});
it('adds relabel and description to measurement item through the context menu on the viewport', () => {
cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport
// Relabel
// Right click on measurement annotation
const [x1, y1] = [150, 100];
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
});
// Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
// Click "Relabel"
cy.get('.form-action')
.contains('Relabel')
.click();
// Search for "Brain"
cy.get('.searchInput').type('Brain');
// Select "Brain" Result
cy.get('.treeInputs > .wrapperLabel')
.contains('Brain')
.click();
// Confirm Selection
cy.get('.checkIconWrapper').click();
// Description
// Right click on measurement annotation
cy.get('@viewport')
.trigger('mousedown', x1, y1, {
which: 3,
})
.trigger('mouseup', x1, y1, {
which: 3,
});
// Contextmenu is visible
cy.get('.ToolContextMenu').should('be.visible');
// Click "Description"
cy.get('.form-action')
.contains('Add Description')
.click();
// Enter description text
const descriptionText = 'Adding text for description test';
cy.get('#description').type(descriptionText);
// Confirm
cy.get('.btn-confirm').click();
//Open measurements menu
cy.get('@measurementsBtn').click();
// Verify if label was added
cy.get('.measurementLocation')
.should('contain.text', 'Brain')
.log('Relabel added with success');
//Verify if descriptionText was added
cy.get('.measurementLocation')
.should('contain.text', descriptionText)
.log('Description added with success');
// Close panel
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.be.enabled');
});
});
@@ -0,0 +1,24 @@
describe('OHIF HTML Extension', () => {
before(() => {
cy.openStudy('Dummy');
cy.expectMinimumThumbnails(1);
});
it('checks if series thumbnails are being displayed', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('SR')
.its('length')
.should('to.be.at.least', 1);
});
it('drags and drop a SR thumbnail into viewport', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('SR')
.drag('.viewport-drop-target');
cy.get(':nth-child(2) > h1').should(
'contain.text',
'Imaging Measurement Report'
);
});
});
@@ -0,0 +1,23 @@
describe('OHIF PDF Extension', () => {
before(() => {
cy.openStudy('Dummy');
cy.expectMinimumThumbnails(1);
});
it('checks if series thumbnails are being displayed', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('DOC')
.its('length')
.should('to.be.at.least', 1);
});
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);
});
});
@@ -0,0 +1,51 @@
describe('OHIF VTK Extension', () => {
before(() => {
cy.openStudy('Juno');
cy.waitDicomImage();
cy.expectMinimumThumbnails(1);
});
beforeEach(() => {
// TODO: We shouldn't have to drag the thumbnail
// This is a known bug; 2D MPR button does not show until viewport
// has data from a drag-n-drop
// Drag and drop first thumbnail into first viewport
cy.get('[data-cy="thumbnail-list"]:nth-child(3)').drag(
'.cornerstone-canvas'
);
cy.get('.PluginSwitch > .toolbar-button')
.as('twodmprBtn')
.should('be.visible')
.then(btn => {
if (!btn.text().includes('Exit')) {
btn.click();
}
});
cy.initVTKToolsAliases();
cy.wait(1000);
});
it('checks if VTK buttons are displayed on the toolbar', () => {
cy.get('@crosshairsBtn')
.should('be.visible')
.contains('Crosshairs');
cy.get('@wwwcBtn')
.should('be.visible')
.contains('WWWC');
cy.get('@rotateBtn')
.should('be.visible')
.contains('Rotate');
cy.get('@slabSlider')
.should('be.visible')
.contains('Slab Thickness');
cy.get('@modeDropdown')
.should('be.visible')
.contains('MIP');
cy.get('@modeCheckbox').should('be.visible');
cy.get('@layoutBtn')
.should('be.visible')
.contains('Layout');
});
});
+3 -1
View File
@@ -7,10 +7,10 @@
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
let percyHealthCheck = require('@percy/cypress/task');
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
@@ -24,4 +24,6 @@ module.exports = (on, config) => {
return args;
}
});
on('task', percyHealthCheck);
};
+18 -1
View File
@@ -10,7 +10,6 @@ export function initCornerstoneToolsAliases() {
cy.get('.ToolbarRow > :nth-child(9)').as('resetBtn');
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
cy.get('.expandableToolMenu').as('moreBtn');
cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
}
@@ -32,3 +31,21 @@ export function initCommonElementsAliases() {
'viewportInfoBottomRight'
);
}
//Creating aliases for Routes
export function initRouteAliases() {
cy.server();
cy.route('GET', '/dcm4chee-arc/**/series').as('getStudySeries');
}
//Creating aliases for VTK tools buttons
export function initVTKToolsAliases() {
cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
cy.get('.ToolbarRow > :nth-child(2)').as('crosshairsBtn');
cy.get('.ToolbarRow > :nth-child(3)').as('wwwcBtn');
cy.get('.ToolbarRow > :nth-child(4)').as('rotateBtn');
cy.get('.slab-thickness').as('slabSlider');
cy.get('.select-ohif').as('modeDropdown');
cy.get('.ohif-check-label').as('modeCheckbox');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
}
+94 -19
View File
@@ -1,7 +1,10 @@
import '@percy/cypress';
import { DragSimulator } from '../helpers/DragSimulator.js';
import {
initCornerstoneToolsAliases,
initCommonElementsAliases,
initRouteAliases,
initVTKToolsAliases,
} from './aliases.js';
// ***********************************************
@@ -36,14 +39,19 @@ import {
* @param {string} PatientName - Patient name that we would like to search for
*/
Cypress.Commands.add('openStudy', patientName => {
cy.initRouteAliases();
cy.visit('/');
cy.get('#patientName').type(patientName);
cy.get('.studylistStudy > .patientName')
.as('patientResult')
.then({ timeout: 5000 }, $patientResult => {
cy.contains(patientName).click();
});
cy.get('.studylistStudy > .patientName', { timeout: 5000 })
.contains(patientName)
.click();
// cy.get('.studylistStudy > .patientName')
// .as('patientResult')
// .should({ timeout: 3000 }, () => {
// cy.contains(patientName).click();
// });
});
/**
@@ -103,6 +111,12 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
cy.get('[data-cy=thumbnail-list]', { timeout: 10000 }).should($itemList => {
expect($itemList.length >= seriesToWait).to.be.true;
});
});
//Command to wait DICOM image to load into the viewport
Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
cy.window()
@@ -182,21 +196,82 @@ Cypress.Commands.add('initCommonElementsAliases', () => {
initCommonElementsAliases();
});
//Add measurements in the viewport
Cypress.Commands.add('addLengthMeasurement', () => {
cy.initCornerstoneToolsAliases();
cy.get('@lengthBtn').click();
const firstClick = [150, 100];
const secondClick = [130, 170];
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
//Initialize aliases for Routes
Cypress.Commands.add('initRouteAliases', () => {
initRouteAliases();
});
//Initialize aliases for VTK tools
Cypress.Commands.add('initVTKToolsAliases', () => {
initVTKToolsAliases();
});
//Add measurements in the viewport
Cypress.Commands.add('addAngleMeasurement', () => {
cy.initCornerstoneToolsAliases();
cy.get('@angleBtn').click();
const initPos = [180, 390];
const midPos = [300, 410];
const finalPos = [180, 450];
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
Cypress.Commands.add(
'addLengthMeasurement',
(firstClick = [150, 100], secondClick = [130, 170]) => {
cy.initCornerstoneToolsAliases();
cy.get('@lengthBtn').click();
cy.addLine('@viewport', firstClick, secondClick);
}
);
//Add measurements in the viewport
Cypress.Commands.add(
'addAngleMeasurement',
(initPos = [180, 390], midPos = [300, 410], finalPos = [180, 450]) => {
cy.initCornerstoneToolsAliases();
cy.get('@angleBtn').click();
cy.addAngle('@viewport', initPos, midPos, finalPos);
}
);
/**
* Tests if element is NOT in viewport, or does not exist in DOM
*
* @param {string} element - element selector string or alias
* @returns
*/
Cypress.Commands.add('isNotInViewport', element => {
cy.get(element, { timeout: 3000 }).should($el => {
const bottom = Cypress.$(cy.state('window')).height() - 50;
const right = Cypress.$(cy.state('window')).width() - 50;
// If it's not visible, it's not in the viewport
if ($el) {
const rect = $el[0].getBoundingClientRect();
// TODO: support leftOf, above
const isBeneath = rect.top >= bottom && rect.bottom >= bottom;
const isRightOf = rect.left >= right && rect.right >= right;
const isNotInViewport = isBeneath && isRightOf;
expect(isNotInViewport).to.be.true;
}
});
});
/**
* Tests if element is in viewport, or it does exist in DOM
*
* @param {string} element - element selector string or alias
* @returns
*/
Cypress.Commands.add('isInViewport', element => {
cy.get(element, { timeout: 3000 }).should($el => {
const bottom = Cypress.$(cy.state('window')).height();
const right = Cypress.$(cy.state('window')).width();
// If it's not visible, it's not in the viewport
if ($el) {
const rect = $el[0].getBoundingClientRect();
// TODO: support leftOf, above
const isBeneath = rect.top < bottom && rect.bottom < bottom;
const isRightOf = rect.left < right && rect.right < right;
const isInViewport = isBeneath && isRightOf;
expect(isInViewport).to.be.true;
}
});
});
+11 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "1.6.0",
"version": "1.10.3",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -17,20 +17,19 @@
},
"proxy": "http://localhost:8042",
"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",
"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:e2e": "cross-env NODE_ENV=development 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 SKIP_MINIMIZE=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 SKIP_MINIMIZE=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --progress --config .webpack/webpack.pwa.js",
"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",
"start": "yarn run dev",
"test:e2e": "cypress open",
"test:e2e:ci": "cypress run",
"test:e2e:ci": "percy exec -- cypress run",
"test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci",
"test:e2e:serve": "serve -l 3000 -s dist",
"test:e2e:serve": "serve -n -l 3000 -s dist",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage",
"prepublishOnly": "yarn run build:package",
@@ -46,14 +45,14 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^1.3.0",
"@ohif/core": "^1.4.0",
"@ohif/extension-cornerstone": "^2.0.0",
"@ohif/extension-dicom-html": "^1.0.0",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.0",
"@ohif/extension-vtk": "^0.52.6",
"@ohif/i18n": "^0.50.5",
"@ohif/ui": "^0.53.3",
"@ohif/extension-vtk": "^0.52.13",
"@ohif/i18n": "^0.51.0",
"@ohif/ui": "^0.55.0",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
@@ -83,7 +82,7 @@
"redux-oidc": "3.1.x",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"vtk.js": "^11.0.1"
"vtk.js": "^11.6.0"
},
"devDependencies": {
"cypress": "^3.3.1",
+7 -2
View File
@@ -27,6 +27,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import { getActiveContexts } from './store/layout/selectors.js';
import i18n from '@ohif/i18n';
import store from './store';
import { SnackbarProvider } from '@ohif/ui';
// Contexts
import WhiteLabellingContext from './context/WhiteLabellingContext';
@@ -99,7 +100,9 @@ class App extends Component {
<WhiteLabellingContext.Provider
value={this.props.whiteLabelling}
>
<OHIFStandaloneViewer userManager={userManager} />
<SnackbarProvider>
<OHIFStandaloneViewer userManager={userManager} />
</SnackbarProvider>
</WhiteLabellingContext.Provider>
</Router>
</UserManagerContext.Provider>
@@ -116,7 +119,9 @@ class App extends Component {
<I18nextProvider i18n={i18n}>
<Router basename={this.props.routerBasename}>
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
<OHIFStandaloneViewer />
<SnackbarProvider>
<OHIFStandaloneViewer />
</SnackbarProvider>
</WhiteLabellingContext.Provider>
</Router>
</I18nextProvider>
@@ -96,3 +96,27 @@
color: #91b9cd;
margin-right: 1rem;
}
.header-versionInfo {
display: inline-block;
color: black;
background: #9ccef9;
padding: 0px 8px;
border-radius: 16px;
font-size: 12px;
margin-left: 10px;
font-weight: bold;
}
.header-versionInfoHome {
display: block;
color: black;
position: absolute;
bottom: 16px;
right: 0;
background: #9ccef9;
padding: 0px 8px;
border-radius: 16px;
font-size: 12px;
font-weight: bold;
}
@@ -101,6 +101,14 @@ class Header extends Component {
</Link>
)}
<span
className={`${
this.props.home ? 'header-versionInfoHome' : 'header-versionInfo'
}`}
>
v{process.env.VERSION_NUMBER}
</span>
{this.props.children}
{showStudyList && !this.props.home && (
@@ -1,7 +1,7 @@
import { LayoutManager } from '@ohif/ui';
import ViewportGrid from './ViewportGrid.js';
import { MODULE_TYPES } from '@ohif/core';
import { connect } from 'react-redux';
import { extensionManager } from './../App.js';
import { extensionManager } from './../../App.js';
const mapStateToProps = state => {
const availableViewportModules = {};
@@ -18,9 +18,13 @@ const mapStateToProps = state => {
defaultPlugin = viewportModules[0].extensionId;
}
const { numRows, numColumns, layout, activeViewportIndex } = state.viewports;
return {
layout: state.viewports.layout,
activeViewportIndex: state.viewports.activeViewportIndex,
numRows,
numColumns,
layout,
activeViewportIndex,
// TODO: rename `availableViewportModules`
availablePlugins: availableViewportModules,
// TODO: rename `defaultViewportModule`
@@ -28,9 +32,9 @@ const mapStateToProps = state => {
};
};
const ConnectedLayoutManager = connect(
const ConnectedViewportGrid = connect(
mapStateToProps,
null
)(LayoutManager);
)(ViewportGrid);
export default ConnectedLayoutManager;
export default ConnectedViewportGrid;
@@ -0,0 +1,10 @@
/**
*
*
* @export
* @param {*} props
* @returns
*/
export default function DefaultViewport(props) {
return <div>{JSON.stringify(props)}</div>;
}
@@ -0,0 +1,7 @@
.empty-viewport {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: var(--text-secondary-color);
}
@@ -0,0 +1,12 @@
/**
*
*
* @returns
*/
function EmptyViewport() {
return (
<div className="empty-viewport">
<p>Please drag a stack here to view images.</p>
</div>
);
}
@@ -7,16 +7,3 @@
.viewport-container.active {
border: var(--viewport-border-thickness) solid var(--active-color);
}
.EmptyViewport {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: var(--text-secondary-color);
}
.LayoutPanel {
width: 100%;
height: 100%;
}
@@ -0,0 +1,168 @@
import './ViewportGrid.css';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
//
import ViewportPane from './ViewportPane.js';
import DefaultViewport from './DefaultViewport.js';
import EmptyViewport from './EmptyViewport.js';
const ViewportGrid = function(props) {
const {
activeViewportIndex,
availablePlugins,
defaultPlugin: defaultPluginName,
layout,
numRows,
numColumns,
setViewportData,
studies,
viewportData,
children,
} = props;
const rowSize = 100 / numRows;
const colSize = 100 / numColumns;
// http://grid.malven.co/
if (!viewportData || !viewportData.length) {
return null;
}
const ViewportPanes = layout.viewports.map((layout, viewportIndex) => {
const displaySet = viewportData[viewportIndex];
const data = {
displaySet,
studies,
};
// Use whichever plugin is currently in use in the panel
// unless nothing is specified. If nothing is specified
// and the display set has a plugin specified, use that.
//
// TODO: Change this logic to:
// - Plugins define how capable they are of displaying a SopClass
// - When updating a panel, ensure that the currently enabled plugin
// in the viewport is capable of rendering this display set. If not
// then use the most capable available plugin
const pluginName =
!layout.plugin && displaySet && displaySet.plugin
? displaySet.plugin
: layout.plugin;
const ViewportComponent = _getViewportComponent(
data, // Why do we pass this as `ViewportData`, when that's not really what it is?
viewportIndex,
children,
availablePlugins,
pluginName,
defaultPluginName
);
return (
<ViewportPane
onDrop={({
viewportIndex,
studyInstanceUid,
displaySetInstanceUid,
}) => {
setViewportData({
viewportIndex,
studyInstanceUid,
displaySetInstanceUid,
});
}}
viewportIndex={viewportIndex} // Needed by `setViewportData`
className={classNames('viewport-container', {
active: activeViewportIndex === viewportIndex,
})}
key={viewportIndex}
>
{ViewportComponent}
</ViewportPane>
);
});
return (
<div
style={{
display: 'grid',
gridTemplateRows: `repeat(${numRows}, ${rowSize}%)`,
gridTemplateColumns: `repeat(${numColumns}, ${colSize}%)`,
height: '100%',
width: '100%',
}}
>
{ViewportPanes}
</div>
);
};
ViewportGrid.propTypes = {
viewportData: PropTypes.array.isRequired,
supportsDragAndDrop: PropTypes.bool.isRequired,
activeViewportIndex: PropTypes.number.isRequired,
layout: PropTypes.object.isRequired,
availablePlugins: PropTypes.object.isRequired,
setViewportData: PropTypes.func.isRequired,
studies: PropTypes.array,
children: PropTypes.node,
};
ViewportGrid.defaultProps = {
viewportData: [],
numRows: 1,
numColumns: 1,
layout: {
viewports: [{}],
},
activeViewportIndex: 0,
supportsDragAndDrop: true,
availablePlugins: {
DefaultViewport,
},
defaultPlugin: 'defaultViewportPlugin',
};
/**
*
*
* @param {*} plugin
* @param {*} viewportData
* @param {*} viewportIndex
* @param {*} children
* @returns
*/
function _getViewportComponent(
viewportData,
viewportIndex,
children,
availablePlugins,
pluginName,
defaultPluginName
) {
if (viewportData.displaySet) {
pluginName = pluginName || defaultPluginName;
const ViewportComponent = availablePlugins[pluginName];
if (!ViewportComponent) {
throw new Error(
`No Viewport Component available for name ${pluginName}.
Available plugins: ${JSON.stringify(availablePlugins)}`
);
}
return (
<ViewportComponent
viewportData={viewportData}
viewportIndex={viewportIndex}
children={[children]}
/>
);
}
return <EmptyViewport />;
}
export default ViewportGrid;
@@ -0,0 +1,10 @@
.viewport-drop-target {
opacity: 1;
position: relative; /* Locks in Scrollbar */
transition: 0.3s all ease;
}
.viewport-drop-target.hovered {
opacity: 0.5;
cursor: copy;
}
@@ -0,0 +1,51 @@
import React from 'react';
import { useDrop } from 'react-dnd';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import './ViewportPane.css';
const ViewportPane = function(props) {
const { children, onDrop, viewportIndex, className: propClassName } = props;
const [{ hovered, highlighted }, drop] = useDrop({
accept: 'thumbnail',
drop: (droppedItem, monitor) => {
const canDrop = monitor.canDrop();
const isOver = monitor.isOver();
if (canDrop && isOver && onDrop) {
const { studyInstanceUid, displaySetInstanceUid } = droppedItem;
onDrop({ viewportIndex, studyInstanceUid, displaySetInstanceUid });
}
},
// Monitor, and collect props.
// Returned as values by `useDrop`
collect: monitor => ({
highlighted: monitor.canDrop(),
hovered: monitor.isOver(),
}),
});
return (
<div
className={classNames(
'viewport-drop-target',
{ hovered: hovered },
{ highlighted: highlighted },
propClassName
)}
ref={drop}
>
{children}
</div>
);
};
ViewportPane.propTypes = {
children: PropTypes.node.isRequired,
viewportIndex: PropTypes.number.isRequired,
onDrop: PropTypes.func.isRequired,
className: PropTypes.string,
};
export default ViewportPane;
@@ -0,0 +1,5 @@
import ConnectedViewportGrid from './ConnectedViewportGrid.js';
import ViewportGrid from './ViewportGrid.js';
export default ViewportGrid;
export { ConnectedViewportGrid, ViewportGrid };
@@ -7,7 +7,7 @@ const { setLayout, setViewportActive } = OHIF.redux.actions;
const mapStateToProps = state => {
return {
currentLayout: state.viewports.layout,
activeViewportIndex: state.viewports.activeViewportIndex
activeViewportIndex: state.viewports.activeViewportIndex,
};
};
@@ -15,10 +15,11 @@ const mapDispatchToProps = dispatch => {
return {
// TODO: Change if layout switched becomes more complex
onChange: (selectedCell, currentLayout, activeViewportIndex) => {
let viewports = [];
const rows = selectedCell.row + 1;
const columns = selectedCell.col + 1;
const numViewports = rows * columns;
const viewports = [];
const numRows = selectedCell.row + 1;
const numColumns = selectedCell.col + 1;
const numViewports = numRows * numColumns;
for (let i = 0; i < numViewports; i++) {
// Hacky way to allow users to exit MPR "mode"
const viewport = currentLayout.viewports[i];
@@ -28,16 +29,16 @@ const mapDispatchToProps = dispatch => {
}
viewports.push({
height: `${100 / rows}%`,
width: `${100 / columns}%`,
plugin,
});
}
const layout = {
numRows,
numColumns,
viewports,
};
const maxActiveIndex = rows * columns - 1;
const maxActiveIndex = numViewports - 1;
if (activeViewportIndex > maxActiveIndex) {
dispatch(setViewportActive(0));
}
@@ -52,9 +53,10 @@ const mergeProps = (propsFromState, propsFromDispatch) => {
const { currentLayout, activeViewportIndex } = propsFromState;
return {
onChange: selectedCell => onChangeFromDispatch(selectedCell, currentLayout, activeViewportIndex)
onChange: selectedCell =>
onChangeFromDispatch(selectedCell, currentLayout, activeViewportIndex),
};
}
};
const ConnectedLayoutButton = connect(
mapStateToProps,
@@ -1,17 +1,22 @@
import OHIF from "@ohif/core";
import PluginSwitch from "./PluginSwitch.js";
import { commandsManager } from "./../App.js";
import { connect } from "react-redux";
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
layout,
};
};
@@ -19,7 +24,7 @@ const mapDispatchToProps = dispatch => {
return {
setLayout: data => {
dispatch(setLayout(data));
}
},
};
};
@@ -33,25 +38,35 @@ const mapDispatchToProps = dispatch => {
}*/
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
//const { activeViewportIndex, layout } = propsFromState;
//const { setLayout } = propsFromDispatch;
const { activeViewportIndex, viewportSpecificData } = propsFromState;
const { studies } = ownProps;
const { setLayout } = propsFromDispatch;
// TODO: Do not display certain options if the current display set
// cannot be displayed using these view types
const mpr = () => {
commandsManager.runCommand("mpr2d");
}
;
commandsManager.runCommand('mpr2d');
};
const exitMpr = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
setLayout(layout);
};
return {
mpr
mpr,
exitMpr,
activeViewportIndex,
viewportSpecificData,
studies,
};
};
const ConnectedPluginSwitch = connect(
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(PluginSwitch);
export default ConnectedPluginSwitch;
)(ConnectedPluginSwitch);
@@ -6,19 +6,101 @@ 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 (
<div className="PluginSwitch">
<ToolbarButton label = "2D MPR"
icon = "cube"
onClick = {this.props.mpr} />
</div>
<>
{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;
@@ -25,6 +25,7 @@ class ToolbarRow extends Component {
selectedRightSidePanel: PropTypes.string.isRequired,
handleSidePanelChange: PropTypes.func,
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
studies: PropTypes.array,
};
constructor(props) {
@@ -128,7 +129,7 @@ class ToolbarRow extends Component {
</div>
{buttonComponents}
<ConnectedLayoutButton />
<ConnectedPluginSwitch />
<ConnectedPluginSwitch studies={this.props.studies} />
<div
className="pull-right m-t-1 rm-x-1"
style={{ marginLeft: 'auto' }}
@@ -229,7 +230,6 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
});
}
/**
* A handy way for us to handle different button types. IE. firing commands for
* buttons, or initiation built in behavior.
@@ -279,6 +279,7 @@ class Viewer extends Component {
this.setState(updatedState);
}}
studies={this.props.studies}
/>
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
@@ -1,7 +1,7 @@
import './ViewerMain.css';
import { Component } from 'react';
import ConnectedLayoutManager from './ConnectedLayoutManager.js';
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
import ConnectedToolContextMenu from './ConnectedToolContextMenu.js';
import PropTypes from 'prop-types';
import React from 'react';
@@ -124,11 +124,15 @@ class ViewerMain extends Component {
return viewportData;
};
setViewportData = ({ viewportIndex, item }) => {
setViewportData = ({
viewportIndex,
studyInstanceUid,
displaySetInstanceUid,
}) => {
const displaySet = this.findDisplaySet(
this.props.studies,
item.studyInstanceUid,
item.displaySetInstanceUid
studyInstanceUid,
displaySetInstanceUid
);
this.props.setViewportSpecificData(viewportIndex, displaySet);
@@ -138,14 +142,14 @@ class ViewerMain extends Component {
return (
<div className="ViewerMain">
{this.state.displaySets.length && (
<ConnectedLayoutManager
<ConnectedViewportGrid
studies={this.props.studies}
viewportData={this.getViewportData()}
setViewportData={this.setViewportData}
>
{/* Children to add to each viewport that support children */}
<ConnectedToolContextMenu />
</ConnectedLayoutManager>
</ConnectedViewportGrid>
)}
</div>
);
@@ -1,6 +1,5 @@
// https://github.com/maxmantz/redux-oidc/blob/master/docs/API.md
import { loadUser, createUserManager } from 'redux-oidc';
import { WebStorageStateStore, InMemoryWebStorage } from 'oidc-client';
/**
* Creates a userManager from oidcSettings;
@@ -21,17 +20,12 @@ export default function(store, oidcSettings) {
return;
}
// Do not store tokens in localStorage or sessionStorage
// https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTML5_Security_Cheat_Sheet.md#local-storage
const userStore = new WebStorageStateStore({ store: new InMemoryWebStorage() });
const settings = {
...oidcSettings,
automaticSilentRenew: true,
revokeAccessTokenOnSignout: true,
filterProtocolClaims: true,
loadUserInfo: true,
userStore,
};
const userManager = createUserManager(settings);
+15 -8
View File
@@ -1,6 +1,6 @@
/* Sizes */
:root {
/* Sizes */
--top-bar-height: 40px;
--top-bar-expanded-height: 160px;
--toolbar-height: 78px;
@@ -9,19 +9,26 @@
--right-sidepanel-menu-width: 323px;
--study-list-padding: 8%;
--study-list-padding-medium-screen: 10px;
}
--snackbar-size: 344px;
/* Transitions */
/* Transitions */
:root {
--transition-duration: 0.3s;
--transition-effect: ease;
--sidepanel-transition: all 0.3s ease;
}
/* Thicknesses */
/* Thicknesses */
:root {
--viewport-border-thickness: 1px;
--ui-border-thickness: 1px;
/** Colors **/
--snackbar-success: rgba(94, 164, 0, 0.9);
--snackbar-error: rgba(236, 61, 61, 0.9);
--snackbar-warning: rgba(235, 173, 23, 0.9);
--snackbar-info: rgba(54, 156, 199, 0.9);
/** z-Index **/
--snackbar-zIndex: 10;
}
+959 -404
View File
File diff suppressed because it is too large. Load diff