Compare commits
174
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cf1e54490 | ||
|
|
85f836cd91 | ||
|
|
f5560c11b0 | ||
|
|
bef50f9298 | ||
|
|
986b7ae2bf | ||
|
|
86c479ac04 | ||
|
|
460e375f0a | ||
|
|
78344c2c66 | ||
|
|
b451ce4407 | ||
|
|
21b586b08f | ||
|
|
3bbeb1c521 | ||
|
|
7668b69f20 | ||
|
|
3a12138134 | ||
|
|
97786b808c | ||
|
|
cd1baa1b36 | ||
|
|
3ae3bee452 | ||
|
|
fd017c4512 | ||
|
|
c680720ce5 | ||
|
|
dfc4e89a91 | ||
|
|
13d337aaab | ||
|
|
4f26940005 | ||
|
|
9d08e81aa5 | ||
|
|
1a539d04c2 | ||
|
|
5cc78c867f | ||
|
|
cefa480979 | ||
|
|
66537bf1b9 | ||
|
|
862d426c71 | ||
|
|
e62f403fe9 | ||
|
|
dea0ceab57 | ||
|
|
a6c1e6c717 | ||
|
|
b03739428f | ||
|
|
2a52d9f05d | ||
|
|
d5e21bb5bb | ||
|
|
04ffe3ec76 | ||
|
|
9cdf8b690b | ||
|
|
8d49b31d03 | ||
|
|
e4cfae7ad9 | ||
|
|
e53041f630 | ||
|
|
35c54eac56 | ||
|
|
e7be573328 | ||
|
|
8a7706b286 | ||
|
|
c217b8bbde | ||
|
|
74f3cbc4b2 | ||
|
|
d091cd604c | ||
|
|
3a4280163d | ||
|
|
2d75e01ea0 | ||
|
|
5c5a49486d | ||
|
|
a8a57132a7 | ||
|
|
92c8996ed4 | ||
|
|
f07dca5fa4 | ||
|
|
c99e0d8a23 | ||
|
|
84144bc3ef | ||
|
|
5cbdd0c616 | ||
|
|
fc015326c7 | ||
|
|
01860b6094 | ||
|
|
cfdf827ff0 | ||
|
|
ceb6837153 | ||
|
|
b01b0108b0 | ||
|
|
d6862e7418 | ||
|
|
beb7083e55 | ||
|
|
412fe4e23f | ||
|
|
659649e10b | ||
|
|
f431b277dd | ||
|
|
2bc361cdca | ||
|
|
c6f306c0de | ||
|
|
c0ea27f6b3 | ||
|
|
df8d4029f1 | ||
|
|
a36cb63926 | ||
|
|
e51529e6c9 | ||
|
|
750a150644 | ||
|
|
a9d28d85da | ||
|
|
6ee5094cbe | ||
|
|
6bfb9bb140 | ||
|
|
db5b760432 | ||
|
|
e7de8be2d8 | ||
|
|
cb2088daf7 | ||
|
|
2a743554b6 | ||
|
|
a28984ef1f | ||
|
|
95e5e11d09 | ||
|
|
ff9f199850 | ||
|
|
1cc94f36a7 | ||
|
|
5c5223fcf1 | ||
|
|
8513ad9ef3 | ||
|
|
66d8bc6d6f | ||
|
|
0064a4b1cd | ||
|
|
74ce89b22b | ||
|
|
c92975f555 | ||
|
|
ce6c0533a0 | ||
|
|
4df624b8c4 | ||
|
|
e064fd353e | ||
|
|
458d310c78 | ||
|
|
73bc4d9094 | ||
|
|
7709ebb107 | ||
|
|
559b637290 | ||
|
|
36910f2b47 | ||
|
|
9e10c2b2de | ||
|
|
c5cbd77cb9 | ||
|
|
b3b4c10e4c | ||
|
|
855020d730 | ||
|
|
8503cbce4e | ||
|
|
c8667fb2d6 | ||
|
|
674ca9f96f | ||
|
|
7a4b97b4e8 | ||
|
|
c79c0c301d | ||
|
|
ff5060f276 | ||
|
|
dafb26b34b | ||
|
|
c4f9d5e750 | ||
|
|
cf40a8388c | ||
|
|
d596ab5c3d | ||
|
|
72198eee7d | ||
|
|
057f68a9b4 | ||
|
|
034ca64d6b | ||
|
|
4a0901aee9 | ||
|
|
d0ec9cf0e0 | ||
|
|
71bea0d4d8 | ||
|
|
e607ed2aa0 | ||
|
|
4939f43509 | ||
|
|
c80ea17cc2 | ||
|
|
aa7dc1e2c1 | ||
|
|
e7e1a8a6cd | ||
|
|
e66c27affb | ||
|
|
ae26904885 | ||
|
|
8bb730318a | ||
|
|
69b96478cb | ||
|
|
816666df7b | ||
|
|
e375a4ab11 | ||
|
|
7f4d71e412 | ||
|
|
a90605c82d | ||
|
|
5b523c2afa | ||
|
|
5551f817e3 | ||
|
|
a9ca3fa026 | ||
|
|
89380353d2 | ||
|
|
edb86fad55 | ||
|
|
395a1be3fb | ||
|
|
65d852b9e4 | ||
|
|
e797a3a141 | ||
|
|
246936f90b | ||
|
|
c57298475a | ||
|
|
6a5ebb3e25 | ||
|
|
291b23aee1 | ||
|
|
72427fe2ff | ||
|
|
882d04c9bf | ||
|
|
d1a24bc9d9 | ||
|
|
16a87fa75f | ||
|
|
6fdac4d605 | ||
|
|
6192502fe0 | ||
|
|
2341b32638 | ||
|
|
45aeaec916 | ||
|
|
66b84ae9c2 | ||
|
|
bc28fe26ac | ||
|
|
7f642a05da | ||
|
|
66a0aadcce | ||
|
|
808af7ee96 | ||
|
|
10b5e071f4 | ||
|
|
fe1969b071 | ||
|
|
16b2a8295f | ||
|
|
d33bb4210d | ||
|
|
ab681b9929 | ||
|
|
f52daca5e5 | ||
|
|
4afc88c0e5 | ||
|
|
f519f8609c | ||
|
|
e27d5b58a4 | ||
|
|
a18b292a9d | ||
|
|
7bd1507a89 | ||
|
|
a634177578 | ||
|
|
fe4d71ad0b | ||
|
|
f723546e6b | ||
|
|
348b8949d4 | ||
|
|
26e037dee7 | ||
|
|
26be96783d | ||
|
|
029ba1fb4a | ||
|
|
56d658f02d | ||
|
|
64921b905f | ||
|
|
d00d7daf41 |
No files matched your search
+179
-52
@@ -13,50 +13,111 @@ version: 2.1
|
|||||||
##
|
##
|
||||||
orbs:
|
orbs:
|
||||||
codecov: codecov/codecov@1.0.5
|
codecov: codecov/codecov@1.0.5
|
||||||
cypress: cypress-io/cypress@1.10.0
|
cypress: cypress-io/cypress@1.11.0
|
||||||
|
|
||||||
defaults: &defaults
|
defaults: &defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:10.16.0
|
- image: circleci/node:12.9.1
|
||||||
environment:
|
environment:
|
||||||
TERM: xterm # Enable colors in term
|
TERM: xterm # Enable colors in term
|
||||||
working_directory: ~/repo
|
working_directory: ~/repo
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CHECKOUT:
|
UNIT_TESTS:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
|
# Update yarn
|
||||||
|
- run: yarn -v
|
||||||
|
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
|
||||||
|
|
||||||
# Enable yarn workspaces
|
# Enable yarn workspaces
|
||||||
- run: yarn config set workspaces-experimental true
|
- run: yarn config set workspaces-experimental true
|
||||||
|
|
||||||
# Checkout code and ALL Git Tags
|
# Checkout code and ALL Git Tags
|
||||||
- checkout:
|
- checkout:
|
||||||
post:
|
post:
|
||||||
- git fetch --all
|
- git fetch --all
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
name: Restore Yarn and Cypress Package Cache
|
name: Restore Yarn and Cypress Package Cache
|
||||||
keys:
|
keys:
|
||||||
# when lock file changes, use increasingly general patterns to restore cache
|
# when lock file changes, use increasingly general patterns to restore cache
|
||||||
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
- yarn-packages-v1-{{ .Branch }}-
|
- yarn-packages-
|
||||||
- yarn-packages-v1-
|
|
||||||
- run:
|
- run:
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: yarn install --frozen-lockfile
|
command: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- save_cache:
|
- save_cache:
|
||||||
name: Save Yarn Package Cache
|
name: Save Yarn Package Cache
|
||||||
paths:
|
paths:
|
||||||
- ~/.cache ## Cache yarn and Cypress
|
- ~/.cache ## Cache yarn and Cypress
|
||||||
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
# Persist :+1:
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: ~/repo
|
|
||||||
paths: .
|
|
||||||
|
|
||||||
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
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
# Update yarn
|
||||||
at: ~/repo
|
#- 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 TESTS
|
||||||
- run:
|
- run:
|
||||||
@@ -92,7 +153,7 @@ jobs:
|
|||||||
flags: 'core'
|
flags: 'core'
|
||||||
|
|
||||||
# Persist :+1:
|
# Persist :+1:
|
||||||
- persist_to_workspace:
|
- persist_to_workspace: &persist_to_workspace
|
||||||
root: ~/repo
|
root: ~/repo
|
||||||
paths: .
|
paths: .
|
||||||
|
|
||||||
@@ -137,25 +198,25 @@ jobs:
|
|||||||
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
|
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
|
||||||
- run: ~/repo/.circleci/build-and-publish-docs.sh
|
- run: ~/repo/.circleci/build-and-publish-docs.sh
|
||||||
|
|
||||||
DOCKER_PUBLISH:
|
DOCKER_MASTER_PUBLISH:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/repo
|
at: ~/repo
|
||||||
- setup_remote_docker:
|
- setup_remote_docker:
|
||||||
docker_layer_caching: true
|
docker_layer_caching: false
|
||||||
- run:
|
- run:
|
||||||
name: Build and push Docker image
|
name: Build and push Docker image
|
||||||
command: |
|
command: |
|
||||||
# This file will exist if a new version was published by
|
# This file will exist if a new version was published by
|
||||||
# our `semantic-release` command in the previous job
|
# our `semantic-release` command in the previous job
|
||||||
if [[ ! -e tmp/updated-version.txt ]]; then
|
if [[ ! -e platform/viewer/success_version.txt ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
# Remove npm config
|
# Remove npm config
|
||||||
rm -f ./.npmrc
|
rm -f ./.npmrc
|
||||||
# Set our version number using vars
|
# Set our version number using vars
|
||||||
export IMAGE_VERSION=$(cat tmp/updated-version.txt)
|
export IMAGE_VERSION=$(cat platform/viewer/success_version.txt)
|
||||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
|
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
|
||||||
echo $IMAGE_VERSION
|
echo $IMAGE_VERSION
|
||||||
echo $IMAGE_VERSION_FULL
|
echo $IMAGE_VERSION_FULL
|
||||||
@@ -166,6 +227,43 @@ jobs:
|
|||||||
docker push ohif/$IMAGE_NAME:latest
|
docker push ohif/$IMAGE_NAME:latest
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
DOCKER_PR_PUBLISH:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
# Enable yarn workspaces
|
||||||
|
- run: yarn config set workspaces-experimental true
|
||||||
|
|
||||||
|
# Checkout code and ALL Git Tags
|
||||||
|
- checkout:
|
||||||
|
post:
|
||||||
|
- git fetch --all
|
||||||
|
|
||||||
|
- restore_cache:
|
||||||
|
name: Restore Yarn and Cypress Package Cache
|
||||||
|
keys:
|
||||||
|
# when lock file changes, use increasingly general patterns to restore cache
|
||||||
|
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
|
- yarn-packages-
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- setup_remote_docker:
|
||||||
|
docker_layer_caching: false
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Build and push Docker image
|
||||||
|
command: |
|
||||||
|
# Remove npm config
|
||||||
|
rm -f ./.npmrc
|
||||||
|
# Set our version number using vars
|
||||||
|
echo $CIRCLE_BUILD_NUM
|
||||||
|
# Build our image, auth, and push
|
||||||
|
docker build --tag ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM .
|
||||||
|
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||||
|
docker push ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM
|
||||||
|
|
||||||
VIEWER_DEMO_BUILD:
|
VIEWER_DEMO_BUILD:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
@@ -175,7 +273,7 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: 'Build Demo Site'
|
name: 'Build Demo Site'
|
||||||
command: yarn run build:demo
|
command: yarn run build:demo
|
||||||
no_output_timeout: 30m
|
no_output_timeout: 45m
|
||||||
- run:
|
- run:
|
||||||
name: 'Upload SourceMaps, Send Deploy Notification'
|
name: 'Upload SourceMaps, Send Deploy Notification'
|
||||||
command: |
|
command: |
|
||||||
@@ -199,7 +297,7 @@ jobs:
|
|||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/repo
|
at: ~/repo
|
||||||
- setup_remote_docker:
|
- setup_remote_docker:
|
||||||
docker_layer_caching: true
|
docker_layer_caching: false
|
||||||
- run:
|
- run:
|
||||||
name: Deploy latest version to viewer.ohif.org
|
name: Deploy latest version to viewer.ohif.org
|
||||||
command: |
|
command: |
|
||||||
@@ -217,103 +315,133 @@ workflows:
|
|||||||
# PULL REQUESTS
|
# PULL REQUESTS
|
||||||
PR_CHECKS:
|
PR_CHECKS:
|
||||||
jobs:
|
jobs:
|
||||||
- CHECKOUT:
|
- UNIT_TESTS:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore:
|
ignore:
|
||||||
- master
|
- master
|
||||||
- feature/*
|
- feature/*
|
||||||
- hotfix/*
|
- hotfix/*
|
||||||
- UNIT_TESTS:
|
|
||||||
requires:
|
|
||||||
- CHECKOUT
|
|
||||||
# E2E: PWA
|
# E2E: PWA
|
||||||
- cypress/run:
|
- cypress/run:
|
||||||
name: 'E2E: PWA'
|
name: 'E2E: PWA'
|
||||||
|
executor: cypress/browsers-chrome76
|
||||||
|
browser: chrome
|
||||||
pre-steps:
|
pre-steps:
|
||||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||||
add wait-on' # Use yarn latest
|
add wait-on' # Use yarn latest
|
||||||
yarn: true
|
yarn: true
|
||||||
store_artifacts: true
|
record: false
|
||||||
|
store_artifacts: false
|
||||||
working_directory: platform/viewer
|
working_directory: platform/viewer
|
||||||
build: yarn run build
|
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||||
start: yarn run test:e2e:dist
|
start: yarn run test:e2e:serve
|
||||||
wait-on: 'http://localhost:3000'
|
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
|
no-workspace: true # Don't persist workspace
|
||||||
|
post-steps:
|
||||||
|
- store_artifacts:
|
||||||
|
path: platform/viewer/cypress/screenshots
|
||||||
requires:
|
requires:
|
||||||
- UNIT_TESTS
|
- UNIT_TESTS
|
||||||
# E2E: script-tag
|
# E2E: script-tag
|
||||||
- cypress/run:
|
- cypress/run:
|
||||||
name: 'E2E: Script Tag'
|
name: 'E2E: Script Tag'
|
||||||
|
executor: cypress/browsers-chrome76
|
||||||
|
browser: chrome
|
||||||
pre-steps:
|
pre-steps:
|
||||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||||
add wait-on' # Use yarn latest
|
add wait-on' # Use yarn latest
|
||||||
yarn: true
|
yarn: true
|
||||||
store_artifacts: true
|
record: false
|
||||||
|
store_artifacts: false
|
||||||
working_directory: platform/viewer
|
working_directory: platform/viewer
|
||||||
build: yarn run build:package
|
build: npx cross-env QUICK_BUILD=true yarn run build:package
|
||||||
start: yarn run test:e2e:dist
|
start: yarn run test:e2e:serve
|
||||||
|
spec: 'cypress/integration/common/**/*'
|
||||||
wait-on: 'http://localhost:3000'
|
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
|
no-workspace: true # Don't persist workspace
|
||||||
|
post-steps:
|
||||||
|
- store_artifacts:
|
||||||
|
path: platform/viewer/cypress/screenshots
|
||||||
requires:
|
requires:
|
||||||
- UNIT_TESTS
|
- UNIT_TESTS
|
||||||
|
|
||||||
|
PR_OPTIONAL_DOCKER_PUBLISH:
|
||||||
|
jobs:
|
||||||
|
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
|
||||||
|
- AWAIT_APPROVAL:
|
||||||
|
type: approval
|
||||||
|
# Update hub.docker.org
|
||||||
|
- DOCKER_PR_PUBLISH:
|
||||||
|
context: Docker Hub
|
||||||
|
requires:
|
||||||
|
- AWAIT_APPROVAL
|
||||||
|
|
||||||
# MERGE TO MASTER
|
# MERGE TO MASTER
|
||||||
RELEASE:
|
RELEASE:
|
||||||
jobs:
|
jobs:
|
||||||
- CHECKOUT:
|
- UNIT_TESTS_PERSIST_WORKSPACE:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: master
|
only: master
|
||||||
- UNIT_TESTS:
|
|
||||||
requires:
|
|
||||||
- CHECKOUT
|
|
||||||
# E2E: PWA + Persist
|
# E2E: PWA + Persist
|
||||||
- cypress/run:
|
- cypress/run:
|
||||||
name: 'E2E: PWA'
|
name: 'E2E: PWA'
|
||||||
|
executor: cypress/browsers-chrome76
|
||||||
|
browser: chrome
|
||||||
pre-steps:
|
pre-steps:
|
||||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||||
add wait-on' # Use yarn latest
|
add wait-on' # Use yarn latest
|
||||||
yarn: true
|
yarn: true
|
||||||
record: true
|
record: false
|
||||||
store_artifacts: true
|
store_artifacts: true
|
||||||
working_directory: platform/viewer
|
working_directory: platform/viewer
|
||||||
build: yarn run build
|
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||||
start: yarn run test:e2e:dist
|
start: yarn run test:e2e:serve
|
||||||
wait-on: 'http://localhost:3000'
|
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
|
no-workspace: true # Don't persist workspace
|
||||||
post-steps:
|
post-steps:
|
||||||
|
- store_artifacts:
|
||||||
|
path: platform/viewer/cypress/screenshots
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: cypress/results
|
path: cypress/results
|
||||||
requires:
|
requires:
|
||||||
- UNIT_TESTS
|
- UNIT_TESTS_PERSIST_WORKSPACE
|
||||||
# E2E: script-tag
|
# E2E: script-tag
|
||||||
- cypress/run:
|
- cypress/run:
|
||||||
name: 'E2E: Script Tag'
|
name: 'E2E: Script Tag'
|
||||||
|
executor: cypress/browsers-chrome76
|
||||||
|
browser: chrome
|
||||||
pre-steps:
|
pre-steps:
|
||||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||||
add wait-on' # Use yarn latest
|
add wait-on' # Use yarn latest
|
||||||
yarn: true
|
yarn: true
|
||||||
store_artifacts: true
|
record: false
|
||||||
|
store_artifacts: false
|
||||||
working_directory: platform/viewer
|
working_directory: platform/viewer
|
||||||
build: yarn run build:package
|
build: npx cross-env QUICK_BUILD=true yarn run build:package
|
||||||
start: yarn run test:e2e:dist
|
start: yarn run test:e2e:serve
|
||||||
|
spec: 'cypress/integration/common/**/*'
|
||||||
wait-on: 'http://localhost:3000'
|
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
|
no-workspace: true # Don't persist workspace
|
||||||
post-steps:
|
post-steps:
|
||||||
|
- store_artifacts:
|
||||||
|
path: platform/viewer/cypress/screenshots
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: cypress/results
|
path: cypress/results
|
||||||
requires:
|
requires:
|
||||||
- UNIT_TESTS
|
- UNIT_TESTS_PERSIST_WORKSPACE
|
||||||
# Update NPM
|
# Update NPM
|
||||||
- NPM_PUBLISH:
|
- NPM_PUBLISH:
|
||||||
requires:
|
requires:
|
||||||
- 'E2E: Script Tag'
|
- 'E2E: Script Tag'
|
||||||
- 'E2E: PWA'
|
- 'E2E: PWA'
|
||||||
|
# Update docs.ohif.org
|
||||||
- DOCS_PUBLISH:
|
- DOCS_PUBLISH:
|
||||||
requires:
|
requires:
|
||||||
- 'E2E: Script Tag'
|
- 'E2E: Script Tag'
|
||||||
@@ -322,12 +450,11 @@ workflows:
|
|||||||
requires:
|
requires:
|
||||||
- 'E2E: Script Tag'
|
- 'E2E: Script Tag'
|
||||||
- 'E2E: PWA'
|
- 'E2E: PWA'
|
||||||
|
# Update viewer.ohif.org
|
||||||
- VIEWER_DEMO_PUBLISH:
|
- VIEWER_DEMO_PUBLISH:
|
||||||
requires:
|
requires:
|
||||||
- VIEWER_DEMO_BUILD
|
- VIEWER_DEMO_BUILD
|
||||||
# Update hub.docker.org
|
# Update hub.docker.org
|
||||||
# - DOCKER_PUBLISH:
|
- DOCKER_MASTER_PUBLISH:
|
||||||
# requires:
|
requires:
|
||||||
# - NPM_PUBLISH
|
- NPM_PUBLISH
|
||||||
# Update viewer.ohif.org
|
|
||||||
# Update docs.ohif.org
|
|
||||||
+3
-1
@@ -8,11 +8,13 @@ coverage:
|
|||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
threshold: 0.10%
|
threshold: 0.5%
|
||||||
core:
|
core:
|
||||||
flags: core
|
flags: core
|
||||||
|
threshold: 0.5%
|
||||||
viewer:
|
viewer:
|
||||||
flags: viewer
|
flags: viewer
|
||||||
|
threshold: 0.5%
|
||||||
patch: off
|
patch: off
|
||||||
flags:
|
flags:
|
||||||
core:
|
core:
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
|
||||||
|
if [ ! -z "$CLIENT_ID" ]
|
||||||
|
then
|
||||||
|
echo "Google Cloud Healthcare $CLIENT_ID has been provided: "
|
||||||
|
echo $CLIENT_ID
|
||||||
|
echo "Updating config..."
|
||||||
|
|
||||||
|
# - Use SED to replace the CLIENT_ID that is currently in google.js
|
||||||
|
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
|
||||||
|
|
||||||
|
# - Copy google.js to overwrite app-config.js
|
||||||
|
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting Nginx to serve the OHIF Viewer..."
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Reduces size of context and hides
|
||||||
|
# files from Docker (can't COPY or ADD these)
|
||||||
|
|
||||||
|
# Output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Root
|
||||||
|
README.md
|
||||||
|
Dockerfile
|
||||||
|
dockerfile
|
||||||
|
|
||||||
|
# Misc. Config
|
||||||
|
.git
|
||||||
|
.DS_Store
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.circleci
|
||||||
|
|
||||||
|
# Unnecessary things to pull into container
|
||||||
|
.circleci/
|
||||||
|
.github/
|
||||||
|
.netlify/
|
||||||
|
.scripts/
|
||||||
|
.vscode/
|
||||||
|
coverage/
|
||||||
|
docs/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PERCY_TOKEN=<your token here>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
### Request Checklist
|
||||||
|
|
||||||
|
- [] Brief description of changes
|
||||||
|
- [] Links to any relevant issues
|
||||||
|
- [] Required status checks are passing
|
||||||
|
- [] `@mention` a maintainer to request a review
|
||||||
|
|
||||||
|
#### Special
|
||||||
|
|
||||||
|
> If this PR is not on a branch for this repo, it is "untrusted" and does not
|
||||||
|
> have access to env vars. Reviewers can kick off a trusted docker deploy by:
|
||||||
|
>
|
||||||
|
> 1. Verifying there are no malicious changes to CI build
|
||||||
|
> 2. Using the [git-push-fork-to-upstream-branch][script] script
|
||||||
|
> 3. Executing the manual approval step in the CI workflow
|
||||||
|
|
||||||
|
- [] Request docker publish of PR
|
||||||
|
|
||||||
|
_Want to improve this process? Consider PR'ing the ability for reviewers to add
|
||||||
|
a label to trigger this process._
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Links
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/
|
||||||
|
[script]: https://github.com/jklukas/git-push-fork-to-upstream-branch
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
+3
-3
@@ -7,13 +7,13 @@ daysUntilStale: 21
|
|||||||
daysUntilClose: 7
|
daysUntilClose: 7
|
||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- 'Story 🙌'
|
- 'Story :raised_hands:'
|
||||||
- 'Bug: Verified 🐛'
|
- 'Bug: Verified :bug:'
|
||||||
- 'Task: CI/Tooling 🤖'
|
- 'Task: CI/Tooling 🤖'
|
||||||
- 'Task: Docs 📖'
|
- 'Task: Docs 📖'
|
||||||
- 'Task: Docs :book:'
|
- 'Task: Docs :book:'
|
||||||
- 'Task: Refactor 🛠'
|
- 'Task: Refactor 🛠'
|
||||||
- 'Task: Tests 🔬'
|
- 'Task: Tests :microscope:'
|
||||||
- 'PR: Awaiting Review 👀'
|
- 'PR: Awaiting Review 👀'
|
||||||
- 'Triage 🏳️'
|
- 'Triage 🏳️'
|
||||||
- 'Announcement 🎉'
|
- 'Announcement 🎉'
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ npm-debug.log
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
|
|
||||||
# Common Example Data Directories
|
# Common Example Data Directories
|
||||||
sampledata/
|
sampledata/
|
||||||
|
|||||||
@@ -1,23 +1,54 @@
|
|||||||
|
// ~~ ENV
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
//
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
const PACKAGE = require('../platform/viewer/package.json');
|
||||||
|
// ~~ RULES
|
||||||
const loadShadersRule = require('./rules/loadShaders.js');
|
const loadShadersRule = require('./rules/loadShaders.js');
|
||||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||||
const transpileJavaScriptRule = require('./rules/transpileJavaScript.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 }) => {
|
module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||||
if (!process.env.NODE_ENV) {
|
if (!process.env.NODE_ENV) {
|
||||||
throw new Error('process.env.NODE_ENV not set');
|
throw new Error('process.env.NODE_ENV not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode =
|
const mode = NODE_ENV === 'production' ? 'production' : 'development';
|
||||||
process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
const isProdBuild = NODE_ENV === 'production';
|
||||||
|
const isQuickBuild = QUICK_BUILD === 'true';
|
||||||
|
|
||||||
return {
|
const config = {
|
||||||
mode,
|
mode: isProdBuild ? 'production' : 'development',
|
||||||
|
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
|
||||||
entry: {
|
entry: {
|
||||||
app: `${SRC_DIR}/index.js`,
|
app: `${SRC_DIR}/index.js`,
|
||||||
},
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: isProdBuild,
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
context: SRC_DIR,
|
context: SRC_DIR,
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
hash: true,
|
||||||
|
timings: true,
|
||||||
|
assets: true,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
warnings: true,
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
transpileJavaScriptRule(mode),
|
transpileJavaScriptRule(mode),
|
||||||
@@ -45,6 +76,8 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
||||||
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
|
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
|
||||||
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
|
'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
|
// 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',
|
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;
|
||||||
};
|
};
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
# This dockerfile is used to publish the `ohif/viewer` image on dockerhub.
|
||||||
|
#
|
||||||
|
# It's a good example of how to build our static application and package it
|
||||||
|
# with a web server capable of hosting it as static content.
|
||||||
|
#
|
||||||
|
# docker build
|
||||||
|
# --------------
|
||||||
|
# If you would like to use this dockerfile to build and tag an image, make sure
|
||||||
|
# you set the context to the project's root directory:
|
||||||
|
# https://docs.docker.com/engine/reference/commandline/build/
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# SUMMARY
|
||||||
|
# --------------
|
||||||
|
# This dockerfile has two stages:
|
||||||
|
#
|
||||||
|
# 1. Building the React application for production
|
||||||
|
# 2. Setting up our Nginx (Alpine Linux) image w/ step one's output
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 1: Build the application
|
||||||
|
# docker build -t ohif/viewer:latest .
|
||||||
|
FROM node:10.16.3-slim as builder
|
||||||
|
|
||||||
|
RUN mkdir /usr/src/app
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Copy Files
|
||||||
|
COPY .docker /usr/src/app/.docker
|
||||||
|
COPY .webpack /usr/src/app/.webpack
|
||||||
|
COPY extensions /usr/src/app/extensions
|
||||||
|
COPY platform /usr/src/app/platform
|
||||||
|
COPY .browserslistrc /usr/src/app/.browserslistrc
|
||||||
|
COPY aliases.config.js /usr/src/app/aliases.config.js
|
||||||
|
COPY babel.config.js /usr/src/app/babel.config.js
|
||||||
|
COPY lerna.json /usr/src/app/lerna.json
|
||||||
|
COPY package.json /usr/src/app/package.json
|
||||||
|
COPY postcss.config.js /usr/src/app/postcss.config.js
|
||||||
|
COPY yarn.lock /usr/src/app/yarn.lock
|
||||||
|
|
||||||
|
# Run the install before copying the rest of the files
|
||||||
|
RUN yarn config set workspaces-experimental true
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
|
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||||
|
ENV QUICK_BUILD true
|
||||||
|
# ENV GENERATE_SOURCEMAP=false
|
||||||
|
# ENV REACT_APP_CONFIG=config/default.js
|
||||||
|
|
||||||
|
RUN yarn run build
|
||||||
|
|
||||||
|
# Stage 2: Bundle the built application into a Docker container
|
||||||
|
# which runs Nginx using Alpine Linux
|
||||||
|
FROM nginx:1.15.5-alpine
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
RUN rm -rf /etc/nginx/conf.d
|
||||||
|
COPY .docker/Viewer-v2.x /etc/nginx/conf.d
|
||||||
|
COPY .docker/Viewer-v2.x/entrypoint.sh /usr/src/
|
||||||
|
RUN chmod 777 /usr/src/entrypoint.sh
|
||||||
|
COPY --from=builder /usr/src/app/platform/viewer/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
EXPOSE 443
|
||||||
|
ENTRYPOINT ["/usr/src/entrypoint.sh"]
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
[![Netlify Status][netlify-image]][netlify-url]
|
[![Netlify Status][netlify-image]][netlify-url]
|
||||||
[![CircleCI][circleci-image]][circleci-url]
|
[![CircleCI][circleci-image]][circleci-url]
|
||||||
[![codecov][codecov-image]][codecov-url]
|
[![codecov][codecov-image]][codecov-url]
|
||||||
|
[![This project is using Percy.io for visual regression testing.][percy-image]](percy-url)
|
||||||
[](#contributors)
|
[](#contributors)
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
@@ -123,8 +124,7 @@ window.config = {
|
|||||||
`window.OHIFStandaloneViewer.installViewer(window.config);`
|
`window.OHIFStandaloneViewer.installViewer(window.config);`
|
||||||
|
|
||||||
This exact setup is demonstrated in this
|
This exact setup is demonstrated in this
|
||||||
[CodeSandbox](https://codesandbox.io/s/ohif-viewer-script-tag-usage-c4u4t) and
|
[CodeSandbox](https://codesandbox.io/s/viewer-script-tag-tprch) and in our
|
||||||
in our
|
|
||||||
[Embedding The Viewer](https://docs.ohif.org/deployment/recipes/embedded-viewer.html)
|
[Embedding The Viewer](https://docs.ohif.org/deployment/recipes/embedded-viewer.html)
|
||||||
deployment recipe.
|
deployment recipe.
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ deployment recipe.
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
|
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
|
||||||
- [Node 8+](https://nodejs.org/en/)
|
- [Node 10+](https://nodejs.org/en/)
|
||||||
- Yarn Workspaces should be enabled on your machine:
|
- Yarn Workspaces should be enabled on your machine:
|
||||||
- `yarn config set workspaces-experimental true`
|
- `yarn config set workspaces-experimental true`
|
||||||
|
|
||||||
@@ -289,6 +289,8 @@ MIT © [OHIF](https://github.com/OHIF)
|
|||||||
[docker-image-url]: https://hub.docker.com/r/ohif/viewer
|
[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-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
|
||||||
[license-url]: LICENSE
|
[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 -->
|
<!-- Links -->
|
||||||
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
|
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
|
||||||
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
|
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
|
||||||
|
|||||||
@@ -140,8 +140,8 @@ children: PropTypes.node,
|
|||||||
customProps: PropTypes.object
|
customProps: PropTypes.object
|
||||||
```
|
```
|
||||||
|
|
||||||
Viewport components are managed by the `LayoutManager`. Which Viewport component
|
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
|
||||||
is used depends on:
|
component is used depends on:
|
||||||
|
|
||||||
- The Layout Configuration
|
- The Layout Configuration
|
||||||
- Registered SopClassHandlers
|
- Registered SopClassHandlers
|
||||||
@@ -159,7 +159,8 @@ For a complete example implementation,
|
|||||||
An extension can register a Toolbar Module by providing a `getToolbarModule()`
|
An extension can register a Toolbar Module by providing a `getToolbarModule()`
|
||||||
method that returns a React Component. The component does not receive any props.
|
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
|
If you want to modify or react to state, you will need to connect to the redux
|
||||||
store.
|
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`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -170,6 +171,38 @@ Toolbar components are rendered in the `ToolbarRow` component.
|
|||||||
For a complete example implementation,
|
For a complete example implementation,
|
||||||
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
|
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
|
||||||
|
|
||||||
|
##### 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
|
||||||
|
|
||||||
|
```js
|
||||||
|
definitions: [
|
||||||
|
...
|
||||||
|
{
|
||||||
|
id: 'Custom',
|
||||||
|
label: 'Custom',
|
||||||
|
icon: 'custom-icon',
|
||||||
|
CustomComponent: CustomToolbarComponent,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
`CustomComponent` components will receive the following props:
|
||||||
|
|
||||||
|
- 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
|
||||||
|
- activeButtons: list of active elements
|
||||||
|
- isActive: if current
|
||||||
|
|
||||||
#### SopClassHandler
|
#### SopClassHandler
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -205,5 +205,5 @@ Please feel free to suggest or contribute your own recipes.
|
|||||||
[host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/
|
[host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/
|
||||||
[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||||
[code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
|
[code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
|
||||||
[code-sandbox]: https://codesandbox.io/s/ohif-viewer-script-tag-usage-b3st9
|
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
@@ -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:
|
project's folder. It should roughly resemble the following:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dist/
|
<root>platform/viewer/dist/
|
||||||
├── app-config.js
|
├── app-config.js
|
||||||
├── app.bundle.js
|
├── app.bundle.js
|
||||||
├── app.css
|
├── app.css
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ conflict issues.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
[code-sandbox]: https://codesandbox.io/s/ohif-script-tag-v103-iniiu
|
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
|
||||||
[whole-slide-base-demo]: https://iniiu.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2020.1519719354.757
|
[whole-slide-base-demo]: https://tprch.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2020.1519719354.757
|
||||||
[ext-code-sandbox]: https://codesandbox.io/s/ohif-script-tag-v103-runtime-extensions-34bw5
|
[ext-code-sandbox]: https://codesandbox.io/s/viewer-script-tag-microscopy-extension-44unk
|
||||||
[whole-slide-ext-demo]: https://34bw5.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2448.1519719572.518
|
[whole-slide-ext-demo]: https://44unk.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2448.1519719572.518
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
@@ -34,5 +35,10 @@ module.exports = (env, argv) => {
|
|||||||
libraryExport: 'default',
|
libraryExport: 'default',
|
||||||
filename: pkg.main,
|
filename: pkg.main,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,113 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.0...@ohif/extension-cornerstone@1.3.1) (2019-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 set tools bidirectional, eraser and annotate command ([#1020](https://github.com/OHIF/Viewers/issues/1020)) ([a28984e](https://github.com/OHIF/Viewers/commit/a28984e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.5...@ohif/extension-cornerstone@1.3.0) (2019-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Multiple fixes and implementation changes to react-cornerstone-viewport ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.4...@ohif/extension-cornerstone@1.2.5) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* version bump issue ([#963](https://github.com/OHIF/Viewers/issues/963)) ([e607ed2](https://github.com/OHIF/Viewers/commit/e607ed2)), closes [#962](https://github.com/OHIF/Viewers/issues/962)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||||
|
* version bump issue ([#962](https://github.com/OHIF/Viewers/issues/962)) ([c80ea17](https://github.com/OHIF/Viewers/commit/c80ea17))
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* DICOM Seg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* DICOM Seg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.1...@ohif/extension-cornerstone@1.2.2) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.0...@ohif/extension-cornerstone@1.2.1) (2019-09-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.1...@ohif/extension-cornerstone@1.2.0) (2019-09-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Annotate:** Add annotate tool back to toolbar ([26be967](https://github.com/OHIF/Viewers/commit/26be967))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.0...@ohif/extension-cornerstone@1.1.1) (2019-09-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.0.1...@ohif/extension-cornerstone@1.1.0) (2019-09-12)
|
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.0.1...@ohif/extension-cornerstone@1.1.0) (2019-09-12)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "1.1.0",
|
"version": "1.3.1",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -32,9 +32,9 @@
|
|||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "^4.0.1",
|
"cornerstone-tools": "^4.0.9",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.0",
|
"dcmjs": "^0.6.1",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@@ -48,6 +48,6 @@
|
|||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"react-cornerstone-viewport": "0.1.30"
|
"react-cornerstone-viewport": "2.x.x"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,36 +23,40 @@ const MEASUREMENT_ACTION_MAP = {
|
|||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
let dataFromStore;
|
let dataFromStore;
|
||||||
|
|
||||||
|
// TODO: This may not be updated anymore :thinking:
|
||||||
if (state.extensions && state.extensions.cornerstone) {
|
if (state.extensions && state.extensions.cornerstone) {
|
||||||
dataFromStore = state.extensions.cornerstone;
|
dataFromStore = state.extensions.cornerstone;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This should be extension configuration
|
|
||||||
// ...dataFromStore -->
|
|
||||||
// availableTools,
|
|
||||||
// onNewImage,
|
|
||||||
// onRightClick,
|
|
||||||
// onTouchPress,
|
|
||||||
// onTouchStart,
|
|
||||||
// onMouseClick,
|
|
||||||
|
|
||||||
// If this is the active viewport, enable prefetching.
|
// If this is the active viewport, enable prefetching.
|
||||||
const { viewportIndex } = ownProps; //.viewportData;
|
const { viewportIndex } = ownProps; //.viewportData;
|
||||||
const isActive = viewportIndex === state.viewports.activeViewportIndex;
|
const isActive = viewportIndex === state.viewports.activeViewportIndex;
|
||||||
const viewportSpecificData =
|
const viewportSpecificData =
|
||||||
state.viewports.viewportSpecificData[viewportIndex] || {};
|
state.viewports.viewportSpecificData[viewportIndex] || {};
|
||||||
|
|
||||||
|
// CINE
|
||||||
|
let isPlaying = false;
|
||||||
|
let frameRate = 24;
|
||||||
|
|
||||||
|
if (viewportSpecificData && viewportSpecificData.cine) {
|
||||||
|
const cine = viewportSpecificData.cine;
|
||||||
|
|
||||||
|
isPlaying = cine.isPlaying === true;
|
||||||
|
frameRate = cine.cineFrameRate || frameRate;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
layout: state.viewports.layout,
|
// layout: state.viewports.layout,
|
||||||
isActive,
|
isActive,
|
||||||
// TODO: Need a cleaner and more versatile way.
|
// TODO: Need a cleaner and more versatile way.
|
||||||
// Currently justing using escape hatch + commands
|
// Currently justing using escape hatch + commands
|
||||||
// activeTool: activeButton && activeButton.command,
|
// activeTool: activeButton && activeButton.command,
|
||||||
...dataFromStore,
|
...dataFromStore,
|
||||||
enableStackPrefetch: isActive,
|
isStackPrefetchEnabled: isActive,
|
||||||
|
isPlaying,
|
||||||
|
frameRate,
|
||||||
//stack: viewportSpecificData.stack,
|
//stack: viewportSpecificData.stack,
|
||||||
cineToolData: viewportSpecificData.cine,
|
// viewport: viewportSpecificData.viewport,
|
||||||
viewport: viewportSpecificData.viewport,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import ConnectedCornerstoneViewport from "./ConnectedCornerstoneViewport";
|
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||||
import OHIF from "@ohif/core";
|
import OHIF from '@ohif/core';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import cornerstone from "cornerstone-core";
|
import cornerstone from 'cornerstone-core';
|
||||||
import handleSegmentationStorage from "./handleSegmentationStorage.js";
|
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager } = OHIF.utils;
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ cornerstone.metaData.addProvider(
|
|||||||
StackManager.setMetadataProvider(metadataProvider);
|
StackManager.setMetadataProvider(metadataProvider);
|
||||||
|
|
||||||
const SOP_CLASSES = {
|
const SOP_CLASSES = {
|
||||||
SEGMENTATION_STORAGE: "1.2.840.10008.5.1.4.1.1.66.4"
|
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||||
};
|
};
|
||||||
|
|
||||||
const specialCaseHandlers = {};
|
const specialCaseHandlers = {};
|
||||||
@@ -28,11 +28,11 @@ specialCaseHandlers[
|
|||||||
|
|
||||||
class OHIFCornerstoneViewport extends Component {
|
class OHIFCornerstoneViewport extends Component {
|
||||||
state = {
|
state = {
|
||||||
viewportData: null
|
viewportData: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
customProps: {}
|
customProps: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -40,17 +40,17 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
displaySet: PropTypes.object,
|
displaySet: PropTypes.object,
|
||||||
viewportIndex: PropTypes.number,
|
viewportIndex: PropTypes.number,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
customProps: PropTypes.object
|
customProps: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = "OHIFCornerstoneViewport";
|
static id = 'OHIFCornerstoneViewport';
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
console.log("OHIFCornerstoneViewport init()");
|
console.log('OHIFCornerstoneViewport init()');
|
||||||
}
|
}
|
||||||
|
|
||||||
static destroy() {
|
static destroy() {
|
||||||
console.log("OHIFCornerstoneViewport destroy()");
|
console.log('OHIFCornerstoneViewport destroy()');
|
||||||
StackManager.clearStacks();
|
StackManager.clearStacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,15 +72,15 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
frameIndex = 0
|
frameIndex = 0
|
||||||
) {
|
) {
|
||||||
if (!studies || !studies.length) {
|
if (!studies || !studies.length) {
|
||||||
throw new Error("Studies not provided.");
|
throw new Error('Studies not provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!studyInstanceUid) {
|
if (!studyInstanceUid) {
|
||||||
throw new Error("StudyInstanceUID not provided.");
|
throw new Error('StudyInstanceUID not provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!displaySetInstanceUid) {
|
if (!displaySetInstanceUid) {
|
||||||
throw new Error("StudyInstanceUID not provided.");
|
throw new Error('StudyInstanceUID not provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create shortcut to displaySet
|
// Create shortcut to displaySet
|
||||||
@@ -89,7 +89,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!study) {
|
if (!study) {
|
||||||
throw new Error("Study not found.");
|
throw new Error('Study not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySet = study.displaySets.find(set => {
|
const displaySet = study.displaySets.find(set => {
|
||||||
@@ -97,7 +97,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!displaySet) {
|
if (!displaySet) {
|
||||||
throw new Error("Display Set not found.");
|
throw new Error('Display Set not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get stack from Stack Manager
|
// Get stack from Stack Manager
|
||||||
@@ -110,7 +110,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
if (sopInstanceUid) {
|
if (sopInstanceUid) {
|
||||||
const index = stack.imageIds.findIndex(imageId => {
|
const index = stack.imageIds.findIndex(imageId => {
|
||||||
const sopCommonModule = cornerstone.metaData.get(
|
const sopCommonModule = cornerstone.metaData.get(
|
||||||
"sopCommonModule",
|
'sopCommonModule',
|
||||||
imageId
|
imageId
|
||||||
);
|
);
|
||||||
if (!sopCommonModule) {
|
if (!sopCommonModule) {
|
||||||
@@ -124,7 +124,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
stack.currentImageIdIndex = index;
|
stack.currentImageIdIndex = index;
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
"SOPInstanceUID provided was not found in specified DisplaySet"
|
'SOPInstanceUID provided was not found in specified DisplaySet'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,22 +132,6 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getViewportData = (
|
|
||||||
studies,
|
|
||||||
studyInstanceUid,
|
|
||||||
displaySetInstanceUid,
|
|
||||||
sopInstanceUid,
|
|
||||||
frameIndex
|
|
||||||
) => {
|
|
||||||
return OHIFCornerstoneViewport.getCornerstoneStack(
|
|
||||||
studies,
|
|
||||||
studyInstanceUid,
|
|
||||||
displaySetInstanceUid,
|
|
||||||
sopInstanceUid,
|
|
||||||
frameIndex
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getViewportData = async (
|
getViewportData = async (
|
||||||
studies,
|
studies,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
@@ -172,7 +156,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
const stack = OHIFCornerstoneViewport.getViewportData(
|
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
|
||||||
studies,
|
studies,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
@@ -183,7 +167,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
viewportData = {
|
viewportData = {
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
stack
|
stack,
|
||||||
};
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -199,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
sopInstanceUid,
|
sopInstanceUid,
|
||||||
frameIndex
|
frameIndex,
|
||||||
} = displaySet;
|
} = displaySet;
|
||||||
|
|
||||||
if (!studyInstanceUid || !displaySetInstanceUid) {
|
if (!studyInstanceUid || !displaySetInstanceUid) {
|
||||||
@@ -208,7 +192,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
|
|
||||||
if (sopClassUids && sopClassUids.length > 1) {
|
if (sopClassUids && sopClassUids.length > 1) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"More than one SOPClassUid in the same series is not yet supported."
|
'More than one SOPClassUid in the same series is not yet supported.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +207,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
frameIndex
|
frameIndex
|
||||||
).then(viewportData => {
|
).then(viewportData => {
|
||||||
this.setState({
|
this.setState({
|
||||||
viewportData
|
viewportData,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -249,25 +233,42 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
render() {
|
render() {
|
||||||
let childrenWithProps = null;
|
let childrenWithProps = null;
|
||||||
|
|
||||||
|
if (!this.state.viewportData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { viewportIndex } = this.props;
|
||||||
|
const {
|
||||||
|
imageIds,
|
||||||
|
currentImageIdIndex,
|
||||||
|
// If this comes from the instance, would be a better default
|
||||||
|
// `FrameTime` in the instance
|
||||||
|
// frameRate = 0,
|
||||||
|
} = this.state.viewportData.stack;
|
||||||
|
|
||||||
// TODO: Does it make more sense to use Context?
|
// TODO: Does it make more sense to use Context?
|
||||||
if (this.props.children && this.props.children.length) {
|
if (this.props.children && this.props.children.length) {
|
||||||
childrenWithProps = this.props.children.map((child, index) => {
|
childrenWithProps = this.props.children.map((child, index) => {
|
||||||
return React.cloneElement(child, {
|
return React.cloneElement(child, {
|
||||||
viewportIndex: this.props.viewportIndex,
|
viewportIndex: this.props.viewportIndex,
|
||||||
key: index
|
key: index,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.state.viewportData && (
|
<ConnectedCornerstoneViewport
|
||||||
<ConnectedCornerstoneViewport
|
viewportIndex={viewportIndex}
|
||||||
viewportData={this.state.viewportData}
|
imageIds={imageIds}
|
||||||
viewportIndex={this.props.viewportIndex}
|
imageIdIndex={currentImageIdIndex}
|
||||||
{...this.props.customProps}
|
// ~~ Connected (From REDUX)
|
||||||
/>
|
// frameRate={frameRate}
|
||||||
)}
|
// isPlaying={false}
|
||||||
|
// isStackPrefetchEnabled={true}
|
||||||
|
// onElementEnabled={() => {}}
|
||||||
|
// setViewportActive{() => {}}
|
||||||
|
{...this.props.customProps}
|
||||||
|
/>
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* We use this component to leverage "Code Splitting"
|
||||||
|
*
|
||||||
|
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default function asyncComponent(importComponent) {
|
||||||
|
class AsyncComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
component: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
// Add dynamically loaded component to state
|
||||||
|
const { default: component } = await importComponent();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
component: component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const C = this.state.component;
|
||||||
|
|
||||||
|
// Render the loaded component, or null
|
||||||
|
return C ? <C {...this.props} /> : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AsyncComponent;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import OHIF from '@ohif/core';
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager, DicomLoaderService } = OHIF.utils;
|
||||||
|
|
||||||
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||||
const study = studies.find(
|
const study = studies.find(
|
||||||
@@ -52,14 +52,6 @@ function addSegMetadataToCornerstoneToolState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retrieveDicomData(wadoUri) {
|
|
||||||
// TODO: Authorization header depends on the server. If we ever have multiple servers
|
|
||||||
// we will need to figure out how / when to pass this information in.
|
|
||||||
return fetch(wadoUri, {
|
|
||||||
headers: OHIF.DICOMWeb.getAuthorizationHeader(),
|
|
||||||
}).then(response => response.arrayBuffer());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSegmentationStorage(
|
async function handleSegmentationStorage(
|
||||||
studies,
|
studies,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
@@ -73,8 +65,11 @@ async function handleSegmentationStorage(
|
|||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
displaySetInstanceUid
|
displaySetInstanceUid
|
||||||
);
|
);
|
||||||
const segWadoUri = displaySet.images[0].getData().wadouri;
|
|
||||||
const arrayBuffer = await retrieveDicomData(segWadoUri);
|
const arrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||||
|
displaySet,
|
||||||
|
studies
|
||||||
|
);
|
||||||
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
||||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||||
dicomData.dict
|
dicomData.dict
|
||||||
@@ -120,6 +115,7 @@ async function handleSegmentationStorage(
|
|||||||
referenceDisplaySet
|
referenceDisplaySet
|
||||||
);
|
);
|
||||||
const stack = Object.assign({}, cachedStack);
|
const stack = Object.assign({}, cachedStack);
|
||||||
|
|
||||||
stack.currentImageIdIndex = 0;
|
stack.currentImageIdIndex = 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import OHIFCornerstoneViewport from './OHIFCornerstoneViewport.js';
|
import init from './init.js';
|
||||||
|
import asyncComponent from './asyncComponent.js';
|
||||||
import commandsModule from './commandsModule.js';
|
import commandsModule from './commandsModule.js';
|
||||||
import toolbarModule from './toolbarModule.js';
|
import toolbarModule from './toolbarModule.js';
|
||||||
|
|
||||||
|
const OHIFCornerstoneViewport = asyncComponent(() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +18,15 @@ export default {
|
|||||||
*/
|
*/
|
||||||
id: 'cornerstone',
|
id: 'cornerstone',
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {object} [configuration={}]
|
||||||
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
|
*/
|
||||||
|
preRegistration(configuration = {}) {
|
||||||
|
init(configuration);
|
||||||
|
},
|
||||||
getViewportModule() {
|
getViewportModule() {
|
||||||
return OHIFCornerstoneViewport;
|
return OHIFCornerstoneViewport;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import csTools from 'cornerstone-tools';
|
||||||
|
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} configuration
|
||||||
|
* @param {Object|Array} configuration.csToolsConfig
|
||||||
|
*/
|
||||||
|
export default function init(configuration = {}) {
|
||||||
|
const { csToolsConfig } = configuration;
|
||||||
|
const { StackManager } = OHIF.utils;
|
||||||
|
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||||
|
|
||||||
|
// ~~ Set our MetadataProvider
|
||||||
|
cornerstone.metaData.addProvider(
|
||||||
|
metadataProvider.provider.bind(metadataProvider)
|
||||||
|
);
|
||||||
|
|
||||||
|
StackManager.setMetadataProvider(metadataProvider);
|
||||||
|
|
||||||
|
// ~~
|
||||||
|
const defaultCsToolsConfig = csToolsConfig || {
|
||||||
|
globalToolSyncEnabled: true,
|
||||||
|
showSVGCursors: true,
|
||||||
|
autoResizeViewports: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
initCornerstoneTools(defaultCsToolsConfig);
|
||||||
|
|
||||||
|
// ~~ Toooools 🙌
|
||||||
|
const {
|
||||||
|
PanTool,
|
||||||
|
ZoomTool,
|
||||||
|
WwwcTool,
|
||||||
|
MagnifyTool,
|
||||||
|
StackScrollTool,
|
||||||
|
StackScrollMouseWheelTool,
|
||||||
|
// Touch
|
||||||
|
PanMultiTouchTool,
|
||||||
|
ZoomTouchPinchTool,
|
||||||
|
// Annotations
|
||||||
|
EraserTool,
|
||||||
|
ArrowAnnotateTool,
|
||||||
|
BidirectionalTool,
|
||||||
|
LengthTool,
|
||||||
|
AngleTool,
|
||||||
|
FreehandRoiTool,
|
||||||
|
EllipticalRoiTool,
|
||||||
|
DragProbeTool,
|
||||||
|
RectangleRoiTool,
|
||||||
|
// Segmentation
|
||||||
|
BrushTool,
|
||||||
|
} = csTools;
|
||||||
|
const tools = [
|
||||||
|
PanTool,
|
||||||
|
ZoomTool,
|
||||||
|
WwwcTool,
|
||||||
|
MagnifyTool,
|
||||||
|
StackScrollTool,
|
||||||
|
StackScrollMouseWheelTool,
|
||||||
|
// Touch
|
||||||
|
PanMultiTouchTool,
|
||||||
|
ZoomTouchPinchTool,
|
||||||
|
// Annotations
|
||||||
|
EraserTool,
|
||||||
|
ArrowAnnotateTool,
|
||||||
|
BidirectionalTool,
|
||||||
|
LengthTool,
|
||||||
|
AngleTool,
|
||||||
|
FreehandRoiTool,
|
||||||
|
EllipticalRoiTool,
|
||||||
|
DragProbeTool,
|
||||||
|
RectangleRoiTool,
|
||||||
|
// Segmentation
|
||||||
|
BrushTool,
|
||||||
|
];
|
||||||
|
|
||||||
|
tools.forEach(tool => csTools.addTool(tool));
|
||||||
|
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
|
||||||
|
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||||
|
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
||||||
|
csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required
|
||||||
|
csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
|
||||||
|
csTools.setToolActive('ZoomTouchPinch', {});
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -69,6 +69,15 @@ const definitions = [
|
|||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: { toolName: 'Length' },
|
commandOptions: { toolName: 'Length' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'ArrowAnnotate',
|
||||||
|
label: 'Annotate',
|
||||||
|
icon: 'measure-non-target',
|
||||||
|
//
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'ArrowAnnotate' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'Angle',
|
id: 'Angle',
|
||||||
label: 'Angle',
|
label: 'Angle',
|
||||||
@@ -191,7 +200,7 @@ const definitions = [
|
|||||||
label: 'Bidirectional',
|
label: 'Bidirectional',
|
||||||
icon: 'measure-target',
|
icon: 'measure-target',
|
||||||
//
|
//
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: { toolName: 'Bidirectional' },
|
commandOptions: { toolName: 'Bidirectional' },
|
||||||
},
|
},
|
||||||
@@ -200,7 +209,7 @@ const definitions = [
|
|||||||
label: 'Eraser',
|
label: 'Eraser',
|
||||||
icon: 'eraser',
|
icon: 'eraser',
|
||||||
//
|
//
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: { toolName: 'Eraser' },
|
commandOptions: { toolName: 'Eraser' },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
@@ -34,5 +35,10 @@ module.exports = (env, argv) => {
|
|||||||
libraryExport: 'default',
|
libraryExport: 'default',
|
||||||
filename: pkg.main,
|
filename: pkg.main,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,41 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.8...@ohif/extension-dicom-html@1.0.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* DICOM Seg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.7...@ohif/extension-dicom-html@0.50.8) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.6...@ohif/extension-dicom-html@0.50.7) (2019-09-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.5...@ohif/extension-dicom-html@0.50.6) (2019-09-10)
|
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.5...@ohif/extension-dicom-html@0.50.6) (2019-09-10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-html",
|
"name": "@ohif/extension-dicom-html",
|
||||||
"version": "0.50.6",
|
"version": "1.0.0",
|
||||||
"description": "OHIF extension for rendering structured reports to HTML",
|
"description": "OHIF extension for rendering structured reports to HTML",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"dcmjs": "^0.6.0",
|
"dcmjs": "^0.6.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@@ -1,46 +1,34 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import DicomHtmlViewport from './DicomHtmlViewport';
|
import DicomHtmlViewport from './DicomHtmlViewport';
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
|
const { DicomLoaderService } = OHIF.utils;
|
||||||
|
|
||||||
class OHIFDicomHtmlViewport extends Component {
|
class OHIFDicomHtmlViewport extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
studies: PropTypes.object,
|
studies: PropTypes.object,
|
||||||
displaySet: PropTypes.object,
|
displaySet: PropTypes.object,
|
||||||
viewportIndex: PropTypes.number
|
viewportIndex: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
byteArray: null,
|
byteArray: null,
|
||||||
error: null
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { displaySet } = this.props.viewportData;
|
const { displaySet, studies } = this.props.viewportData;
|
||||||
const {
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid,
|
|
||||||
wadoRoot,
|
|
||||||
wadoUri,
|
|
||||||
authorizationHeaders
|
|
||||||
} = displaySet;
|
|
||||||
|
|
||||||
this.retrieveDicomData(
|
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
|
||||||
studyInstanceUid,
|
data => {
|
||||||
seriesInstanceUid,
|
const byteArray = new Uint8Array(data);
|
||||||
sopInstanceUid,
|
|
||||||
wadoRoot,
|
|
||||||
wadoUri,
|
|
||||||
authorizationHeaders
|
|
||||||
).then(
|
|
||||||
byteArray => {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
byteArray
|
byteArray: byteArray,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.setState({
|
this.setState({
|
||||||
error
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
@@ -48,27 +36,6 @@ class OHIFDicomHtmlViewport extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieveDicomData(
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid,
|
|
||||||
wadoRoot,
|
|
||||||
wadoUri,
|
|
||||||
authorizationHeaders
|
|
||||||
) {
|
|
||||||
// TODO: Passing in a lot of data we aren't using
|
|
||||||
|
|
||||||
// TODO: Authorization header depends on the server. If we ever have multiple servers
|
|
||||||
// we will need to figure out how / when to pass this information in.
|
|
||||||
return fetch(wadoUri, {
|
|
||||||
headers: authorizationHeaders
|
|
||||||
})
|
|
||||||
.then(response => response.arrayBuffer())
|
|
||||||
.then(arraybuffer => {
|
|
||||||
return new Uint8Array(arraybuffer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* We use this component to leverage "Code Splitting"
|
||||||
|
*
|
||||||
|
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default function asyncComponent(importComponent) {
|
||||||
|
class AsyncComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
component: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
// Add dynamically loaded component to state
|
||||||
|
const { default: component } = await importComponent();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
component: component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const C = this.state.component;
|
||||||
|
|
||||||
|
// Render the loaded component, or null
|
||||||
|
return C ? <C {...this.props} /> : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AsyncComponent;
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import asyncComponent from './asyncComponent.js';
|
||||||
import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js';
|
import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js';
|
||||||
import OHIFDicomHtmlViewport from './OHIFDicomHtmlViewport.js';
|
|
||||||
|
const OHIFDicomHtmlViewport = asyncComponent(() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "OHIFDicomHtmlViewport" */ './OHIFDicomHtmlViewport.js'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -12,5 +18,5 @@ export default {
|
|||||||
},
|
},
|
||||||
getSopClassHandlerModule() {
|
getSopClassHandlerModule() {
|
||||||
return OHIFDicomHtmlSopClassHandler;
|
return OHIFDicomHtmlSopClassHandler;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
@@ -34,5 +35,10 @@ module.exports = (env, argv) => {
|
|||||||
libraryExport: 'default',
|
libraryExport: 'default',
|
||||||
filename: pkg.main,
|
filename: pkg.main,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1
|
||||||
|
})
|
||||||
|
]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.5...@ohif/extension-dicom-microscopy@0.50.6) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.4...@ohif/extension-dicom-microscopy@0.50.5) (2019-09-10)
|
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.4...@ohif/extension-dicom-microscopy@0.50.5) (2019-09-10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-microscopy",
|
"name": "@ohif/extension-dicom-microscopy",
|
||||||
"version": "0.50.5",
|
"version": "0.50.6",
|
||||||
"description": "OHIF extension for Dicom Microscopy",
|
"description": "OHIF extension for Dicom Microscopy",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
import { api } from 'dicom-microscopy-viewer';
|
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
|
||||||
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
|
|
||||||
|
|
||||||
class DicomMicroscopyViewport extends Component {
|
class DicomMicroscopyViewport extends Component {
|
||||||
state = {
|
state = {
|
||||||
error: null,
|
error: null,
|
||||||
@@ -56,9 +53,12 @@ class DicomMicroscopyViewport extends Component {
|
|||||||
}
|
}
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
})
|
})
|
||||||
.then(metadata => {
|
.then(async metadata => {
|
||||||
metadata = metadata.filter(m => m);
|
metadata = metadata.filter(m => m);
|
||||||
|
|
||||||
|
const { api } = await import(/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer');
|
||||||
|
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
|
||||||
|
|
||||||
this.viewer = new microscopyViewer({
|
this.viewer = new microscopyViewer({
|
||||||
client: dicomWebClient,
|
client: dicomWebClient,
|
||||||
metadata,
|
metadata,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* We use this component to leverage "Code Splitting"
|
||||||
|
*
|
||||||
|
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default function asyncComponent(importComponent) {
|
||||||
|
class AsyncComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
component: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
// Add dynamically loaded component to state
|
||||||
|
const { default: component } = await importComponent();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
component: component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const C = this.state.component;
|
||||||
|
|
||||||
|
// Render the loaded component, or null
|
||||||
|
return C ? <C {...this.props} /> : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AsyncComponent;
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import asyncComponent from './asyncComponent.js';
|
||||||
import DicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler.js';
|
import DicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler.js';
|
||||||
import DicomMicroscopyViewport from './DicomMicroscopyViewport.js';
|
|
||||||
|
const DicomMicroscopyViewport = asyncComponent(() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "DicomMicroscopyViewport" */ './DicomMicroscopyViewport.js'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,22 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.4...@ohif/extension-dicom-pdf@1.0.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* DICOM Seg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.3...@ohif/extension-dicom-pdf@0.50.4) (2019-09-10)
|
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.3...@ohif/extension-dicom-pdf@0.50.4) (2019-09-10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-pdf",
|
"name": "@ohif/extension-dicom-pdf",
|
||||||
"version": "0.50.4",
|
"version": "1.0.0",
|
||||||
"description": "OHIF extension for Dicom PDF",
|
"description": "OHIF extension for Dicom PDF",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -4,41 +4,46 @@ import TypedArrayProp from './TypedArrayProp';
|
|||||||
|
|
||||||
// TODO: Should probably use dcmjs for this
|
// TODO: Should probably use dcmjs for this
|
||||||
const SOP_CLASS_UIDS = {
|
const SOP_CLASS_UIDS = {
|
||||||
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1'
|
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1',
|
||||||
};
|
};
|
||||||
|
|
||||||
class DicomPDFViewport extends Component {
|
class DicomPDFViewport extends Component {
|
||||||
state = {
|
state = {
|
||||||
fileURL: null,
|
fileURL: null,
|
||||||
error: null
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
byteArray: TypedArrayProp.uint8
|
byteArray: TypedArrayProp.uint8,
|
||||||
};
|
};
|
||||||
|
|
||||||
renderPDF = dataSet => {
|
renderPDF = (dataSet, byteArray) => {
|
||||||
const sopClassUid = dataSet.string('x00080016');
|
let pdfByteArray = byteArray;
|
||||||
|
|
||||||
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
|
if (dataSet) {
|
||||||
throw new Error('This is not a DICOM-encapsulated PDF');
|
const sopClassUid = dataSet.string('x00080016');
|
||||||
|
|
||||||
|
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
|
||||||
|
throw new Error('This is not a DICOM-encapsulated PDF');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileTag = dataSet.elements.x00420011;
|
||||||
|
const offset = fileTag.dataOffset;
|
||||||
|
const remainder = offset + fileTag.length;
|
||||||
|
pdfByteArray = dataSet.byteArray.slice(offset, remainder);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileTag = dataSet.elements.x00420011;
|
|
||||||
const offset = fileTag.dataOffset;
|
|
||||||
const remainder = offset + fileTag.length;
|
|
||||||
const pdfByteArray = dataSet.byteArray.slice(offset, remainder);
|
|
||||||
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
|
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
|
||||||
const fileURL = URL.createObjectURL(PDF);
|
const fileURL = URL.createObjectURL(PDF);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
fileURL
|
fileURL,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
parseByteArray = byteArray => {
|
parseByteArray = byteArray => {
|
||||||
const options = {
|
const options = {
|
||||||
untilTag: ''
|
untilTag: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
let dataSet;
|
let dataSet;
|
||||||
@@ -47,7 +52,7 @@ class DicomPDFViewport extends Component {
|
|||||||
dataSet = dicomParser.parseDicom(byteArray, options);
|
dataSet = dicomParser.parseDicom(byteArray, options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({
|
this.setState({
|
||||||
error
|
error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +62,7 @@ class DicomPDFViewport extends Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const dataSet = this.parseByteArray(this.props.byteArray);
|
const dataSet = this.parseByteArray(this.props.byteArray);
|
||||||
|
|
||||||
this.renderPDF(dataSet);
|
this.renderPDF(dataSet, this.props.byteArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,60 +1,54 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import OHIF from "@ohif/core";
|
import OHIF from '@ohif/core';
|
||||||
import OHIFComponentPlugin from "./OHIFComponentPlugin.js";
|
import OHIFComponentPlugin from './OHIFComponentPlugin.js';
|
||||||
import DicomPDFViewport from "./DicomPDFViewport";
|
import DicomPDFViewport from './DicomPDFViewport';
|
||||||
|
|
||||||
const { DICOMWeb } = OHIF;
|
const { DicomLoaderService } = OHIF.utils;
|
||||||
|
|
||||||
class OHIFDicomPDFViewport extends Component {
|
class OHIFDicomPDFViewport extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
studies: PropTypes.object,
|
studies: PropTypes.object,
|
||||||
displaySet: PropTypes.object,
|
displaySet: PropTypes.object,
|
||||||
viewportIndex: PropTypes.number
|
viewportIndex: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
byteArray: null,
|
byteArray: null,
|
||||||
error: null
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = "DicomPDFViewportPDF";
|
static id = 'DicomPDFViewportPDF';
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
console.log("DicomPDFViewport init()");
|
console.log('DicomPDFViewport init()');
|
||||||
}
|
}
|
||||||
|
|
||||||
static destroy() {
|
static destroy() {
|
||||||
console.log("DicomPDFViewport destroy()");
|
console.log('DicomPDFViewport destroy()');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { displaySet } = this.props.viewportData;
|
const { displaySet, studies } = this.props.viewportData;
|
||||||
const {
|
const {
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
seriesInstanceUid,
|
seriesInstanceUid,
|
||||||
sopInstanceUid,
|
sopInstanceUid,
|
||||||
wadoRoot,
|
wadoRoot,
|
||||||
wadoUri,
|
wadoUri,
|
||||||
authorizationHeaders
|
authorizationHeaders,
|
||||||
} = displaySet;
|
} = displaySet;
|
||||||
|
|
||||||
this.retrieveDicomData(
|
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
|
||||||
studyInstanceUid,
|
data => {
|
||||||
seriesInstanceUid,
|
const byteArray = new Uint8Array(data);
|
||||||
sopInstanceUid,
|
|
||||||
wadoRoot,
|
|
||||||
wadoUri,
|
|
||||||
authorizationHeaders
|
|
||||||
).then(
|
|
||||||
byteArray => {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
byteArray
|
byteArray: byteArray,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.setState({
|
this.setState({
|
||||||
error
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
@@ -62,27 +56,6 @@ class OHIFDicomPDFViewport extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieveDicomData(
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid,
|
|
||||||
wadoRoot,
|
|
||||||
wadoUri,
|
|
||||||
authorizationHeaders
|
|
||||||
) {
|
|
||||||
// TODO: Passing in a lot of data we aren't using
|
|
||||||
|
|
||||||
// TODO: Authorization header depends on the server. If we ever have multiple servers
|
|
||||||
// we will need to figure out how / when to pass this information in.
|
|
||||||
return fetch(wadoUri, {
|
|
||||||
headers: authorizationHeaders
|
|
||||||
})
|
|
||||||
.then(response => response.arrayBuffer())
|
|
||||||
.then(arraybuffer => {
|
|
||||||
return new Uint8Array(arraybuffer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { id, init, destroy } = OHIFDicomPDFViewport;
|
const { id, init, destroy } = OHIFDicomPDFViewport;
|
||||||
const pluginProps = { id, init, destroy };
|
const pluginProps = { id, init, destroy };
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
@@ -34,5 +35,10 @@ module.exports = (env, argv) => {
|
|||||||
libraryExport: 'default',
|
libraryExport: 'default',
|
||||||
filename: pkg.main,
|
filename: pkg.main,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,334 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.52.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.19...@ohif/extension-vtk@0.52.20) (2019-10-26)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.18...@ohif/extension-vtk@0.52.19) (2019-10-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.17...@ohif/extension-vtk@0.52.18) (2019-10-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.16...@ohif/extension-vtk@0.52.17) (2019-10-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix crosshairs location when using MIP ([#1080](https://github.com/OHIF/Viewers/issues/1080)) ([b451ce4](https://github.com/OHIF/Viewers/commit/b451ce440796f13b0891739d2130e8ee71993d3d))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.15...@ohif/extension-vtk@0.52.16) (2019-10-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Switch to orhtographic view for 2D MPR ([#1074](https://github.com/OHIF/Viewers/issues/1074)) ([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.14...@ohif/extension-vtk@0.52.15) (2019-10-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* ⚡️ Throttle UI refresh of synced WL value in vtk port ([#1070](https://github.com/OHIF/Viewers/issues/1070)) ([9d08e81](https://github.com/OHIF/Viewers/commit/9d08e81aa5def63a5e34f464aae099f7c29f0ac5))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.13...@ohif/extension-vtk@0.52.14) (2019-10-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.12...@ohif/extension-vtk@0.52.13) (2019-10-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.11...@ohif/extension-vtk@0.52.12) (2019-10-18)
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.4...@ohif/extension-vtk@0.52.5) (2019-10-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* ⚡️ Update react-vtkjs-viewport for performance improvement ([6ee5094](https://github.com/OHIF/Viewers/commit/6ee5094))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.3...@ohif/extension-vtk@0.52.4) (2019-10-10)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.2...@ohif/extension-vtk@0.52.3) (2019-10-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.1...@ohif/extension-vtk@0.52.2) (2019-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* 🎸 Update vtk viewport for faster image reconstruction ([#1016](https://github.com/OHIF/Viewers/issues/1016)) ([8513ad9](https://github.com/OHIF/Viewers/commit/8513ad9))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.52.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.0...@ohif/extension-vtk@0.52.1) (2019-10-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.3...@ohif/extension-vtk@0.52.0) (2019-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Synced Window Leveling ([559b637](https://github.com/OHIF/Viewers/commit/559b637)), closes [#558](https://github.com/OHIF/Viewers/issues/558)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.2...@ohif/extension-vtk@0.51.3) (2019-10-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.1...@ohif/extension-vtk@0.51.2) (2019-10-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.51.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.0...@ohif/extension-vtk@0.51.1) (2019-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **rotate:** Use new MPR rotate in react-vtkjs-viewport 0.1.2 ([#964](https://github.com/OHIF/Viewers/issues/964)) ([8503cbc](https://github.com/OHIF/Viewers/commit/8503cbc))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.23...@ohif/extension-vtk@0.51.0) (2019-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.22...@ohif/extension-vtk@0.50.23) (2019-10-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.21...@ohif/extension-vtk@0.50.22) (2019-09-27)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.20...@ohif/extension-vtk@0.50.21) (2019-09-27)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.19...@ohif/extension-vtk@0.50.20) (2019-09-26)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.18...@ohif/extension-vtk@0.50.19) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.17...@ohif/extension-vtk@0.50.18) (2019-09-23)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.16...@ohif/extension-vtk@0.50.17) (2019-09-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.15...@ohif/extension-vtk@0.50.16) (2019-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Set transfer function range to data range when loading volumes in VTK ([#930](https://github.com/OHIF/Viewers/issues/930)) ([2341b32](https://github.com/OHIF/Viewers/commit/2341b32))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.14...@ohif/extension-vtk@0.50.15) (2019-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Bump VTK viewport dependency ([#929](https://github.com/OHIF/Viewers/issues/929)) ([66a0aad](https://github.com/OHIF/Viewers/commit/66a0aad))
|
||||||
|
* Stop adding empty labelmap to VTK Viewports for now. Wait until paint tools are implemented ([#926](https://github.com/OHIF/Viewers/issues/926)) ([7f642a0](https://github.com/OHIF/Viewers/commit/7f642a0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.13...@ohif/extension-vtk@0.50.14) (2019-09-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.12...@ohif/extension-vtk@0.50.13) (2019-09-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.11...@ohif/extension-vtk@0.50.12) (2019-09-12)
|
## [0.50.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.11...@ohif/extension-vtk@0.50.12) (2019-09-12)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-vtk
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-vtk",
|
"name": "@ohif/extension-vtk",
|
||||||
"version": "0.50.12",
|
"version": "0.52.20",
|
||||||
"description": "OHIF extension for VTK.js",
|
"description": "OHIF extension for VTK.js",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.0",
|
"dcmjs": "^0.6.1",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
@@ -48,14 +48,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"react-vtkjs-viewport": "0.0.11"
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"react-vtkjs-viewport": "^0.3.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^0.50.8",
|
"@ohif/core": "^1.5.2",
|
||||||
"@ohif/ui": "^0.51.0",
|
"@ohif/ui": "^0.56.1",
|
||||||
"cornerstone-tools": "^4.0.0",
|
"cornerstone-tools": "^4.0.9",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.0",
|
"dcmjs": "^0.6.1",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"gh-pages": "^2.0.1",
|
"gh-pages": "^2.0.1",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import OHIF from "@ohif/core";
|
import OHIF from '@ohif/core';
|
||||||
import { View2D } from "react-vtkjs-viewport";
|
import { View2D } from 'react-vtkjs-viewport';
|
||||||
import { connect } from "react-redux";
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
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
|
// Hopefully this doesn't break anything under the hood for this one
|
||||||
// activeTool: activeButton && activeButton.command,
|
// activeTool: activeButton && activeButton.command,
|
||||||
...dataFromStore,
|
...dataFromStore,
|
||||||
enableStackPrefetch: isActive
|
enableStackPrefetch: isActive,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
|||||||
|
|
||||||
setViewportSpecificData: data => {
|
setViewportSpecificData: data => {
|
||||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
dispatch(setViewportSpecificData(viewportIndex, data));
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,10 +63,10 @@ const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
|||||||
// Store the API details for later
|
// Store the API details for later
|
||||||
//setViewportSpecificData({ vtkApi: api });
|
//setViewportSpecificData({ vtkApi: api });
|
||||||
|
|
||||||
if (afterCreation && typeof afterCreation === "function") {
|
if (afterCreation && typeof afterCreation === 'function') {
|
||||||
afterCreation(api);
|
afterCreation(api);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
return props;
|
return props;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loadingIndicator {
|
.loadingIndicator {
|
||||||
background-color: rgba(0, 0, 0, 0.75);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
font-size: 18px;
|
font-size: 8px;
|
||||||
height: 100%;
|
height: 20%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
/* Necessary for click-through to cornerstone element below */
|
/* Necessary for click-through to vtkjs element below */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import './LoadingIndicator.css';
|
import './LoadingIndicator.css';
|
||||||
|
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
class LoadingIndicator extends PureComponent {
|
class LoadingIndicator extends PureComponent {
|
||||||
@@ -35,7 +35,8 @@ class LoadingIndicator extends PureComponent {
|
|||||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||||
<div className="indicatorContents">
|
<div className="indicatorContents">
|
||||||
<p>
|
<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}
|
{percComplete}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,4 +47,4 @@ class LoadingIndicator extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoadingIndicator;
|
export default withTranslation('Common')(LoadingIndicator);
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from 'react';
|
||||||
import { getImageData, loadImageData } from "react-vtkjs-viewport";
|
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||||
|
|
||||||
import ConnectedVTKViewport from "./ConnectedVTKViewport";
|
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||||
import LoadingIndicator from "./LoadingIndicator.js";
|
import LoadingIndicator from './LoadingIndicator.js';
|
||||||
import OHIF from "@ohif/core";
|
import OHIF from '@ohif/core';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import cornerstone from "cornerstone-core";
|
import cornerstone from 'cornerstone-core';
|
||||||
import handleSegmentationStorage from "./handleSegmentationStorage.js";
|
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||||
import vtkDataArray from "vtk.js/Sources/Common/Core/DataArray";
|
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
|
||||||
import vtkImageData from "vtk.js/Sources/Common/DataModel/ImageData";
|
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
|
||||||
import vtkVolume from "vtk.js/Sources/Rendering/Core/Volume";
|
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
|
||||||
import vtkVolumeMapper from "vtk.js/Sources/Rendering/Core/VolumeMapper";
|
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager } = OHIF.utils;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ cornerstone.metaData.addProvider(
|
|||||||
StackManager.setMetadataProvider(metadataProvider);
|
StackManager.setMetadataProvider(metadataProvider);
|
||||||
|
|
||||||
const SOP_CLASSES = {
|
const SOP_CLASSES = {
|
||||||
SEGMENTATION_STORAGE: "1.2.840.10008.5.1.4.1.1.66.4"
|
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||||
};
|
};
|
||||||
|
|
||||||
const specialCaseHandlers = {};
|
const specialCaseHandlers = {};
|
||||||
@@ -42,7 +42,7 @@ const volumeCache = {};
|
|||||||
*/
|
*/
|
||||||
function createLabelMapImageData(backgroundImageData) {
|
function createLabelMapImageData(backgroundImageData) {
|
||||||
const labelMapData = vtkImageData.newInstance(
|
const labelMapData = vtkImageData.newInstance(
|
||||||
backgroundImageData.get("spacing", "origin", "direction")
|
backgroundImageData.get('spacing', 'origin', 'direction')
|
||||||
);
|
);
|
||||||
labelMapData.setDimensions(backgroundImageData.getDimensions());
|
labelMapData.setDimensions(backgroundImageData.getDimensions());
|
||||||
labelMapData.computeTransforms();
|
labelMapData.computeTransforms();
|
||||||
@@ -50,7 +50,7 @@ function createLabelMapImageData(backgroundImageData) {
|
|||||||
const values = new Uint8Array(backgroundImageData.getNumberOfPoints());
|
const values = new Uint8Array(backgroundImageData.getNumberOfPoints());
|
||||||
const dataArray = vtkDataArray.newInstance({
|
const dataArray = vtkDataArray.newInstance({
|
||||||
numberOfComponents: 1, // labelmap with single component
|
numberOfComponents: 1, // labelmap with single component
|
||||||
values
|
values,
|
||||||
});
|
});
|
||||||
labelMapData.getPointData().setScalars(dataArray);
|
labelMapData.getPointData().setScalars(dataArray);
|
||||||
|
|
||||||
@@ -61,24 +61,24 @@ class OHIFVTKViewport extends Component {
|
|||||||
state = {
|
state = {
|
||||||
volumes: null,
|
volumes: null,
|
||||||
paintFilterLabelMapImageData: null,
|
paintFilterLabelMapImageData: null,
|
||||||
paintFilterBackgroundImageData: null
|
paintFilterBackgroundImageData: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
studies: PropTypes.object,
|
studies: PropTypes.object,
|
||||||
displaySet: PropTypes.object,
|
displaySet: PropTypes.object,
|
||||||
viewportIndex: PropTypes.number,
|
viewportIndex: PropTypes.number,
|
||||||
children: PropTypes.node
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = "OHIFVTKViewport";
|
static id = 'OHIFVTKViewport';
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
console.log("OHIFVTKViewport init()");
|
console.log('OHIFVTKViewport init()');
|
||||||
}
|
}
|
||||||
|
|
||||||
static destroy() {
|
static destroy() {
|
||||||
console.log("OHIFVTKViewport destroy()");
|
console.log('OHIFVTKViewport destroy()');
|
||||||
StackManager.clearStacks();
|
StackManager.clearStacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
} else if (sopInstanceUid) {
|
} else if (sopInstanceUid) {
|
||||||
const index = stack.imageIds.findIndex(imageId => {
|
const index = stack.imageIds.findIndex(imageId => {
|
||||||
const sopCommonModule = cornerstone.metaData.get(
|
const sopCommonModule = cornerstone.metaData.get(
|
||||||
"sopCommonModule",
|
'sopCommonModule',
|
||||||
imageId
|
imageId
|
||||||
);
|
);
|
||||||
if (!sopCommonModule) {
|
if (!sopCommonModule) {
|
||||||
@@ -151,8 +151,8 @@ class OHIFVTKViewport extends Component {
|
|||||||
|
|
||||||
switch (sopClassUid) {
|
switch (sopClassUid) {
|
||||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||||
throw new Error("Not yet implemented");
|
throw new Error('Not yet implemented');
|
||||||
|
/*
|
||||||
const data = handleSegmentationStorage(
|
const data = handleSegmentationStorage(
|
||||||
stack.imageIds,
|
stack.imageIds,
|
||||||
displaySetInstanceUid
|
displaySetInstanceUid
|
||||||
@@ -164,67 +164,82 @@ class OHIFVTKViewport extends Component {
|
|||||||
return loadImageData(imageDataObject).then(() => {
|
return loadImageData(imageDataObject).then(() => {
|
||||||
return {
|
return {
|
||||||
data: imageDataObject.vtkImageData,
|
data: imageDataObject.vtkImageData,
|
||||||
labelmap: labelmapDataObject
|
labelmap: labelmapDataObject,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
default:
|
default:
|
||||||
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
|
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
|
||||||
|
|
||||||
return loadImageData(imageDataObject).then(() => {
|
return imageDataObject;
|
||||||
return {
|
|
||||||
data: imageDataObject.vtkImageData
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getOrCreateVolume(data, displaySetInstanceUid) {
|
getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
|
||||||
if (volumeCache[displaySetInstanceUid]) {
|
if (volumeCache[displaySetInstanceUid]) {
|
||||||
return 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 volumeActor = vtkVolume.newInstance();
|
||||||
const volumeMapper = vtkVolumeMapper.newInstance();
|
const volumeMapper = vtkVolumeMapper.newInstance();
|
||||||
|
|
||||||
volumeActor.setMapper(volumeMapper);
|
volumeActor.setMapper(volumeMapper);
|
||||||
volumeMapper.setInputData(data);
|
volumeMapper.setInputData(vtkImageData);
|
||||||
|
|
||||||
// TODO: Should look into implementing autoAdjustSampleDistance in vtk
|
volumeActor
|
||||||
const sampleDistance =
|
.getProperty()
|
||||||
1.2 *
|
.getRGBTransferFunction(0)
|
||||||
Math.sqrt(
|
.setRange(lower, upper);
|
||||||
data
|
|
||||||
.getSpacing()
|
const spacing = vtkImageData.getSpacing();
|
||||||
.map((v) => v * v)
|
// Set the sample distance to half the mean length of one side. This is where the divide by 6 comes from.
|
||||||
.reduce((a, b) => a + b, 0)
|
// https://github.com/Kitware/VTK/blob/6b559c65bb90614fb02eb6d1b9e3f0fca3fe4b0b/Rendering/VolumeOpenGL2/vtkSmartVolumeMapper.cxx#L344
|
||||||
);
|
const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6;
|
||||||
|
|
||||||
volumeMapper.setSampleDistance(sampleDistance);
|
volumeMapper.setSampleDistance(sampleDistance);
|
||||||
|
|
||||||
|
// Be generous to surpress warnings, as the logging really hurts performance.
|
||||||
|
// TODO: maybe we should auto adjust samples to 1000.
|
||||||
|
volumeMapper.setMaximumSamplesPerRay(4000);
|
||||||
|
|
||||||
volumeCache[displaySetInstanceUid] = volumeActor;
|
volumeCache[displaySetInstanceUid] = volumeActor;
|
||||||
|
|
||||||
return volumeActor;
|
return volumeActor;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setStateFromProps() {
|
setStateFromProps() {
|
||||||
const { studies, displaySet } = this.props.viewportData;
|
const { studies, displaySet } = this.props.viewportData;
|
||||||
const {
|
const {
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
sopInstanceUid,
|
sopInstanceUid,
|
||||||
frameIndex
|
frameIndex,
|
||||||
} = displaySet;
|
} = displaySet;
|
||||||
|
|
||||||
if (sopClassUids.length > 1) {
|
if (sopClassUids.length > 1) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"More than one SOPClassUid in the same series is not yet supported."
|
'More than one SOPClassUid in the same series is not yet supported.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sopClassUid = sopClassUids[0];
|
const sopClassUid = sopClassUids[0];
|
||||||
|
const imageDataObject = this.getViewportData(
|
||||||
let { data, labelmap } = await this.getViewportData(
|
|
||||||
studies,
|
studies,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
@@ -233,17 +248,45 @@ class OHIFVTKViewport extends Component {
|
|||||||
frameIndex
|
frameIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!labelmap) {
|
this.imageDataObject = imageDataObject;
|
||||||
labelmap = createLabelMapImageData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const volumeActor = this.getOrCreateVolume(data, displaySetInstanceUid);
|
// TODO: Temporarily disabling this since it is not yet
|
||||||
|
// being used and hurts performance significantly.
|
||||||
|
/*if (!labelmap) {
|
||||||
|
labelmap = createLabelMapImageData(data);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
const volumeActor = this.getOrCreateVolume(
|
||||||
|
imageDataObject,
|
||||||
|
displaySetInstanceUid
|
||||||
|
);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
volumes: [volumeActor],
|
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
|
||||||
paintFilterBackgroundImageData: data,
|
paintFilterLabelMapImageData: null, // TODO
|
||||||
paintFilterLabelMapImageData: labelmap
|
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() {
|
componentDidMount() {
|
||||||
@@ -264,6 +307,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() {
|
render() {
|
||||||
let childrenWithProps = null;
|
let childrenWithProps = null;
|
||||||
|
|
||||||
@@ -272,30 +346,32 @@ class OHIFVTKViewport extends Component {
|
|||||||
childrenWithProps = this.props.children.map((child, index) => {
|
childrenWithProps = this.props.children.map((child, index) => {
|
||||||
return React.cloneElement(child, {
|
return React.cloneElement(child, {
|
||||||
viewportIndex: this.props.viewportIndex,
|
viewportIndex: this.props.viewportIndex,
|
||||||
key: index
|
key: index,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = { width: "100%", height: "100%", position: "relative" };
|
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.state.volumes ? (
|
<div style={style}>
|
||||||
<ConnectedVTKViewport
|
{!this.state.isLoaded && (
|
||||||
volumes={this.state.volumes}
|
<LoadingIndicator percentComplete={this.state.percentComplete} />
|
||||||
paintFilterLabelMapImageData={
|
)}
|
||||||
this.state.paintFilterLabelMapImageData
|
{this.state.volumes && (
|
||||||
}
|
<ConnectedVTKViewport
|
||||||
paintFilterBackgroundImageData={
|
volumes={this.state.volumes}
|
||||||
this.state.paintFilterBackgroundImageData
|
paintFilterLabelMapImageData={
|
||||||
}
|
this.state.paintFilterLabelMapImageData
|
||||||
viewportIndex={this.props.viewportIndex}
|
}
|
||||||
/>
|
paintFilterBackgroundImageData={
|
||||||
) : (
|
this.state.paintFilterBackgroundImageData
|
||||||
<div style={style}>
|
}
|
||||||
<LoadingIndicator />
|
viewportIndex={this.props.viewportIndex}
|
||||||
</div>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* We use this component to leverage "Code Splitting"
|
||||||
|
*
|
||||||
|
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default function asyncComponent(importComponent) {
|
||||||
|
class AsyncComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
component: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
// Add dynamically loaded component to state
|
||||||
|
const { default: component } = await importComponent();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
component: component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const C = this.state.component;
|
||||||
|
|
||||||
|
// Render the loaded component, or null
|
||||||
|
return C ? <C {...this.props} /> : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AsyncComponent;
|
||||||
|
}
|
||||||
@@ -2,68 +2,28 @@ import {
|
|||||||
vtkInteractorStyleMPRCrosshairs,
|
vtkInteractorStyleMPRCrosshairs,
|
||||||
vtkInteractorStyleMPRWindowLevel,
|
vtkInteractorStyleMPRWindowLevel,
|
||||||
vtkInteractorStyleMPRSlice,
|
vtkInteractorStyleMPRSlice,
|
||||||
|
vtkInteractorStyleMPRRotate,
|
||||||
vtkSVGCrosshairsWidget,
|
vtkSVGCrosshairsWidget,
|
||||||
vtkSVGWidgetManager,
|
|
||||||
} from 'react-vtkjs-viewport';
|
} from 'react-vtkjs-viewport';
|
||||||
|
|
||||||
import setMPRLayout from './utils/setMPRLayout.js';
|
import setMPRLayout from './utils/setMPRLayout.js';
|
||||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||||
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
|
||||||
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
|
|
||||||
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
|
|
||||||
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
||||||
|
import throttle from 'lodash.throttle';
|
||||||
|
|
||||||
const { BlendMode } = Constants;
|
const { BlendMode } = Constants;
|
||||||
|
|
||||||
// TODO: Put this somewhere else
|
// TODO: Put this somewhere else
|
||||||
let apis = {};
|
let apis = {};
|
||||||
let currentSlabThickness = 0.1;
|
|
||||||
|
|
||||||
function getCrosshairCallbackForIndex(index) {
|
|
||||||
return ({ worldPos }) => {
|
|
||||||
// Set camera focal point to world coordinate for linked views
|
|
||||||
apis.forEach((api, viewportIndex) => {
|
|
||||||
if (viewportIndex !== index) {
|
|
||||||
// We are basically doing the same as getSlice but with the world coordinate
|
|
||||||
// that we want to jump to instead of the camera focal point.
|
|
||||||
// I would rather do the camera adjustment directly but I keep
|
|
||||||
// doing it wrong and so this is good enough for now.
|
|
||||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
|
||||||
|
|
||||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
|
||||||
const sliceNormal = istyle.getSliceNormal();
|
|
||||||
const transform = vtkMatrixBuilder
|
|
||||||
.buildFromDegree()
|
|
||||||
.identity()
|
|
||||||
.rotateFromDirections(sliceNormal, [1, 0, 0]);
|
|
||||||
|
|
||||||
const mutatedWorldPos = worldPos.slice();
|
|
||||||
transform.apply(mutatedWorldPos);
|
|
||||||
const slice = mutatedWorldPos[0];
|
|
||||||
|
|
||||||
istyle.setSlice(slice);
|
|
||||||
|
|
||||||
renderWindow.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderer = api.genericRenderWindow.getRenderer();
|
|
||||||
const wPos = vtkCoordinate.newInstance();
|
|
||||||
wPos.setCoordinateSystemToWorld();
|
|
||||||
wPos.setValue(worldPos);
|
|
||||||
|
|
||||||
const displayPosition = wPos.getComputedDisplayValue(renderer);
|
|
||||||
const { svgWidgetManager } = api;
|
|
||||||
api.svgWidgets.crosshairsWidget.setPoint(
|
|
||||||
displayPosition[0],
|
|
||||||
displayPosition[1]
|
|
||||||
);
|
|
||||||
svgWidgetManager.render();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _getActiveViewportVTKApi(viewports) {
|
async function _getActiveViewportVTKApi(viewports) {
|
||||||
const { layout, viewportSpecificData, activeViewportIndex } = viewports;
|
const {
|
||||||
|
numRows,
|
||||||
|
numColumns,
|
||||||
|
layout,
|
||||||
|
viewportSpecificData,
|
||||||
|
activeViewportIndex,
|
||||||
|
} = viewports;
|
||||||
|
|
||||||
const currentData = layout.viewports[activeViewportIndex];
|
const currentData = layout.viewports[activeViewportIndex];
|
||||||
if (currentData && currentData.plugin === 'vtk') {
|
if (currentData && currentData.plugin === 'vtk') {
|
||||||
@@ -81,6 +41,8 @@ async function _getActiveViewportVTKApi(viewports) {
|
|||||||
api = await setViewportToVTK(
|
api = await setViewportToVTK(
|
||||||
displaySet,
|
displaySet,
|
||||||
activeViewportIndex,
|
activeViewportIndex,
|
||||||
|
numRows,
|
||||||
|
numColumns,
|
||||||
layout,
|
layout,
|
||||||
viewportSpecificData
|
viewportSpecificData
|
||||||
);
|
);
|
||||||
@@ -94,49 +56,13 @@ async function _getActiveViewportVTKApi(viewports) {
|
|||||||
|
|
||||||
function _setView(api, sliceNormal, viewUp) {
|
function _setView(api, sliceNormal, viewUp) {
|
||||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
const renderer = api.genericRenderWindow.getRenderer();
|
|
||||||
const camera = renderer.getActiveCamera();
|
|
||||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||||
istyle.setSliceNormal(...sliceNormal);
|
istyle.setSliceNormal(...sliceNormal);
|
||||||
camera.setViewUp(...viewUp);
|
istyle.setViewUp(...viewUp);
|
||||||
|
|
||||||
renderWindow.render();
|
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 currentNormal;
|
|
||||||
if (currentIStyle.getSliceNormal && istyle.getSliceNormal) {
|
|
||||||
currentNormal = currentIStyle.getSliceNormal();
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentSlabThickness;
|
|
||||||
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
|
|
||||||
currentSlabThickness = currentIStyle.getSlabThickness();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderWindow.getInteractor().setInteractorStyle(istyle);
|
|
||||||
|
|
||||||
// TODO: Not sure why this is required the second time this function is called
|
|
||||||
istyle.setInteractor(renderWindow.getInteractor());
|
|
||||||
|
|
||||||
if (istyle.getVolumeMapper() !== api.volumes[0]) {
|
|
||||||
if (currentNormal) {
|
|
||||||
istyle.setSliceNormal(currentNormal);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSlabThickness) {
|
|
||||||
istyle.setSlabThickness(currentSlabThickness);
|
|
||||||
}
|
|
||||||
|
|
||||||
istyle.setVolumeMapper(api.volumes[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
axial: async ({ viewports }) => {
|
axial: async ({ viewports }) => {
|
||||||
const api = await _getActiveViewportVTKApi(viewports);
|
const api = await _getActiveViewportVTKApi(viewports);
|
||||||
@@ -161,69 +87,91 @@ const actions = {
|
|||||||
},
|
},
|
||||||
enableRotateTool: () => {
|
enableRotateTool: () => {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const istyle = vtkInteractorStyleMPRSlice.newInstance();
|
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||||
|
|
||||||
switchMPRInteractors(api, istyle);
|
api.setInteractorStyle({ istyle });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
enableCrosshairsTool: () => {
|
enableCrosshairsTool: () => {
|
||||||
apis.forEach((api, index) => {
|
apis.forEach((api, apiIndex) => {
|
||||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||||
|
|
||||||
switchMPRInteractors(api, istyle);
|
api.setInteractorStyle({
|
||||||
|
istyle,
|
||||||
istyle.setCallback(getCrosshairCallbackForIndex(index));
|
configuration: { apis, apiIndex },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
enableLevelTool: () => {
|
enableLevelTool: () => {
|
||||||
|
function updateVOI(apis, windowWidth, windowCenter) {
|
||||||
|
apis.forEach(api => {
|
||||||
|
api.updateVOI(windowWidth, windowCenter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
|
||||||
|
|
||||||
|
const callbacks = {
|
||||||
|
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
|
||||||
|
apis.forEach(api => {
|
||||||
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
|
|
||||||
|
renderWindow.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
throttledUpdateVOIs(apis, windowWidth, windowCenter);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||||
|
|
||||||
switchMPRInteractors(api, istyle);
|
api.setInteractorStyle({ istyle, callbacks });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setSlabThickness: slabThickness => {
|
setSlabThickness: ({ slabThickness }) => {
|
||||||
currentSlabThickness = slabThickness;
|
|
||||||
|
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
api.setSlabThickness(slabThickness);
|
||||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
|
||||||
|
|
||||||
if (istyle.setSlabThickness) {
|
|
||||||
istyle.setSlabThickness(currentSlabThickness);
|
|
||||||
|
|
||||||
// TODO: Do this inside the interactors in a setSlabThickness function instead
|
|
||||||
const renderer = api.genericRenderWindow.getRenderer();
|
|
||||||
const camera = renderer.getActiveCamera();
|
|
||||||
const dist = camera.getDistance();
|
|
||||||
const near = dist - currentSlabThickness / 2;
|
|
||||||
const far = dist + currentSlabThickness / 2;
|
|
||||||
|
|
||||||
camera.setClippingRange(near, far);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderWindow.render();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
changeSlabThickness: ({ change }) => {
|
changeSlabThickness: ({ change }) => {
|
||||||
currentSlabThickness += change;
|
apis.forEach(api => {
|
||||||
currentSlabThickness = Math.max(currentSlabThickness, 0.1);
|
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
|
||||||
|
|
||||||
|
api.setSlabThickness(slabThickness);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setBlendModeToComposite: () => {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||||
|
|
||||||
if (istyle.setSlabThickness) {
|
const slabThickness = api.getSlabThickness();
|
||||||
istyle.setSlabThickness(currentSlabThickness);
|
|
||||||
|
const mapper = api.volumes[0].getMapper();
|
||||||
|
if (mapper.setBlendModeToComposite) {
|
||||||
|
mapper.setBlendModeToComposite();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (istyle.setSlabThickness) {
|
||||||
|
istyle.setSlabThickness(slabThickness);
|
||||||
|
}
|
||||||
|
renderWindow.render();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setBlendModeToMaximumIntensity: () => {
|
||||||
|
apis.forEach(api => {
|
||||||
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
|
const mapper = api.volumes[0].getMapper();
|
||||||
|
if (mapper.setBlendModeToMaximumIntensity) {
|
||||||
|
mapper.setBlendModeToMaximumIntensity();
|
||||||
|
}
|
||||||
renderWindow.render();
|
renderWindow.render();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setBlendMode: ({ blendMode }) => {
|
setBlendMode: ({ blendMode }) => {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
|
||||||
|
|
||||||
api.volumes[0].getMapper().setBlendMode(blendMode);
|
api.volumes[0].getMapper().setBlendMode(blendMode);
|
||||||
|
|
||||||
@@ -231,75 +179,60 @@ const actions = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
mpr2d: async ({ viewports }) => {
|
mpr2d: async ({ viewports }) => {
|
||||||
|
// TODO push a lot of this backdoor logic lower down to the library level.
|
||||||
const displaySet =
|
const displaySet =
|
||||||
viewports.viewportSpecificData[viewports.activeViewportIndex];
|
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 {
|
try {
|
||||||
apiByViewport = await setMPRLayout(displaySet);
|
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
apis = apiByViewport;
|
if (cornerstoneVOI) {
|
||||||
|
setVOI(cornerstoneVOI);
|
||||||
|
}
|
||||||
|
|
||||||
/*const rgbTransferFunction = apiByViewport[0].volumes[0]
|
// Add widgets and set default interactorStyle of each viewport.
|
||||||
.getProperty()
|
apis.forEach((api, apiIndex) => {
|
||||||
.getRGBTransferFunction(0);
|
api.addSVGWidget(
|
||||||
rgbTransferFunction.onModified(() => {
|
vtkSVGCrosshairsWidget.newInstance(),
|
||||||
apiByViewport.forEach(a => {
|
'crosshairsWidget'
|
||||||
const renderWindow = a.genericRenderWindow.getRenderWindow();
|
);
|
||||||
|
|
||||||
renderWindow.render();
|
|
||||||
});
|
|
||||||
});*/
|
|
||||||
|
|
||||||
apiByViewport.forEach((api, index) => {
|
|
||||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
|
||||||
const renderer = api.genericRenderWindow.getRenderer();
|
|
||||||
const camera = renderer.getActiveCamera();
|
|
||||||
|
|
||||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||||
renderWindow.getInteractor().setInteractorStyle(istyle);
|
|
||||||
|
|
||||||
istyle.setVolumeMapper(api.volumes[0]);
|
api.setInteractorStyle({
|
||||||
istyle.setCallback(getCrosshairCallbackForIndex(index));
|
istyle,
|
||||||
|
configuration: { apis, apiIndex },
|
||||||
const svgWidgetManager = vtkSVGWidgetManager.newInstance();
|
});
|
||||||
svgWidgetManager.setRenderer(renderer);
|
|
||||||
svgWidgetManager.setScale(1);
|
|
||||||
|
|
||||||
const crosshairsWidget = vtkSVGCrosshairsWidget.newInstance();
|
|
||||||
|
|
||||||
svgWidgetManager.addWidget(crosshairsWidget);
|
|
||||||
svgWidgetManager.render();
|
|
||||||
|
|
||||||
api.svgWidgetManager = svgWidgetManager;
|
|
||||||
api.svgWidgets = {
|
|
||||||
crosshairsWidget,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (index) {
|
|
||||||
default:
|
|
||||||
case 0:
|
|
||||||
//Axial
|
|
||||||
istyle.setSliceNormal(0, 0, 1);
|
|
||||||
camera.setViewUp(0, -1, 0);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
// sagittal
|
|
||||||
istyle.setSliceNormal(1, 0, 0);
|
|
||||||
camera.setViewUp(0, 0, 1);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
// Coronal
|
|
||||||
istyle.setSliceNormal(0, 1, 0);
|
|
||||||
camera.setViewUp(0, 0, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderWindow.render();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -338,12 +271,12 @@ const definitions = {
|
|||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
setBlendModeToComposite: {
|
setBlendModeToComposite: {
|
||||||
commandFn: actions.setBlendMode,
|
commandFn: actions.setBlendModeToComposite,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||||
},
|
},
|
||||||
setBlendModeToMaximumIntensity: {
|
setBlendModeToMaximumIntensity: {
|
||||||
commandFn: actions.setBlendMode,
|
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||||
},
|
},
|
||||||
@@ -389,3 +322,41 @@ export default {
|
|||||||
definitions,
|
definitions,
|
||||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getVOIFromCornerstoneViewport(displaySet) {
|
||||||
|
const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom);
|
||||||
|
|
||||||
|
if (cornerstoneElement) {
|
||||||
|
const imageId = cornerstoneElement.image.imageId;
|
||||||
|
|
||||||
|
const { modality } = cornerstone.metaData.get(
|
||||||
|
'generalSeriesModule',
|
||||||
|
imageId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (modality !== 'PT') {
|
||||||
|
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
|
||||||
|
|
||||||
|
return {
|
||||||
|
windowWidth,
|
||||||
|
windowCenter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVOI(voi) {
|
||||||
|
const { windowWidth, windowCenter } = voi;
|
||||||
|
const lower = windowCenter - windowWidth / 2.0;
|
||||||
|
const upper = windowCenter + windowWidth / 2.0;
|
||||||
|
|
||||||
|
const rgbTransferFunction = apis[0].volumes[0]
|
||||||
|
.getProperty()
|
||||||
|
.getRGBTransferFunction(0);
|
||||||
|
|
||||||
|
rgbTransferFunction.setRange(lower, upper);
|
||||||
|
|
||||||
|
apis.forEach(api => {
|
||||||
|
api.updateVOI(windowWidth, windowCenter);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import OHIFVTKViewport from './OHIFVTKViewport.js';
|
import asyncComponent from './asyncComponent.js';
|
||||||
import commandsModule from './commandsModule.js';
|
import commandsModule from './commandsModule.js';
|
||||||
import toolbarModule from './toolbarModule.js';
|
import toolbarModule from './toolbarModule.js';
|
||||||
// This feels weird
|
// This feels weird
|
||||||
// import loadLocales from './loadLocales';
|
// import loadLocales from './loadLocales';
|
||||||
|
|
||||||
|
const OHIFVTKViewport = asyncComponent(() =>
|
||||||
|
import(/* webpackChunkName: "OHIFVTKViewport" */ './OHIFVTKViewport.js')
|
||||||
|
);
|
||||||
|
|
||||||
const vtkExtension = {
|
const vtkExtension = {
|
||||||
/**
|
/**
|
||||||
* Only required property. Should be a unique value across all extensions.
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Range, Checkbox, Select } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './slab-thickness-toolbar-button.styl';
|
||||||
|
|
||||||
|
const SLIDER = {
|
||||||
|
MIN: 0.1,
|
||||||
|
MAX: 1000,
|
||||||
|
STEP: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolbarLabel = props => {
|
||||||
|
const { label } = props;
|
||||||
|
return <div className="toolbar-button-label">{label}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolbarSlider = props => {
|
||||||
|
const { value, min, max, onChange } = props;
|
||||||
|
return (
|
||||||
|
<div className="toolbar-slider-container">
|
||||||
|
<label htmlFor="toolbar-slider">{value}mm</label>
|
||||||
|
<Range
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={SLIDER.STEP}
|
||||||
|
onChange={onChange}
|
||||||
|
id="toolbar-slider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getSelectOptions = button => {
|
||||||
|
return button.operationButtons.map(button => {
|
||||||
|
return {
|
||||||
|
key: button.label,
|
||||||
|
value: button.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getClassNames = (isActive, className) => {
|
||||||
|
return classnames('toolbar-button', 'slab-thickness', className, {
|
||||||
|
active: isActive,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _applySlabThickness = (
|
||||||
|
value,
|
||||||
|
modeChecked,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button
|
||||||
|
) => {
|
||||||
|
if (!modeChecked || !toolbarClickCallback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { actionButton } = button;
|
||||||
|
|
||||||
|
const generateOperation = (operation, value) => {
|
||||||
|
// Combine slider value into slider operation
|
||||||
|
const generatedOperation = { ...operation };
|
||||||
|
generatedOperation.commandOptions = {
|
||||||
|
...operation.commandOptions,
|
||||||
|
slabThickness: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
return generatedOperation;
|
||||||
|
};
|
||||||
|
|
||||||
|
const operation = generateOperation(actionButton, value);
|
||||||
|
toolbarClickCallback(operation, event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _applyModeOperation = (
|
||||||
|
operation,
|
||||||
|
modeChecked,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button
|
||||||
|
) => {
|
||||||
|
// in case modeChecked has not being triggered by user yet
|
||||||
|
if (typeof modeChecked !== 'boolean') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { deactivateButton } = button;
|
||||||
|
|
||||||
|
const _operation = modeChecked ? operation : deactivateButton;
|
||||||
|
if (toolbarClickCallback && _operation) {
|
||||||
|
toolbarClickCallback(_operation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getInitialState = currentSelectedOption => {
|
||||||
|
return {
|
||||||
|
value: SLIDER.MIN,
|
||||||
|
sliderMin: SLIDER.MIN,
|
||||||
|
sliderMax: SLIDER.MAX,
|
||||||
|
modeChecked: undefined,
|
||||||
|
operation: currentSelectedOption,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const INITIAL_OPTION_INDEX = 0;
|
||||||
|
const _getInitialtSelectedOption = (button = {}) => {
|
||||||
|
return (
|
||||||
|
button.operationButtons && button.operationButtons[INITIAL_OPTION_INDEX]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function SlabThicknessToolbarComponent({
|
||||||
|
parentContext,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button,
|
||||||
|
activeButtons,
|
||||||
|
isActive,
|
||||||
|
className,
|
||||||
|
}) {
|
||||||
|
const currentSelectedOption = _getInitialtSelectedOption(button);
|
||||||
|
const [state, setState] = useState(_getInitialState(currentSelectedOption));
|
||||||
|
const { label, operationButtons } = button;
|
||||||
|
const _className = _getClassNames(isActive, className);
|
||||||
|
const selectOptions = _getSelectOptions(button);
|
||||||
|
function onChangeSelect(selectedValue) {
|
||||||
|
// find select value
|
||||||
|
const operation = operationButtons.find(
|
||||||
|
button => button.id === selectedValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (operation === state.operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState({ ...state, operation });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeCheckbox(checked) {
|
||||||
|
setState({ ...state, modeChecked: checked });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeSlider(event) {
|
||||||
|
const value = Number(event.target.value);
|
||||||
|
|
||||||
|
if (value !== state.value) {
|
||||||
|
setState({ ...state, value, modeChecked: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
_applyModeOperation(
|
||||||
|
state.operation,
|
||||||
|
state.modeChecked,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button
|
||||||
|
);
|
||||||
|
}, [state.modeChecked, state.operation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
_applySlabThickness(
|
||||||
|
state.value,
|
||||||
|
state.modeChecked,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button
|
||||||
|
);
|
||||||
|
}, [state.operation, state.modeChecked, state.value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={_className}>
|
||||||
|
<div className="container">
|
||||||
|
<ToolbarSlider
|
||||||
|
value={state.value}
|
||||||
|
min={state.sliderMin}
|
||||||
|
max={state.sliderMax}
|
||||||
|
onChange={onChangeSlider}
|
||||||
|
/>
|
||||||
|
<ToolbarLabel key="toolbar-label" label={label} />
|
||||||
|
</div>
|
||||||
|
<div className="controller">
|
||||||
|
<Checkbox
|
||||||
|
label="mode"
|
||||||
|
checked={state.modeChecked}
|
||||||
|
onChange={onChangeCheckbox}
|
||||||
|
></Checkbox>
|
||||||
|
<Select
|
||||||
|
key="toolbar-select"
|
||||||
|
options={selectOptions}
|
||||||
|
value={selectOptions[INITIAL_OPTION_INDEX].value}
|
||||||
|
onChange={onChangeSelect}
|
||||||
|
></Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SlabThicknessToolbarComponent.propTypes = {
|
||||||
|
parentContext: PropTypes.object.isRequired,
|
||||||
|
toolbarClickCallback: PropTypes.func.isRequired,
|
||||||
|
button: PropTypes.object.isRequired,
|
||||||
|
activeButtons: PropTypes.array.isRequired,
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SlabThicknessToolbarComponent;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.slab-thickness
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 60px;
|
||||||
|
margin-top: -12px;
|
||||||
|
&:hover
|
||||||
|
color: var(--default-color);
|
||||||
|
.container
|
||||||
|
padding-right: 10px;
|
||||||
|
height: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
.toolbar-slider-container
|
||||||
|
width: 80px;
|
||||||
|
margin: 0px 2px 2px 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
&:hover
|
||||||
|
color: var(--hover-color);
|
||||||
|
input
|
||||||
|
height: 20px;
|
||||||
|
padding-top: 6px;
|
||||||
|
&::-moz-range-thumb
|
||||||
|
height: 16px;
|
||||||
|
margin-top: -7px;
|
||||||
|
&::-webkit-slider-thumb
|
||||||
|
height: 16px;
|
||||||
|
margin-top: -7px;
|
||||||
|
.controller
|
||||||
|
width: 62px;
|
||||||
|
.ohif-check-container
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding-left: 18px;
|
||||||
|
.ohif-check-label
|
||||||
|
text-transform: capitalize;
|
||||||
|
&:hover
|
||||||
|
color: var(--hover-color);
|
||||||
|
.ohif-checkbox
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: white;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
top: 22%;
|
||||||
|
&.ohif-checked:after
|
||||||
|
background-color: var(--active-color);
|
||||||
|
top: 17%;
|
||||||
|
left: 17%;
|
||||||
|
.select-ohif
|
||||||
|
color: var(--default-color);
|
||||||
|
border-color: var(--default-color);
|
||||||
|
background-color: black;
|
||||||
|
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
|
||||||
|
&:hover
|
||||||
|
color: var(--hover-color);
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
|
||||||
|
|
||||||
const TOOLBAR_BUTTON_TYPES = {
|
const TOOLBAR_BUTTON_TYPES = {
|
||||||
COMMAND: 'command',
|
COMMAND: 'command',
|
||||||
SET_TOOL_ACTIVE: 'setToolActive',
|
SET_TOOL_ACTIVE: 'setToolActive',
|
||||||
@@ -31,6 +33,7 @@ const definitions = [
|
|||||||
commandName: 'enableRotateTool',
|
commandName: 'enableRotateTool',
|
||||||
commandOptions: {},
|
commandOptions: {},
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
{
|
{
|
||||||
id: 'setBlendModeToComposite',
|
id: 'setBlendModeToComposite',
|
||||||
label: 'Disable MIP',
|
label: 'Disable MIP',
|
||||||
@@ -68,6 +71,50 @@ const definitions = [
|
|||||||
commandName: 'decreaseSlabThickness',
|
commandName: 'decreaseSlabThickness',
|
||||||
commandOptions: {},
|
commandOptions: {},
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
id: 'changeSlabThickness',
|
||||||
|
label: 'Slab Thickness',
|
||||||
|
icon: 'soft-tissue',
|
||||||
|
CustomComponent: SlabThicknessToolbarComponent,
|
||||||
|
commandName: 'setSlabThickness',
|
||||||
|
actionButton: {
|
||||||
|
id: 'setSlabThickness',
|
||||||
|
label: 'slider',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
commandName: 'setSlabThickness',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
deactivateButton: {
|
||||||
|
id: 'setBlendModeToComposite',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setBlendModeToComposite',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
operationButtons: [
|
||||||
|
{
|
||||||
|
id: 'setBlendModeToMaximumIntensity',
|
||||||
|
label: 'MIP',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setBlendModeToMaximumIntensity',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'setBlendModeToMinimumIntensity',
|
||||||
|
label: 'MinIP',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setBlendModeToMinimumIntensity',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'setBlendModeToAverageIntensity',
|
||||||
|
label: 'AvgIP',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setBlendModeToAverageIntensity',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,60 +1,56 @@
|
|||||||
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
let viewports = [];
|
const viewports = [];
|
||||||
const rows = 1;
|
const numViewports = numRows * numColumns;
|
||||||
const columns = 3;
|
|
||||||
const numViewports = rows * columns;
|
|
||||||
const viewportSpecificData = {};
|
|
||||||
for (let i = 0; i < numViewports; i++) {
|
|
||||||
viewports.push({
|
|
||||||
height: `${100 / rows}%`,
|
|
||||||
width: `${100 / columns}%`,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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] = displaySet;
|
||||||
viewportSpecificData[i].plugin = 'vtk';
|
viewportSpecificData[i].plugin = 'vtk';
|
||||||
}
|
}
|
||||||
const layout = {
|
|
||||||
viewports,
|
|
||||||
};
|
|
||||||
|
|
||||||
const viewportIndices = [0, 1, 2];
|
|
||||||
let updatedViewports = layout.viewports;
|
|
||||||
|
|
||||||
const apis = [];
|
const apis = [];
|
||||||
viewportIndices.forEach(viewportIndex => {
|
viewports.forEach((viewport, index) => {
|
||||||
apis[viewportIndex] = null;
|
apis[index] = null;
|
||||||
/*const currentData = layout.viewports[viewportIndex];
|
const viewportProps = viewportPropsArray[index];
|
||||||
if (currentData && currentData.plugin === 'vtk') {
|
viewports[index] = Object.assign({}, viewports[index], {
|
||||||
reject(new Error('Should not have reached this point??'));
|
|
||||||
}*/
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
// plugin: 'vtk',
|
|
||||||
vtk: {
|
vtk: {
|
||||||
mode: 'mpr', // TODO: not used
|
mode: 'mpr', // TODO: not used
|
||||||
afterCreation: api => {
|
afterCreation: api => {
|
||||||
apis[viewportIndex] = api;
|
apis[index] = api;
|
||||||
|
|
||||||
if (apis.every(a => !!a)) {
|
if (apis.every(a => !!a)) {
|
||||||
resolve(apis);
|
resolve(apis);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
...viewportProps,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
updatedViewports = setSingleLayoutData(
|
|
||||||
updatedViewports,
|
|
||||||
viewportIndex,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setLayoutAndViewportData(
|
setLayoutAndViewportData(
|
||||||
{ viewports: updatedViewports },
|
{
|
||||||
|
numRows,
|
||||||
|
numColumns,
|
||||||
|
viewports,
|
||||||
|
},
|
||||||
viewportSpecificData
|
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;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
|
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
|
||||||
import setSingleLayoutData from './setSingleLayoutData.js';
|
|
||||||
|
|
||||||
export default function setViewportToVTK(
|
export default function setViewportToVTK(
|
||||||
displaySet,
|
displaySet,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
|
numRows,
|
||||||
|
numColumns,
|
||||||
layout,
|
layout,
|
||||||
viewportSpecificData
|
viewportSpecificData
|
||||||
) {
|
) {
|
||||||
@@ -13,7 +14,9 @@ export default function setViewportToVTK(
|
|||||||
reject(new Error('Should not have reached this point??'));
|
reject(new Error('Should not have reached this point??'));
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
const data = {
|
const viewports = layout.viewports.slice();
|
||||||
|
|
||||||
|
viewports[viewportIndex] = Object.assign({}, viewports[viewportIndex], {
|
||||||
// plugin: 'vtk',
|
// plugin: 'vtk',
|
||||||
vtk: {
|
vtk: {
|
||||||
mode: 'mpr', // TODO: not used
|
mode: 'mpr', // TODO: not used
|
||||||
@@ -21,18 +24,16 @@ export default function setViewportToVTK(
|
|||||||
resolve(api);
|
resolve(api);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const updatedViewports = setSingleLayoutData(
|
|
||||||
layout.viewports,
|
|
||||||
viewportIndex,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedViewportData = viewportSpecificData;
|
const updatedViewportData = viewportSpecificData;
|
||||||
|
|
||||||
setLayoutAndViewportData(
|
setLayoutAndViewportData(
|
||||||
{ viewports: updatedViewports },
|
{
|
||||||
|
numRows,
|
||||||
|
numColumns,
|
||||||
|
viewports,
|
||||||
|
},
|
||||||
updatedViewportData
|
updatedViewportData
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+19
-1
@@ -21,11 +21,14 @@
|
|||||||
"dev:project": ".scripts/dev.sh",
|
"dev:project": ".scripts/dev.sh",
|
||||||
"dev:orthanc": "lerna run dev:orthanc --stream",
|
"dev:orthanc": "lerna run dev:orthanc --stream",
|
||||||
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
||||||
|
"start": "yarn run dev",
|
||||||
"test": "yarn run test:unit",
|
"test": "yarn run test:unit",
|
||||||
"test:unit": "jest --collectCoverage",
|
"test:unit": "jest --collectCoverage",
|
||||||
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
|
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
|
||||||
"test:e2e": "lerna run test:e2e --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:dist": "lerna run test:e2e:dist --stream",
|
||||||
|
"test:e2e:serve": "lerna run test:e2e:serve --stream",
|
||||||
"see-changed": "lerna changed",
|
"see-changed": "lerna changed",
|
||||||
"docs:publish": "chmod +x ./build-and-publish-docs.sh && ./build-and-publish-docs.sh",
|
"docs:publish": "chmod +x ./build-and-publish-docs.sh && ./build-and-publish-docs.sh",
|
||||||
"release": "yarn run lerna:version && yarn run lerna:publish",
|
"release": "yarn run lerna:version && yarn run lerna:publish",
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
"@babel/plugin-transform-runtime": "^7.5.0",
|
"@babel/plugin-transform-runtime": "^7.5.0",
|
||||||
"@babel/preset-env": "^7.5.0",
|
"@babel/preset-env": "^7.5.0",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"@percy/cypress": "^2.2.0",
|
||||||
"babel-eslint": "9.x",
|
"babel-eslint": "9.x",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-inline-react-svg": "1.1.0",
|
"babel-plugin-inline-react-svg": "1.1.0",
|
||||||
@@ -55,12 +59,15 @@
|
|||||||
"copy-webpack-plugin": "^5.0.3",
|
"copy-webpack-plugin": "^5.0.3",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"css-loader": "^3.2.0",
|
"css-loader": "^3.2.0",
|
||||||
|
"dotenv": "^8.1.0",
|
||||||
"eslint": "5.16.0",
|
"eslint": "5.16.0",
|
||||||
|
"eslint-config-prettier": "^6.4.0",
|
||||||
"eslint-config-react-app": "^4.0.1",
|
"eslint-config-react-app": "^4.0.1",
|
||||||
"eslint-plugin-flowtype": "2.x",
|
"eslint-plugin-flowtype": "2.x",
|
||||||
"eslint-plugin-import": "2.x",
|
"eslint-plugin-import": "2.x",
|
||||||
"eslint-plugin-jsx-a11y": "6.x",
|
"eslint-plugin-jsx-a11y": "6.x",
|
||||||
"eslint-plugin-node": "^9.1.0",
|
"eslint-plugin-node": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.1",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-react": "7.x",
|
"eslint-plugin-react": "7.x",
|
||||||
"eslint-plugin-react-hooks": "1.5.0",
|
"eslint-plugin-react-hooks": "1.5.0",
|
||||||
@@ -89,7 +96,7 @@
|
|||||||
"stylelint-config-recommended": "^2.2.0",
|
"stylelint-config-recommended": "^2.2.0",
|
||||||
"stylus": "^0.54.5",
|
"stylus": "^0.54.5",
|
||||||
"stylus-loader": "^3.0.2",
|
"stylus-loader": "^3.0.2",
|
||||||
"terser-webpack-plugin": "^1.4.1",
|
"terser-webpack-plugin": "^2.1.0",
|
||||||
"webpack": "^4.35.2",
|
"webpack": "^4.35.2",
|
||||||
"webpack-cli": "^3.3.5",
|
"webpack-cli": "^3.3.5",
|
||||||
"webpack-dev-server": "^3.7.2",
|
"webpack-dev-server": "^3.7.2",
|
||||||
@@ -98,6 +105,17 @@
|
|||||||
"workbox-webpack-plugin": "^4.3.1",
|
"workbox-webpack-plugin": "^4.3.1",
|
||||||
"worker-loader": "^2.0.0"
|
"worker-loader": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"src/**/*.{js,json,css}": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/@babel/runtime": "7.5.5"
|
"**/@babel/runtime": "7.5.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,167 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.1...@ohif/core@1.5.2) (2019-10-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* set SR in ActiveViewport by clicking thumb ([#1091](https://github.com/OHIF/Viewers/issues/1091)) ([986b7ae](https://github.com/OHIF/Viewers/commit/986b7ae2bf4f7d27f326e62f93285ce20eaf0a79))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.0...@ohif/core@1.5.1) (2019-10-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.4.0...@ohif/core@1.5.0) (2019-10-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List ([#1069](https://github.com/OHIF/Viewers/issues/1069)) ([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Improve usability of Google Cloud adapter, including direct routes to studies ([#989](https://github.com/OHIF/Viewers/issues/989)) ([2bc361c](https://github.com/OHIF/Viewers/commit/2bc361c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.1.0...@ohif/core@1.2.0) (2019-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* OHIF-1002 Study lazy load should be true by default ([#1004](https://github.com/OHIF/Viewers/issues/1004)) ([66d8bc6](https://github.com/OHIF/Viewers/commit/66d8bc6))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Allow a server requestOptions.auth to be a function that returns the Authorization header. ([#928](https://github.com/OHIF/Viewers/issues/928)) ([0064a4b](https://github.com/OHIF/Viewers/commit/0064a4b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.2...@ohif/core@1.1.0) (2019-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.1...@ohif/core@1.0.2) (2019-10-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Temporarily sort SEG files to the end of the display set list as a workaround for several metadata issues ([#987](https://github.com/OHIF/Viewers/issues/987)) ([b3b4c10](https://github.com/OHIF/Viewers/commit/b3b4c10))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.0...@ohif/core@1.0.1) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Check for Value in 00081155 sequence (Few patient protocol images doesn't have this value) and removed a duplicate declaration ([#921](https://github.com/OHIF/Viewers/issues/921)) ([d0ec9cf](https://github.com/OHIF/Viewers/commit/d0ec9cf))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.10...@ohif/core@1.0.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* DICOM Seg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.9...@ohif/core@0.50.10) (2019-09-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Avoid using variable name "module" ([#942](https://github.com/OHIF/Viewers/issues/942)) ([72427fe](https://github.com/OHIF/Viewers/commit/72427fe)), closes [#940](https://github.com/OHIF/Viewers/issues/940)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.8...@ohif/core@0.50.9) (2019-09-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.7...@ohif/core@0.50.8) (2019-09-10)
|
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.7...@ohif/core@0.50.8) (2019-09-10)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/core
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "0.50.8",
|
"version": "1.5.2",
|
||||||
"description": "Generic business logic for web-based medical imaging applications",
|
"description": "Generic business logic for web-based medical imaging applications",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-tools": "^4.0.0",
|
"cornerstone-tools": "^4.0.9",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dicom-parser": "^1.8.3"
|
"dicom-parser": "^1.8.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import user from '../user';
|
|||||||
* @export
|
* @export
|
||||||
* @param {Object} [server={}]
|
* @param {Object} [server={}]
|
||||||
* @param {Object} [server.requestOptions]
|
* @param {Object} [server.requestOptions]
|
||||||
* @param {string} [server.requestOptions.auth]
|
* @param {string|function} [server.requestOptions.auth]
|
||||||
* @returns {Object} { Authorization }
|
* @returns {Object} { Authorization }
|
||||||
*/
|
*/
|
||||||
export default function getAuthorizationHeader({ requestOptions } = {}) {
|
export default function getAuthorizationHeader({ requestOptions } = {}) {
|
||||||
@@ -19,8 +19,13 @@ export default function getAuthorizationHeader({ requestOptions } = {}) {
|
|||||||
const accessToken = user && user.getAccessToken && user.getAccessToken();
|
const accessToken = user && user.getAccessToken && user.getAccessToken();
|
||||||
|
|
||||||
if (requestOptions && requestOptions.auth) {
|
if (requestOptions && requestOptions.auth) {
|
||||||
// HTTP Basic Auth (user:password)
|
if (typeof requestOptions.auth === 'function') {
|
||||||
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
|
// Custom Auth Header
|
||||||
|
headers.Authorization = requestOptions.auth(requestOptions);
|
||||||
|
} else {
|
||||||
|
// HTTP Basic Auth (user:password)
|
||||||
|
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
|
||||||
|
}
|
||||||
} else if (accessToken) {
|
} else if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ describe('getAuthorizationHeader', () => {
|
|||||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => {
|
it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => {
|
||||||
const validServer = {
|
const validServer = {
|
||||||
requestOptions: {
|
requestOptions: {
|
||||||
auth: {
|
auth: 'dummy_user:dummy_password',
|
||||||
user: 'dummy_user',
|
|
||||||
password: 'dummy_password',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,9 +23,7 @@ describe('getAuthorizationHeader', () => {
|
|||||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no password', () => {
|
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no password', () => {
|
||||||
const validServerWithoutPassword = {
|
const validServerWithoutPassword = {
|
||||||
requestOptions: {
|
requestOptions: {
|
||||||
auth: {
|
auth: 'dummy_user',
|
||||||
user: 'dummy_user',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,22 +38,19 @@ describe('getAuthorizationHeader', () => {
|
|||||||
expect(authentication).toEqual(expectedAuthorizationHeader);
|
expect(authentication).toEqual(expectedAuthorizationHeader);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no username', () => {
|
it('should return a HTTP Basic Auth when server contains requestOptions.auth custom function', () => {
|
||||||
const validServerWithoutPassword = {
|
const validServerCustomAuth = {
|
||||||
requestOptions: {
|
requestOptions: {
|
||||||
auth: {
|
auth: options => `Basic ${options.token}`,
|
||||||
user: 'dummy_user',
|
token: 'ZHVtbXlfdXNlcjpkdW1teV9wYXNzd29yZA==',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedAuthorizationHeader = {
|
const expectedAuthorizationHeader = {
|
||||||
Authorization: `Basic ${btoa(
|
Authorization: `Basic ${validServerCustomAuth.requestOptions.token}`,
|
||||||
validServerWithoutPassword.requestOptions.auth
|
|
||||||
)}`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const authentication = getAuthorizationHeader(validServerWithoutPassword);
|
const authentication = getAuthorizationHeader(validServerCustomAuth);
|
||||||
|
|
||||||
expect(authentication).toEqual(expectedAuthorizationHeader);
|
expect(authentication).toEqual(expectedAuthorizationHeader);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// import { api } from 'dicomweb-client'
|
// import { api } from 'dicomweb-client'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
DICOMwebClient: jest.fn().mockImplementation(() => {
|
DICOMwebClient: jest.fn().mockImplementation(function() {
|
||||||
return {
|
this.retrieveStudyMetadata = jest.fn().mockResolvedValue([]);
|
||||||
retrieveStudyMetadata: jest.fn().mockResolvedValue([]),
|
this.retrieveSeriesMetadata = jest.fn(function(options) {
|
||||||
};
|
const { studyInstanceUID, seriesInstanceUID } = options;
|
||||||
|
return Promise.resolve([{ studyInstanceUID, seriesInstanceUID }]);
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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' ] ]
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -8,7 +8,9 @@ import { SeriesMetadata } from './SeriesMetadata';
|
|||||||
// - createStacks
|
// - createStacks
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
// - createStacks
|
// - createStacks
|
||||||
import { isImage } from './../../utils/isImage';
|
import { isImage } from '../../utils/isImage';
|
||||||
|
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
|
||||||
|
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||||
|
|
||||||
export class StudyMetadata extends Metadata {
|
export class StudyMetadata extends Metadata {
|
||||||
constructor(data, uid) {
|
constructor(data, uid) {
|
||||||
@@ -88,6 +90,101 @@ export class StudyMetadata extends Metadata {
|
|||||||
return this._displaySets.slice();
|
return this._displaySets.slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a series metadata object into display sets
|
||||||
|
* @param {Array} sopClassHandlerModules List of SOP Class Modules
|
||||||
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
|
* @param {Array} [givenDisplaySets] An optional list to which the display sets will be appended
|
||||||
|
* @returns {Array} The list of display sets created for the given series object
|
||||||
|
*/
|
||||||
|
_createDisplaySetsForSeries(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
series,
|
||||||
|
givenDisplaySets
|
||||||
|
) {
|
||||||
|
const study = this;
|
||||||
|
const displaySets = Array.isArray(givenDisplaySets) ? givenDisplaySets : [];
|
||||||
|
const anyInstances = series.getInstanceCount() > 0;
|
||||||
|
|
||||||
|
if (!anyInstances) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopClassUids = getSopClassUids(series);
|
||||||
|
|
||||||
|
if (sopClassHandlerModules && sopClassHandlerModules.length > 0) {
|
||||||
|
const displaySet = _getDisplaySetFromSopClassModule(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
series,
|
||||||
|
study,
|
||||||
|
sopClassUids
|
||||||
|
);
|
||||||
|
if (displaySet) {
|
||||||
|
displaySet.sopClassModule = true;
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
return displaySets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
|
||||||
|
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
|
||||||
|
|
||||||
|
// Search through the instances (InstanceMetadata object) of this series
|
||||||
|
// Split Multi-frame instances and Single-image modalities
|
||||||
|
// into their own specific display sets. Place the rest of each
|
||||||
|
// series into another display set.
|
||||||
|
const stackableInstances = [];
|
||||||
|
series.forEachInstance(instance => {
|
||||||
|
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
|
||||||
|
if (
|
||||||
|
!isImage(instance.getRawValue('x00080016')) &&
|
||||||
|
!instance.getRawValue('x00280010')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (isMultiFrame(instance)) {
|
||||||
|
displaySet = makeDisplaySet(series, [instance]);
|
||||||
|
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
isClip: true,
|
||||||
|
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||||
|
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
|
||||||
|
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
|
||||||
|
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
||||||
|
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
} else if (isSingleImageModality(instance.modality)) {
|
||||||
|
displaySet = makeDisplaySet(series, [instance]);
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
|
||||||
|
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||||
|
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
||||||
|
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
} else {
|
||||||
|
stackableInstances.push(instance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stackableInstances.length) {
|
||||||
|
const displaySet = makeDisplaySet(series, stackableInstances);
|
||||||
|
displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID());
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return displaySets;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a set of series to be placed in the Study Metadata
|
* Creates a set of series to be placed in the Study Metadata
|
||||||
* The series that appear in the Study Metadata must represent
|
* The series that appear in the Study Metadata must represent
|
||||||
@@ -101,103 +198,44 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @returns {Array} An array of series to be placed in the Study Metadata
|
* @returns {Array} An array of series to be placed in the Study Metadata
|
||||||
*/
|
*/
|
||||||
createDisplaySets(sopClassHandlerModules) {
|
createDisplaySets(sopClassHandlerModules) {
|
||||||
const study = this;
|
|
||||||
const displaySets = [];
|
const displaySets = [];
|
||||||
const anyDisplaySets = study.getSeriesCount();
|
const anyDisplaySets = this.getSeriesCount();
|
||||||
const anySopClassHandlerModules =
|
|
||||||
sopClassHandlerModules && sopClassHandlerModules.length > 0;
|
|
||||||
|
|
||||||
if (!anyDisplaySets) {
|
if (!anyDisplaySets) {
|
||||||
return displaySets;
|
return displaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through the series (SeriesMetadata)
|
// Loop through the series (SeriesMetadata)
|
||||||
this.forEachSeries(series => {
|
this.forEachSeries(
|
||||||
const anyInstances = series.getInstanceCount() > 0;
|
series =>
|
||||||
if (!anyInstances) {
|
void this._createDisplaySetsForSeries(
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sopClassUids = getSopClassUids(series);
|
|
||||||
|
|
||||||
if (anySopClassHandlerModules) {
|
|
||||||
const displaySet = _getDisplaySetFromSopClassModule(
|
|
||||||
sopClassHandlerModules,
|
sopClassHandlerModules,
|
||||||
series,
|
series,
|
||||||
study,
|
displaySets
|
||||||
sopClassUids
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (displaySet) {
|
return sortDisplaySetList(displaySets);
|
||||||
displaySets.push(displaySet);
|
}
|
||||||
|
|
||||||
return;
|
sortDisplaySets() {
|
||||||
}
|
sortDisplaySetList(this._displaySets);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
|
/**
|
||||||
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
|
* Method to append display sets from a given series to the internal list of display sets
|
||||||
|
* @param {Array} sopClassHandlerModules A list of SOP Class Handler Modules
|
||||||
// Search through the instances (InstanceMetadata object) of this series
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
// Split Multi-frame instances and Single-image modalities
|
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
|
||||||
// into their own specific display sets. Place the rest of each
|
*/
|
||||||
// series into another display set.
|
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
|
||||||
const stackableInstances = [];
|
if (this.containsSeries(series)) {
|
||||||
series.forEachInstance(instance => {
|
this.setDisplaySets(
|
||||||
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
|
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
||||||
if (
|
);
|
||||||
!isImage(instance.getRawValue('x00080016')) &&
|
return true;
|
||||||
!instance.getRawValue('x00280010')
|
}
|
||||||
) {
|
return false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let displaySet;
|
|
||||||
|
|
||||||
if (isMultiFrame(instance)) {
|
|
||||||
displaySet = makeDisplaySet(series, [instance]);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
isClip: true,
|
|
||||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
|
||||||
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
|
|
||||||
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
|
|
||||||
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
|
||||||
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
} else if (isSingleImageModality(instance.modality)) {
|
|
||||||
displaySet = makeDisplaySet(series, [instance]);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
|
|
||||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
|
||||||
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
|
||||||
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
} else {
|
|
||||||
stackableInstances.push(instance);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (stackableInstances.length) {
|
|
||||||
const displaySet = makeDisplaySet(series, stackableInstances);
|
|
||||||
displaySet.setAttribute(
|
|
||||||
'studyInstanceUid',
|
|
||||||
study.getStudyInstanceUID()
|
|
||||||
);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
displaySets.sort(_sortBySeriesNumber);
|
|
||||||
|
|
||||||
return displaySets;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -205,7 +243,10 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @param {Array} displaySets Array of display sets (ImageSet[])
|
* @param {Array} displaySets Array of display sets (ImageSet[])
|
||||||
*/
|
*/
|
||||||
setDisplaySets(displaySets) {
|
setDisplaySets(displaySets) {
|
||||||
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
if (Array.isArray(displaySets) && displaySets.length > 0) {
|
||||||
|
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
||||||
|
this.sortDisplaySets();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,7 +255,7 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @returns {boolean} True on success, false on failure.
|
* @returns {boolean} True on success, false on failure.
|
||||||
*/
|
*/
|
||||||
addDisplaySet(displaySet) {
|
addDisplaySet(displaySet) {
|
||||||
if (displaySet instanceof ImageSet) {
|
if (displaySet instanceof ImageSet || displaySet.sopClassModule) {
|
||||||
this._displaySets.push(displaySet);
|
this._displaySets.push(displaySet);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -319,6 +360,12 @@ export class StudyMetadata extends Metadata {
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
containsSeries(series) {
|
||||||
|
return (
|
||||||
|
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the number of series within the current study.
|
* Retrieve the number of series within the current study.
|
||||||
* @returns {number} The number of series in the current study.
|
* @returns {number} The number of series in the current study.
|
||||||
@@ -575,6 +622,16 @@ const makeDisplaySet = (series, instances) => {
|
|||||||
imageSet.getImage(0).getRawValue('x00200013')
|
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;
|
return imageSet;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -649,13 +706,51 @@ function _getDisplaySetFromSopClassModule(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sort series primarily by modality (i.e., series with references to other
|
||||||
|
* series like SEG, KO or PR are grouped in the end of the list) and then by
|
||||||
|
* series number:
|
||||||
|
*
|
||||||
|
* --------
|
||||||
|
* | CT #3 |
|
||||||
|
* | CT #4 |
|
||||||
|
* | CT #5 |
|
||||||
|
* --------
|
||||||
|
* | SEG #1 |
|
||||||
|
* | SEG #2 |
|
||||||
|
* --------
|
||||||
*
|
*
|
||||||
* @param {*} a - DisplaySet
|
* @param {*} a - DisplaySet
|
||||||
* @param {*} b - DisplaySet
|
* @param {*} b - DisplaySet
|
||||||
*/
|
*/
|
||||||
function _sortBySeriesNumber(a, b) {
|
|
||||||
|
function seriesSortingCriteria(a, b) {
|
||||||
|
const isLowPriorityA = isLowPriorityModality(a.modality);
|
||||||
|
const isLowPriorityB = isLowPriorityModality(b.modality);
|
||||||
|
if (!isLowPriorityA && isLowPriorityB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (isLowPriorityA && !isLowPriorityB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return sortBySeriesNumber(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort series by series number. Series with low
|
||||||
|
* @param {*} a - DisplaySet
|
||||||
|
* @param {*} b - DisplaySet
|
||||||
|
*/
|
||||||
|
function sortBySeriesNumber(a, b) {
|
||||||
const seriesNumberAIsGreaterOrUndefined =
|
const seriesNumberAIsGreaterOrUndefined =
|
||||||
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
|
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
|
||||||
|
|
||||||
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
|
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts a list of display set objects
|
||||||
|
* @param {Array} list A list of display sets to be sorted
|
||||||
|
*/
|
||||||
|
function sortDisplaySetList(list) {
|
||||||
|
return list.sort(seriesSortingCriteria);
|
||||||
|
}
|
||||||
@@ -504,12 +504,12 @@ export default class ProtocolEngine {
|
|||||||
/**
|
/**
|
||||||
* Sets the current layout
|
* Sets the current layout
|
||||||
*
|
*
|
||||||
* @param rows
|
* @param {number} numRows
|
||||||
* @param columns
|
* @param {number} numColumns
|
||||||
*/
|
*/
|
||||||
setLayout(rows, columns) {
|
setLayout(numRows, numColumns) {
|
||||||
if (rows < 1 && columns < 1) {
|
if (numRows < 1 && numColumns < 1) {
|
||||||
log.error(`Invalid layout ${rows} x ${columns}`);
|
log.error(`Invalid layout ${numRows} x ${numColumns}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,16 +519,13 @@ export default class ProtocolEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let viewports = [];
|
let viewports = [];
|
||||||
const numViewports = rows * columns;
|
const numViewports = numRows * numColumns;
|
||||||
|
|
||||||
for (let i = 0; i < numViewports; i++) {
|
for (let i = 0; i < numViewports; i++) {
|
||||||
viewports.push({
|
viewports.push({});
|
||||||
height: `${100 / rows}%`,
|
|
||||||
width: `${100 / columns}%`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options.setLayout({ viewports });
|
this.options.setLayout({ numRows, numColumns, viewports });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -634,14 +631,10 @@ export default class ProtocolEngine {
|
|||||||
//console.log('renderedCallback for ' + element.id);
|
//console.log('renderedCallback for ' + element.id);
|
||||||
customSettings.forEach(customSetting => {
|
customSettings.forEach(customSetting => {
|
||||||
log.trace(
|
log.trace(
|
||||||
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${
|
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
|
||||||
customSetting.id
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
log.trace(
|
log.trace(
|
||||||
`ProtocolEngine::currentViewportData.renderedCallback with value: ${
|
`ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
|
||||||
customSetting.value
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setting = CustomViewportSettings[customSetting.id];
|
const setting = CustomViewportSettings[customSetting.id];
|
||||||
|
|||||||
@@ -26,9 +26,34 @@ export const setViewportActive = viewportIndex => ({
|
|||||||
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,
|
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 => ({
|
export const clearViewportSpecificData = viewportIndex => ({
|
||||||
@@ -87,12 +112,6 @@ export const setServers = servers => ({
|
|||||||
servers,
|
servers,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setViewportLayoutAndData = (layout, viewportSpecificData) => ({
|
|
||||||
type: SET_VIEWPORT_LAYOUT_AND_DATA,
|
|
||||||
layout,
|
|
||||||
viewportSpecificData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
// VIEWPORT
|
// VIEWPORT
|
||||||
setViewportActive,
|
setViewportActive,
|
||||||
|
|||||||
@@ -94,19 +94,20 @@ describe('actions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create an action to set the viewport layout', () => {
|
it('should create an action to set the viewport layout', () => {
|
||||||
const layout = {
|
const numRows = 1;
|
||||||
viewports: [
|
const numColumns = 2;
|
||||||
{
|
const viewports = [{ plugin: 'vtk' }, { plugin: 'vtk' }];
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const expectedAction = {
|
const expectedAction = {
|
||||||
type: types.SET_VIEWPORT_LAYOUT,
|
type: types.SET_VIEWPORT_LAYOUT,
|
||||||
layout,
|
numRows,
|
||||||
|
numColumns,
|
||||||
|
viewports,
|
||||||
};
|
};
|
||||||
expect(actions.setLayout(layout)).toEqual(expectedAction);
|
|
||||||
|
expect(actions.setLayout({ numRows, numColumns, viewports })).toEqual(
|
||||||
|
expectedAction
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import actions from './actions.js';
|
import actions from './actions.js';
|
||||||
import reducers from './reducers';
|
import reducers from './reducers';
|
||||||
import localStorage from './localStorage.js';
|
import localStorage from './localStorage.js';
|
||||||
|
import sessionStorage from './sessionStorage.js';
|
||||||
|
|
||||||
const redux = {
|
const redux = {
|
||||||
reducers,
|
reducers,
|
||||||
actions,
|
actions,
|
||||||
localStorage,
|
localStorage,
|
||||||
|
sessionStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default redux;
|
export default redux;
|
||||||
@@ -2,7 +2,7 @@ import redux from './index.js';
|
|||||||
|
|
||||||
describe('redux exports', () => {
|
describe('redux exports', () => {
|
||||||
test('have not changed', () => {
|
test('have not changed', () => {
|
||||||
const expectedExports = ['actions', 'reducers', 'localStorage'].sort();
|
const expectedExports = ['actions', 'reducers', 'localStorage', 'sessionStorage'].sort();
|
||||||
|
|
||||||
const exports = Object.keys(redux).sort();
|
const exports = Object.keys(redux).sort();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
const LocalStorageApi = window.localStorage;
|
||||||
|
const localStorageKey = 'state';
|
||||||
export const loadState = () => {
|
export const loadState = () => {
|
||||||
try {
|
try {
|
||||||
const serializedState = window.localStorage.getItem('state');
|
const serializedState = LocalStorageApi.getItem(localStorageKey);
|
||||||
if (!serializedState) {
|
if (!serializedState) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -14,7 +16,7 @@ export const loadState = () => {
|
|||||||
export const saveState = state => {
|
export const saveState = state => {
|
||||||
try {
|
try {
|
||||||
const serializedState = JSON.stringify(state);
|
const serializedState = JSON.stringify(state);
|
||||||
localStorage.setItem('state', serializedState);
|
LocalStorageApi.setItem(localStorageKey, serializedState);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import cloneDeep from 'lodash.clonedeep';
|
|||||||
import merge from 'lodash.merge';
|
import merge from 'lodash.merge';
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
numRows: 1,
|
||||||
|
numColumns: 1,
|
||||||
activeViewportIndex: 0,
|
activeViewportIndex: 0,
|
||||||
layout: {
|
layout: {
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
// plugin: 'cornerstone',
|
// plugin: 'cornerstone',
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -34,21 +34,34 @@ const defaultState = {
|
|||||||
* @param {Object} [action.viewportSpecificData]
|
* @param {Object} [action.viewportSpecificData]
|
||||||
*/
|
*/
|
||||||
const viewports = (state = defaultState, action) => {
|
const viewports = (state = defaultState, action) => {
|
||||||
let currentData;
|
|
||||||
let viewportSpecificData;
|
let viewportSpecificData;
|
||||||
let useActiveViewport = false;
|
let useActiveViewport = false;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SET_VIEWPORT_LAYOUT_AND_DATA:
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
viewportSpecificData: action.viewportSpecificData,
|
|
||||||
layout: action.layout,
|
|
||||||
});
|
|
||||||
case SET_VIEWPORT_ACTIVE:
|
case SET_VIEWPORT_ACTIVE:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
activeViewportIndex: action.viewportIndex,
|
activeViewportIndex: action.viewportIndex,
|
||||||
});
|
});
|
||||||
case SET_VIEWPORT_LAYOUT:
|
case SET_VIEWPORT_LAYOUT: {
|
||||||
return Object.assign({}, state, { layout: action.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: {
|
case SET_VIEWPORT: {
|
||||||
const layout = cloneDeep(state.layout);
|
const layout = cloneDeep(state.layout);
|
||||||
const hasPlugin = action.data && action.data.plugin;
|
const hasPlugin = action.data && action.data.plugin;
|
||||||
@@ -71,14 +84,24 @@ const viewports = (state = defaultState, action) => {
|
|||||||
// Allow fall-through
|
// Allow fall-through
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
case SET_SPECIFIC_DATA: {
|
case SET_SPECIFIC_DATA: {
|
||||||
|
const layout = cloneDeep(state.layout);
|
||||||
|
const hasPlugin = action.data && action.data.plugin;
|
||||||
const viewportIndex = useActiveViewport
|
const viewportIndex = useActiveViewport
|
||||||
? state.activeViewportIndex
|
? state.activeViewportIndex
|
||||||
: action.viewportIndex;
|
: action.viewportIndex;
|
||||||
currentData = cloneDeep(state.viewportSpecificData[viewportIndex]) || {};
|
const { dom } = state.viewportSpecificData[viewportIndex];
|
||||||
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
|
||||||
viewportSpecificData[viewportIndex] = merge({}, currentData, action.data);
|
|
||||||
|
|
||||||
return Object.assign({}, state, { viewportSpecificData });
|
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||||
|
viewportSpecificData[viewportIndex] = {
|
||||||
|
dom,
|
||||||
|
...action.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasPlugin) {
|
||||||
|
layout.viewports[viewportIndex].plugin = action.data.plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign({}, state, { layout, viewportSpecificData });
|
||||||
}
|
}
|
||||||
case CLEAR_VIEWPORT:
|
case CLEAR_VIEWPORT:
|
||||||
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||||
|
|||||||
@@ -6,13 +6,10 @@ describe('viewports reducer', () => {
|
|||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual({
|
expect(reducer(undefined, {})).toEqual({
|
||||||
activeViewportIndex: 0,
|
activeViewportIndex: 0,
|
||||||
|
numRows: 1,
|
||||||
|
numColumns: 1,
|
||||||
layout: {
|
layout: {
|
||||||
viewports: [
|
viewports: [{}],
|
||||||
{
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
viewportSpecificData: {},
|
viewportSpecificData: {},
|
||||||
});
|
});
|
||||||
@@ -34,23 +31,25 @@ describe('viewports reducer', () => {
|
|||||||
it('should handle SET_VIEWPORT_LAYOUT', () => {
|
it('should handle SET_VIEWPORT_LAYOUT', () => {
|
||||||
const setViewportLayoutAction = {
|
const setViewportLayoutAction = {
|
||||||
type: types.SET_VIEWPORT_LAYOUT,
|
type: types.SET_VIEWPORT_LAYOUT,
|
||||||
layout: {
|
numRows: 1,
|
||||||
viewports: [
|
numColumns: 2,
|
||||||
{
|
viewports: [
|
||||||
height: '100%',
|
{
|
||||||
width: '50%',
|
plugin: 'cornerstone',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
height: '100%',
|
plugin: 'vtk',
|
||||||
width: '50%',
|
},
|
||||||
},
|
],
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedState = reducer({}, setViewportLayoutAction);
|
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
|
// If there were previous keys, this would have
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
const SessionStorageApi = window.sessionStorage;
|
||||||
|
const sessionStorageKey = 'state';
|
||||||
|
export const loadState = () => {
|
||||||
|
try {
|
||||||
|
const serializedState = SessionStorageApi.getItem(sessionStorageKey);
|
||||||
|
if (!serializedState) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(serializedState);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveState = state => {
|
||||||
|
try {
|
||||||
|
const serializedState = JSON.stringify(state);
|
||||||
|
SessionStorageApi.setItem(sessionStorageKey, serializedState);
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionStorage = {
|
||||||
|
saveState,
|
||||||
|
loadState,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sessionStorage;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import RetrieveMetadata from './services/wado/retrieveMetadata.js';
|
import RetrieveMetadata from './services/wado/retrieveMetadata.js';
|
||||||
|
|
||||||
const module = 'RetrieveStudyMetadata';
|
const moduleName = 'RetrieveStudyMetadata';
|
||||||
// Cache for promises. Prevents unnecessary subsequent calls to the server
|
// Cache for promises. Prevents unnecessary subsequent calls to the server
|
||||||
const StudyMetaDataPromises = new Map();
|
const StudyMetaDataPromises = new Map();
|
||||||
|
|
||||||
@@ -17,11 +17,11 @@ export function retrieveStudyMetadata(server, studyInstanceUid) {
|
|||||||
// corresponding promise from the "StudyMetaDataPromises" map...
|
// corresponding promise from the "StudyMetaDataPromises" map...
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
throw new Error(`${module}: Required 'server' parameter not provided.`);
|
throw new Error(`${moduleName}: Required 'server' parameter not provided.`);
|
||||||
}
|
}
|
||||||
if (!studyInstanceUid) {
|
if (!studyInstanceUid) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${module}: Required 'studyInstanceUid' parameter not provided.`
|
`${moduleName}: Required 'studyInstanceUid' parameter not provided.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import DICOMWeb from '../../../DICOMWeb/';
|
import DICOMWeb from '../../../DICOMWeb/';
|
||||||
|
import isLowPriorityModality from '../../../utils/isLowPriorityModality';
|
||||||
|
|
||||||
|
const INFO = Symbol('INFO');
|
||||||
|
|
||||||
const WADOProxy = {
|
const WADOProxy = {
|
||||||
convertURL: (url, server) => {
|
convertURL: (url, server) => {
|
||||||
@@ -94,9 +97,7 @@ function buildInstanceWadoRsUri(
|
|||||||
seriesInstanceUid,
|
seriesInstanceUid,
|
||||||
sopInstanceUid
|
sopInstanceUid
|
||||||
) {
|
) {
|
||||||
return `${
|
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
||||||
server.wadoRoot
|
|
||||||
}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInstanceFrameWadoRsUri(
|
function buildInstanceFrameWadoRsUri(
|
||||||
@@ -133,7 +134,8 @@ function getSourceImageInstanceUid(instance) {
|
|||||||
if (
|
if (
|
||||||
SourceImageSequence &&
|
SourceImageSequence &&
|
||||||
SourceImageSequence.Value &&
|
SourceImageSequence.Value &&
|
||||||
SourceImageSequence.Value.length
|
SourceImageSequence.Value.length &&
|
||||||
|
SourceImageSequence.Value[0]['00081155'].Value
|
||||||
) {
|
) {
|
||||||
return SourceImageSequence.Value[0]['00081155'].Value[0];
|
return SourceImageSequence.Value[0]['00081155'].Value[0];
|
||||||
}
|
}
|
||||||
@@ -165,9 +167,9 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const arrayBufferToPaletteColorLUT = result => {
|
const arrayBufferToPaletteColorLUT = result => {
|
||||||
const arraybuffer = result[0]
|
const arraybuffer = result[0];
|
||||||
const byteArray = new Uint8Array(arraybuffer)
|
const byteArray = new Uint8Array(arraybuffer);
|
||||||
const lut = []
|
const lut = [];
|
||||||
|
|
||||||
for (let i = 0; i < numLutEntries; i++) {
|
for (let i = 0; i < numLutEntries; i++) {
|
||||||
if (bits === 16) {
|
if (bits === 16) {
|
||||||
@@ -270,200 +272,338 @@ function getRadiopharmaceuticalInfo(instance) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function makeSOPInstance(server, study, instance) {
|
||||||
|
const { studyInstanceUid } = study;
|
||||||
|
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||||
|
let series = study.seriesMap[seriesInstanceUid];
|
||||||
|
|
||||||
|
if (!series) {
|
||||||
|
series = {
|
||||||
|
seriesInstanceUid,
|
||||||
|
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||||
|
modality: DICOMWeb.getString(instance['00080060']),
|
||||||
|
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
||||||
|
seriesDate: DICOMWeb.getString(instance['00080021']),
|
||||||
|
seriesTime: DICOMWeb.getString(instance['00080031']),
|
||||||
|
instances: [],
|
||||||
|
};
|
||||||
|
study.seriesMap[seriesInstanceUid] = series;
|
||||||
|
study.seriesList.push(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||||
|
const wadouri = buildInstanceWadoUrl(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
const baseWadoRsUri = buildInstanceWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
const wadorsuri = buildInstanceFrameWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
|
||||||
|
const sopInstance = {
|
||||||
|
imageType: DICOMWeb.getString(instance['00080008']),
|
||||||
|
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||||
|
modality: DICOMWeb.getString(instance['00080060']),
|
||||||
|
sopInstanceUid,
|
||||||
|
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||||
|
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||||
|
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||||
|
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||||
|
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||||
|
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||||
|
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||||
|
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
|
||||||
|
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||||
|
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||||
|
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||||
|
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
|
||||||
|
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||||
|
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||||
|
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||||
|
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||||
|
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
|
||||||
|
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
|
||||||
|
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||||
|
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||||
|
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||||
|
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||||
|
rescaleType: DICOMWeb.getNumber(instance['00281054']),
|
||||||
|
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||||
|
laterality: DICOMWeb.getString(instance['00200062']),
|
||||||
|
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||||
|
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
|
||||||
|
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
|
||||||
|
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
|
||||||
|
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
||||||
|
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
|
||||||
|
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
||||||
|
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
|
||||||
|
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
||||||
|
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
||||||
|
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
||||||
|
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
||||||
|
echoNumber: DICOMWeb.getString(instance['00180086']),
|
||||||
|
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
|
||||||
|
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
|
||||||
|
baseWadoRsUri: baseWadoRsUri,
|
||||||
|
wadouri: WADOProxy.convertURL(wadouri, server),
|
||||||
|
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
|
||||||
|
wadoRoot: server.wadoRoot,
|
||||||
|
imageRendering: server.imageRendering,
|
||||||
|
thumbnailRendering: server.thumbnailRendering,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
||||||
|
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
|
||||||
|
const redPaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281101'])
|
||||||
|
);
|
||||||
|
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281102'])
|
||||||
|
);
|
||||||
|
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281103'])
|
||||||
|
);
|
||||||
|
const palettes = await getPaletteColors(
|
||||||
|
server,
|
||||||
|
instance,
|
||||||
|
redPaletteColorLookupTableDescriptor
|
||||||
|
);
|
||||||
|
|
||||||
|
if (palettes) {
|
||||||
|
if (palettes.uid) {
|
||||||
|
sopInstance.paletteColorLookupTableUID = palettes.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
sopInstance.redPaletteColorLookupTableData = palettes.red;
|
||||||
|
sopInstance.greenPaletteColorLookupTableData = palettes.green;
|
||||||
|
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
|
||||||
|
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
|
||||||
|
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
|
||||||
|
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
series.instances.push(sopInstance);
|
||||||
|
return sopInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a plain JS object that describes a study (a study descriptor object)
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {Object} aSopInstance a SOP Instance from which study information will be added
|
||||||
|
*/
|
||||||
|
function createStudy(server, aSopInstance) {
|
||||||
|
// TODO: Pass a reference ID to the server instead of including the URLs here
|
||||||
|
return {
|
||||||
|
seriesList: [],
|
||||||
|
seriesMap: Object.create(null),
|
||||||
|
seriesLoader: null,
|
||||||
|
wadoUriRoot: server.wadoUriRoot,
|
||||||
|
wadoRoot: server.wadoRoot,
|
||||||
|
qidoRoot: server.qidoRoot,
|
||||||
|
patientName: DICOMWeb.getName(aSopInstance['00100010']),
|
||||||
|
patientId: DICOMWeb.getString(aSopInstance['00100020']),
|
||||||
|
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
|
||||||
|
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
|
||||||
|
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
|
||||||
|
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
|
||||||
|
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
|
||||||
|
modalities: DICOMWeb.getString(aSopInstance['00080061']),
|
||||||
|
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
|
||||||
|
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
|
||||||
|
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
|
||||||
|
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a list of SOP Instances to a given study object descriptor
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {Object} study The study descriptor to which the given SOP instances will be added
|
||||||
|
* @param {Array} sopInstanceList A list of SOP instance objects
|
||||||
|
*/
|
||||||
|
async function addInstancesToStudy(server, study, sopInstanceList) {
|
||||||
|
return Promise.all(
|
||||||
|
sopInstanceList.map(function(sopInstance) {
|
||||||
|
return makeSOPInstance(server, study, sopInstance);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses result data from a WADO search into Study MetaData
|
* Parses result data from a WADO search into Study MetaData
|
||||||
* Returns an object populated with study metadata, including the
|
* Returns an object populated with study metadata, including the
|
||||||
* series list.
|
* series list.
|
||||||
*
|
*
|
||||||
* @param server
|
* @param {Object} server Object with server configuration paramenters
|
||||||
* @param studyInstanceUid
|
* @param {Array} sopInstanceList List of SOP Instances that build up to the study
|
||||||
* @param resultData
|
* @resolves {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
|
||||||
*/
|
*/
|
||||||
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
async function createStudyFromSOPInstanceList(server, sopInstanceList) {
|
||||||
if (!resultData.length) {
|
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
|
||||||
return;
|
const firstSopInstance = sopInstanceList[0];
|
||||||
|
const study = createStudy(server, firstSopInstance);
|
||||||
|
await addInstancesToStudy(server, study, sopInstanceList);
|
||||||
|
return study;
|
||||||
}
|
}
|
||||||
|
throw new Error('Failed to create study out of provided SOP instance list');
|
||||||
const anInstance = resultData[0];
|
|
||||||
if (!anInstance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Pass a reference ID to the server instead of including the URLs here
|
|
||||||
const studyData = {
|
|
||||||
seriesList: [],
|
|
||||||
studyInstanceUid,
|
|
||||||
wadoUriRoot: server.wadoUriRoot,
|
|
||||||
wadoRoot: server.wadoRoot,
|
|
||||||
qidoRoot: server.qidoRoot,
|
|
||||||
patientName: DICOMWeb.getName(anInstance['00100010']),
|
|
||||||
patientId: DICOMWeb.getString(anInstance['00100020']),
|
|
||||||
patientAge: DICOMWeb.getNumber(anInstance['00101010']),
|
|
||||||
patientSize: DICOMWeb.getNumber(anInstance['00101020']),
|
|
||||||
patientWeight: DICOMWeb.getNumber(anInstance['00101030']),
|
|
||||||
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
|
|
||||||
studyDate: DICOMWeb.getString(anInstance['00080020']),
|
|
||||||
modalities: DICOMWeb.getString(anInstance['00080061']),
|
|
||||||
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
|
||||||
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
|
||||||
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
|
|
||||||
institutionName: DICOMWeb.getString(anInstance['00080080']),
|
|
||||||
};
|
|
||||||
|
|
||||||
const seriesMap = {};
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
resultData.map(async function(instance) {
|
|
||||||
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
|
||||||
let series = seriesMap[seriesInstanceUid];
|
|
||||||
|
|
||||||
if (!series) {
|
|
||||||
series = {
|
|
||||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
|
||||||
modality: DICOMWeb.getString(instance['00080060']),
|
|
||||||
seriesInstanceUid: seriesInstanceUid,
|
|
||||||
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
|
||||||
seriesDate: DICOMWeb.getString(instance['00080021']),
|
|
||||||
seriesTime: DICOMWeb.getString(instance['00080031']),
|
|
||||||
instances: [],
|
|
||||||
};
|
|
||||||
seriesMap[seriesInstanceUid] = series;
|
|
||||||
studyData.seriesList.push(series);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
|
||||||
const wadouri = buildInstanceWadoUrl(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
const baseWadoRsUri = buildInstanceWadoRsUri(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
const wadorsuri = buildInstanceFrameWadoRsUri(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
|
|
||||||
const instanceSummary = {
|
|
||||||
imageType: DICOMWeb.getString(instance['00080008']),
|
|
||||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
|
||||||
modality: DICOMWeb.getString(instance['00080060']),
|
|
||||||
sopInstanceUid,
|
|
||||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
|
||||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
|
||||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
|
||||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
|
||||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
|
||||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
|
||||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
|
||||||
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
|
|
||||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
|
||||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
|
||||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
|
||||||
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
|
|
||||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
|
||||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
|
||||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
|
||||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
|
||||||
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
|
|
||||||
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
|
|
||||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
|
||||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
|
||||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
|
||||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
|
||||||
rescaleType: DICOMWeb.getNumber(instance['00281054']),
|
|
||||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
|
||||||
laterality: DICOMWeb.getString(instance['00200062']),
|
|
||||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
|
||||||
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
|
|
||||||
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
|
|
||||||
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
|
|
||||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
|
||||||
frameTimeVector: parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00181065'])
|
|
||||||
),
|
|
||||||
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
|
||||||
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
|
|
||||||
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
|
||||||
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
|
||||||
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
|
||||||
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
|
||||||
echoNumber: DICOMWeb.getString(instance['00180086']),
|
|
||||||
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
|
|
||||||
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
|
|
||||||
baseWadoRsUri: baseWadoRsUri,
|
|
||||||
wadouri: WADOProxy.convertURL(wadouri, server),
|
|
||||||
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
|
|
||||||
imageRendering: server.imageRendering,
|
|
||||||
thumbnailRendering: server.thumbnailRendering,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
|
||||||
if (instanceSummary.photometricInterpretation === 'PALETTE COLOR') {
|
|
||||||
const redPaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281101'])
|
|
||||||
);
|
|
||||||
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281102'])
|
|
||||||
);
|
|
||||||
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281103'])
|
|
||||||
);
|
|
||||||
const palettes = await getPaletteColors(
|
|
||||||
server,
|
|
||||||
instance,
|
|
||||||
redPaletteColorLookupTableDescriptor
|
|
||||||
);
|
|
||||||
|
|
||||||
if (palettes) {
|
|
||||||
if (palettes.uid) {
|
|
||||||
instanceSummary.paletteColorLookupTableUID = palettes.uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceSummary.redPaletteColorLookupTableData = palettes.red;
|
|
||||||
instanceSummary.greenPaletteColorLookupTableData = palettes.green;
|
|
||||||
instanceSummary.bluePaletteColorLookupTableData = palettes.blue;
|
|
||||||
instanceSummary.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
|
|
||||||
instanceSummary.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
|
|
||||||
instanceSummary.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
series.instances.push(instanceSummary);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return studyData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve Study MetaData from a DICOM server using a WADO call
|
* Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series
|
||||||
*
|
* will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed
|
||||||
* @param server
|
* @param {Object} server Object with server configuration paramenters
|
||||||
* @param studyInstanceUid
|
* @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
|
||||||
* @returns {Promise}
|
* @returns {Object} A study descriptor object
|
||||||
*/
|
*/
|
||||||
async function RetrieveMetadata(server, studyInstanceUid) {
|
async function RetrieveMetadata(server, studyInstanceUid) {
|
||||||
const config = {
|
return (server.enableStudyLazyLoad !== false
|
||||||
|
? lazyLoadStudyMetadata
|
||||||
|
: loadStudyMetadata)(server, studyInstanceUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} server
|
||||||
|
* @param {*} studyInstanceUID
|
||||||
|
*/
|
||||||
|
async function loadStudyMetadata(server, studyInstanceUID) {
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
url: server.wadoRoot,
|
url: server.wadoRoot,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
};
|
});
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
return dicomWeb
|
||||||
const options = {
|
.retrieveStudyMetadata({ studyInstanceUID })
|
||||||
studyInstanceUID: studyInstanceUid,
|
.then(result => createStudyFromSOPInstanceList(server, result));
|
||||||
};
|
}
|
||||||
|
|
||||||
return dicomWeb.retrieveStudyMetadata(options).then(result => {
|
async function lazyLoadStudyMetadata(server, studyInstanceUid) {
|
||||||
return resultDataToStudyMetadata(server, studyInstanceUid, result);
|
const seriesInstanceUids = await searchStudySeries(server, studyInstanceUid);
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
|
url: server.wadoRoot,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
});
|
||||||
|
const seriesLoader = makeSeriesLoader(
|
||||||
|
dicomWeb,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUids
|
||||||
|
);
|
||||||
|
const firstSeries = await seriesLoader.next();
|
||||||
|
const study = await createStudyFromSOPInstanceList(
|
||||||
|
server,
|
||||||
|
firstSeries.sopInstances
|
||||||
|
);
|
||||||
|
if (seriesLoader.hasNext()) {
|
||||||
|
attachSeriesLoader(server, study, seriesLoader);
|
||||||
|
}
|
||||||
|
return study;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachSeriesLoader(server, study, seriesLoader) {
|
||||||
|
study.seriesLoader = Object.freeze({
|
||||||
|
hasNext() {
|
||||||
|
return seriesLoader.hasNext();
|
||||||
|
},
|
||||||
|
async next() {
|
||||||
|
const series = await seriesLoader.next();
|
||||||
|
await addInstancesToStudy(server, study, series.sopInstances);
|
||||||
|
return study.seriesMap[series.seriesInstanceUID];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
|
||||||
|
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||||
|
* @param {string} studyInstanceUID The Study Instance UID from which series will be loaded
|
||||||
|
* @param {Array} seriesInstanceUIDList A list of Series Instance UIDs
|
||||||
|
* @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID
|
||||||
|
*/
|
||||||
|
function makeSeriesLoader(
|
||||||
|
dicomWebClient,
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUIDList
|
||||||
|
) {
|
||||||
|
return Object.freeze({
|
||||||
|
hasNext() {
|
||||||
|
return seriesInstanceUIDList.length > 0;
|
||||||
|
},
|
||||||
|
async next() {
|
||||||
|
const seriesInstanceUID = seriesInstanceUIDList.shift();
|
||||||
|
const sopInstances = await dicomWebClient.retrieveSeriesMetadata({
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
});
|
||||||
|
return { studyInstanceUID, seriesInstanceUID, sopInstances };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search series of a given study
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {string} studyInstanceUID The Study Instance UID to search series from;
|
||||||
|
* @returns {Arrays} A list of Series Instance UIDs
|
||||||
|
*/
|
||||||
|
async function searchStudySeries(server, studyInstanceUID) {
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
|
url: server.qidoRoot,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
});
|
||||||
|
const seriesList = await dicomWeb.searchForSeries({ studyInstanceUID });
|
||||||
|
return seriesList
|
||||||
|
.sort(seriesSortingCriteria)
|
||||||
|
.map(series => getSeriesInfo(series).seriesInstanceUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Series sorting criteria: series considered low priority are moved to the end
|
||||||
|
* of the list and series number is used to break ties
|
||||||
|
* @param {Object} firstSeries
|
||||||
|
* @param {Object} secondSeries
|
||||||
|
*/
|
||||||
|
function seriesSortingCriteria(firstSeries, secondSeries) {
|
||||||
|
const a = getSeriesInfo(firstSeries);
|
||||||
|
const b = getSeriesInfo(secondSeries);
|
||||||
|
if (!a.isLowPriority && b.isLowPriority) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.isLowPriority && !b.isLowPriority) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return a.seriesNumber - b.seriesNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an object with processed series information and saves its reference
|
||||||
|
* inside the series object itself to simplify sorting
|
||||||
|
* @param {Object} series The raw series object
|
||||||
|
*/
|
||||||
|
function getSeriesInfo(series) {
|
||||||
|
let info = series[INFO];
|
||||||
|
if (!info) {
|
||||||
|
const modality = DICOMWeb.getString(series['00080060'], '').toUpperCase();
|
||||||
|
info = Object.freeze({
|
||||||
|
modality,
|
||||||
|
isLowPriority: isLowPriorityModality(modality),
|
||||||
|
seriesInstanceUid: DICOMWeb.getString(series['0020000E']),
|
||||||
|
seriesNumber: DICOMWeb.getNumber(series['00200011'], 0) || 0,
|
||||||
|
});
|
||||||
|
series[INFO] = info;
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
export default RetrieveMetadata;
|
export default RetrieveMetadata;
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
|
import { api } from 'dicomweb-client';
|
||||||
|
import DICOMWeb from '../DICOMWeb';
|
||||||
|
|
||||||
|
const getImageId = imageObj => {
|
||||||
|
if (!imageObj) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof imageObj.getImageId === 'function'
|
||||||
|
? imageObj.getImageId()
|
||||||
|
: imageObj.url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findImageIdOnStudies = (studies, displaySetInstanceUid) => {
|
||||||
|
const study = studies.find(study => {
|
||||||
|
const displaySet = study.displaySets.some(
|
||||||
|
displaySet => displaySet.displaySetInstanceUid === displaySetInstanceUid
|
||||||
|
);
|
||||||
|
return displaySet;
|
||||||
|
});
|
||||||
|
const { seriesList = [] } = study;
|
||||||
|
const { instances = [] } = seriesList[0] || {};
|
||||||
|
const instance = instances[0];
|
||||||
|
|
||||||
|
return getImageId(instance);
|
||||||
|
};
|
||||||
|
|
||||||
|
const someInvalidStrings = strings => {
|
||||||
|
const stringsArray = Array.isArray(strings) ? strings : [strings];
|
||||||
|
const emptyString = string => !string;
|
||||||
|
let invalid = stringsArray.some(emptyString);
|
||||||
|
return invalid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageInstance = dataset => {
|
||||||
|
return dataset && dataset.images && dataset.images[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageInstanceId = imageInstance => {
|
||||||
|
return getImageId(imageInstance);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchIt = (url, headers = DICOMWeb.getAuthorizationHeader()) => {
|
||||||
|
return fetch(url, headers).then(response => response.arrayBuffer());
|
||||||
|
};
|
||||||
|
|
||||||
|
const cornerstoneRetriever = imageId => {
|
||||||
|
return cornerstone.loadAndCacheImage(imageId).then(image => {
|
||||||
|
return image && image.data && image.data.byteArray.buffer;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const wadorsRetriever = (
|
||||||
|
url,
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
sopInstanceUID,
|
||||||
|
headers = DICOMWeb.getAuthorizationHeader()
|
||||||
|
) => {
|
||||||
|
const config = {
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
|
|
||||||
|
return dicomWeb.retrieveInstance({
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
sopInstanceUID,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageLoaderType = imageId => {
|
||||||
|
const loaderRegExp = /^\w+\:/;
|
||||||
|
const loaderType = loaderRegExp.exec(imageId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
(loaderRegExp.lastIndex === 0 &&
|
||||||
|
loaderType &&
|
||||||
|
loaderType[0] &&
|
||||||
|
loaderType[0].replace(':', '')) ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DicomLoaderService = new (class {
|
||||||
|
getLocalData(dataset, studies) {
|
||||||
|
if (dataset && dataset.localFile) {
|
||||||
|
// Use referenced imageInstance
|
||||||
|
const imageInstance = getImageInstance(dataset);
|
||||||
|
let imageId = getImageInstanceId(imageInstance);
|
||||||
|
|
||||||
|
// or Try to get it from studies
|
||||||
|
if (someInvalidStrings(imageId)) {
|
||||||
|
imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!someInvalidStrings(imageId)) {
|
||||||
|
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataByImageType(dataset) {
|
||||||
|
const imageInstance = getImageInstance(dataset);
|
||||||
|
|
||||||
|
if (imageInstance) {
|
||||||
|
const imageId = getImageInstanceId(imageInstance);
|
||||||
|
let getDicomDataMethod = fetchIt;
|
||||||
|
const loaderType = getImageLoaderType(imageId);
|
||||||
|
|
||||||
|
switch (loaderType) {
|
||||||
|
case 'dicomfile':
|
||||||
|
getDicomDataMethod = cornerstoneRetriever.bind(this, imageId);
|
||||||
|
break;
|
||||||
|
case 'wadors':
|
||||||
|
const url = imageInstance.getData().wadoRoot;
|
||||||
|
const studyInstanceUID = imageInstance.getStudyInstanceUID();
|
||||||
|
const seriesInstanceUID = imageInstance.getSeriesInstanceUID();
|
||||||
|
const sopInstanceUID = imageInstance.getSOPInstanceUID();
|
||||||
|
const invalidParams = someInvalidStrings([
|
||||||
|
url,
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
sopInstanceUID,
|
||||||
|
]);
|
||||||
|
if (invalidParams) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDicomDataMethod = wadorsRetriever.bind(
|
||||||
|
this,
|
||||||
|
url,
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
sopInstanceUID
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'wadouri':
|
||||||
|
// Strip out the image loader specifier
|
||||||
|
imageId = imageId.substring(imageId.indexOf(':') + 1);
|
||||||
|
|
||||||
|
if (someInvalidStrings(imageId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getDicomDataMethod = fetchIt.bind(this, imageId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDicomDataMethod();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataByDatasetType(dataset) {
|
||||||
|
const {
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid,
|
||||||
|
authorizationHeaders,
|
||||||
|
wadoRoot,
|
||||||
|
wadoUri,
|
||||||
|
} = dataset;
|
||||||
|
// Retrieve wadors or just try to fetch wadouri
|
||||||
|
if (!someInvalidStrings(wadoRoot)) {
|
||||||
|
return wadorsRetriever(
|
||||||
|
wadoRoot,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid,
|
||||||
|
authorizationHeaders
|
||||||
|
);
|
||||||
|
} else if (!someInvalidStrings(wadoUri)) {
|
||||||
|
return fetchIt(wadoUri, { headers: authorizationHeaders });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*getLoaderIterator(dataset, studies) {
|
||||||
|
yield this.getLocalData(dataset, studies);
|
||||||
|
yield this.getDataByImageType(dataset);
|
||||||
|
yield this.getDataByDatasetType(dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
findDicomDataPromise(dataset, studies) {
|
||||||
|
const loaderIterator = this.getLoaderIterator(dataset, studies);
|
||||||
|
// it returns first valid retriever method.
|
||||||
|
for (const loader of loaderIterator) {
|
||||||
|
if (loader) {
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case of no valid loader
|
||||||
|
throw new Error('Invalid dicom data loader');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default DicomLoaderService;
|
||||||
@@ -5,11 +5,16 @@ function getWADORSImageUrl(instance, frame) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to sum 1 because WADO-RS frame number is 1-based
|
// Use null to obtain an imageId which represents the instance
|
||||||
frame = frame ? parseInt(frame) + 1 : 1;
|
if (frame === null) {
|
||||||
|
wadorsuri = wadorsuri.replace(/frames\/(\d+)/, '');
|
||||||
|
} else {
|
||||||
|
// We need to sum 1 because WADO-RS frame number is 1-based
|
||||||
|
frame = frame ? parseInt(frame) + 1 : 1;
|
||||||
|
|
||||||
// Replaces /frame/1 by /frame/{frame}
|
// Replaces /frame/1 by /frame/{frame}
|
||||||
wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`);
|
wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`);
|
||||||
|
}
|
||||||
|
|
||||||
return wadorsuri;
|
return wadorsuri;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import sortBy from './sortBy.js';
|
|||||||
import studyMetadataManager from './studyMetadataManager';
|
import studyMetadataManager from './studyMetadataManager';
|
||||||
import updateMetaDataManager from './updateMetaDataManager.js';
|
import updateMetaDataManager from './updateMetaDataManager.js';
|
||||||
import writeScript from './writeScript.js';
|
import writeScript from './writeScript.js';
|
||||||
|
import DicomLoaderService from './dicomLoaderService.js';
|
||||||
|
import * as urlUtil from './urlUtil';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
guid,
|
guid,
|
||||||
@@ -21,6 +23,8 @@ const utils = {
|
|||||||
// Updates WADO-RS metaDataManager
|
// Updates WADO-RS metaDataManager
|
||||||
updateMetaDataManager,
|
updateMetaDataManager,
|
||||||
DICOMTagDescriptions,
|
DICOMTagDescriptions,
|
||||||
|
DicomLoaderService,
|
||||||
|
urlUtil,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -35,6 +39,8 @@ export {
|
|||||||
// Updates WADO-RS metaDataManager
|
// Updates WADO-RS metaDataManager
|
||||||
updateMetaDataManager,
|
updateMetaDataManager,
|
||||||
DICOMTagDescriptions,
|
DICOMTagDescriptions,
|
||||||
|
DicomLoaderService,
|
||||||
|
urlUtil,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default utils;
|
export default utils;
|
||||||
@@ -14,6 +14,8 @@ describe('Top level exports', () => {
|
|||||||
// Updates WADO-RS metaDataManager
|
// Updates WADO-RS metaDataManager
|
||||||
'updateMetaDataManager',
|
'updateMetaDataManager',
|
||||||
'DICOMTagDescriptions',
|
'DICOMTagDescriptions',
|
||||||
|
'DicomLoaderService',
|
||||||
|
'urlUtil'
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const exports = Object.keys(utils.default).sort();
|
const exports = Object.keys(utils.default).sort();
|
||||||
|
|||||||
@@ -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',
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR']);
|
||||||
|
|
||||||
|
export default function isLowPriorityModality(modality) {
|
||||||
|
return LOW_PRIORITY_MODALITIES.includes(modality);
|
||||||
|
}
|
||||||
@@ -70,32 +70,48 @@ const getWadoRsInstanceMetaData = (study, series, instance) => {
|
|||||||
.toJSON();
|
.toJSON();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function updateMetaDataManager(study) {
|
/**
|
||||||
study.seriesList.forEach(series => {
|
* Update metadata manager with instances of a specifc series
|
||||||
series.instances.forEach(instance => {
|
* @param {Object} study A plain study descriptor object
|
||||||
// Cache just images that are going to be loaded via WADO-RS
|
* @param {Object} series A Series descriptor object contaning the instances to be added to the manager
|
||||||
if (
|
*/
|
||||||
instance.imageRendering !== 'wadors' &&
|
function updateMetaDataManagerForSeries(study, series) {
|
||||||
instance.thumbnailRendering !== 'wadors'
|
series.instances.forEach(instance => {
|
||||||
) {
|
// Cache just images that are going to be loaded via WADO-RS
|
||||||
return;
|
if (
|
||||||
}
|
instance.imageRendering !== 'wadors' &&
|
||||||
|
instance.thumbnailRendering !== 'wadors'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const metaData = getWadoRsInstanceMetaData(study, series, instance);
|
const metaData = getWadoRsInstanceMetaData(study, series, instance);
|
||||||
const numberOfFrames = instance.numberOfFrames || 1;
|
const numberOfFrames = instance.numberOfFrames || 1;
|
||||||
|
|
||||||
// We can share the same metaData with all frames because it doesn't have
|
// We can share the same metaData with all frames because it doesn't have
|
||||||
// any frame specific data, such as frameNumber, pixelData, offset, etc.
|
// any frame specific data, such as frameNumber, pixelData, offset, etc.
|
||||||
// WADO-RS frame number is 1-based
|
// WADO-RS frame number is 1-based
|
||||||
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
|
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
|
||||||
const imageId = getWADORSImageId(instance, frameNumber);
|
const imageId = getWADORSImageId(instance, frameNumber);
|
||||||
|
|
||||||
// TODO Drop dependency on this
|
// TODO Drop dependency on this
|
||||||
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
|
cornerstoneWADOImageLoader.wadors.metaDataManager.add(imageId, metaData);
|
||||||
imageId,
|
}
|
||||||
metaData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update metadata manager
|
||||||
|
* @param {Object} study A plain study descriptor object
|
||||||
|
* @param {string} [seriesInstanceUid] The Series Instance UID of the series to be added (Optional)
|
||||||
|
*/
|
||||||
|
export default function updateMetaDataManager(study, seriesInstanceUid) {
|
||||||
|
if (seriesInstanceUid) {
|
||||||
|
const series = study.seriesMap[seriesInstanceUid];
|
||||||
|
updateMetaDataManagerForSeries(study, series);
|
||||||
|
} else {
|
||||||
|
study.seriesList.forEach(series => {
|
||||||
|
updateMetaDataManagerForSeries(study, series);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import lib from 'query-string';
|
||||||
|
|
||||||
|
const PARAM_SEPARATOR = ';';
|
||||||
|
const PARAM_PATTERN_IDENTIFIER = ':';
|
||||||
|
|
||||||
|
function toLowerCaseFirstLetter(word) {
|
||||||
|
return word[0].toLowerCase() + word.slice(1);
|
||||||
|
}
|
||||||
|
const getQueryFilters = (location = {}) => {
|
||||||
|
const { search } = location;
|
||||||
|
|
||||||
|
if (!search) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParameters = parse(search);
|
||||||
|
const filters = {};
|
||||||
|
|
||||||
|
Object.entries(searchParameters).forEach(([key, value]) => {
|
||||||
|
filters[toLowerCaseFirstLetter(key)] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decode = (strToDecode = '') => {
|
||||||
|
try {
|
||||||
|
const decoded = window.atob(strToDecode);
|
||||||
|
return decoded;
|
||||||
|
} catch (e) {
|
||||||
|
return strToDecode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parse = toParse => {
|
||||||
|
if (toParse) {
|
||||||
|
return lib.parse(toParse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
const parseParam = paramStr => {
|
||||||
|
const _paramDecoded = decode(paramStr);
|
||||||
|
if (_paramDecoded && typeof _paramDecoded === 'string') {
|
||||||
|
return _paramDecoded.split(PARAM_SEPARATOR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceParam = (path = '', paramKey, paramValue) => {
|
||||||
|
const paramPattern = `${PARAM_PATTERN_IDENTIFIER}${paramKey}`;
|
||||||
|
if (paramValue) {
|
||||||
|
return path.replace(paramPattern, paramValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidPath = path => {
|
||||||
|
const paramPatternPiece = `/${PARAM_PATTERN_IDENTIFIER}`;
|
||||||
|
return path.indexOf(paramPatternPiece) < 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryString = {
|
||||||
|
getQueryFilters,
|
||||||
|
};
|
||||||
|
|
||||||
|
const paramString = {
|
||||||
|
isValidPath,
|
||||||
|
parseParam,
|
||||||
|
replaceParam,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { parse, queryString, paramString };
|
||||||
@@ -3,6 +3,28 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [0.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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* CineDialog buttons label ([#998](https://github.com/OHIF/Viewers/issues/998)) ([4df624b](https://github.com/OHIF/Viewers/commit/4df624b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.3...@ohif/i18n@0.50.4) (2019-09-10)
|
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.3...@ohif/i18n@0.50.4) (2019-09-10)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/i18n
|
**Note:** Version bump only for package @ohif/i18n
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/i18n",
|
"name": "@ohif/i18n",
|
||||||
"version": "0.50.4",
|
"version": "0.51.0",
|
||||||
"description": "Internationalization library for The OHIF Viewer",
|
"description": "Internationalization library for The OHIF Viewer",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"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"
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Next image": "$t(Common:Play) $t(Common:Image)",
|
"Next image": "$t(Common:Next) $t(Common:Image)",
|
||||||
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||||
"Skip to first image": "Skip to first $t(Common:Image)",
|
"Skip to first image": "Skip to first $t(Common:Image)",
|
||||||
"Skip to last image": "Skip, to last $t(Common:Image)",
|
"Skip to last image": "Skip to last $t(Common:Image)",
|
||||||
"fps": "fps"
|
"fps": "fps"
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AboutModal from "./AboutModal.json";
|
||||||
import Buttons from "./Buttons.json";
|
import Buttons from "./Buttons.json";
|
||||||
import CineDialog from "./CineDialog.json";
|
import CineDialog from "./CineDialog.json";
|
||||||
import Common from "./Common.json";
|
import Common from "./Common.json";
|
||||||
@@ -8,6 +9,7 @@ import UserPreferencesModal from "./UserPreferencesModal.json";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
'en-US': {
|
'en-US': {
|
||||||
|
AboutModal,
|
||||||
Buttons,
|
Buttons,
|
||||||
CineDialog,
|
CineDialog,
|
||||||
Common,
|
Common,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Next image": " $t(Common:Image) $t(Common:Next)",
|
"Next image": "$t(Common:Image) $t(Common:Next)",
|
||||||
"Play / Stop": "$t(Common:Play) / Stop",
|
"Play / Stop": "$t(Common:Play) / Stop",
|
||||||
"Previous image": "$t(Common:Image) $t(Common:Previous)",
|
"Previous image": "$t(Common:Image) $t(Common:Previous)",
|
||||||
"Skip to first image": "Ir a la primera $t(Common:Image)",
|
"Skip to first image": "Ir a la primera $t(Common:Image)",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Next image": "$t(Common:Play) $t(Common:Image)",
|
"Next image": "$t(Common:Next) $t(Common:Image)",
|
||||||
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||||
"Skip to first image": "$t(Common:Image)最初にスキップ",
|
"Skip to first image": "$t(Common:Image)最初にスキップ",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AboutModal from "./AboutModal.json";
|
||||||
import Buttons from "./Buttons.json";
|
import Buttons from "./Buttons.json";
|
||||||
import CineDialog from "./CineDialog.json";
|
import CineDialog from "./CineDialog.json";
|
||||||
import Common from "./Common.json";
|
import Common from "./Common.json";
|
||||||
@@ -6,6 +7,7 @@ import UserPreferencesModal from "./UserPreferencesModal.json";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
'pt-BR': {
|
'pt-BR': {
|
||||||
|
AboutModal,
|
||||||
Buttons,
|
Buttons,
|
||||||
CineDialog,
|
CineDialog,
|
||||||
Common,
|
Common,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Next image": "$t(Common:Play) $t(Common:Image)",
|
"Next image": "$t(Common:Next) $t(Common:Image)",
|
||||||
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||||
"Skip to first image": "Bỏ qua đến đầu $t(Common:Image)",
|
"Skip to first image": "Bỏ qua đến đầu $t(Common:Image)",
|
||||||
|
|||||||
@@ -3,6 +3,140 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.56.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.56.0...@ohif/ui@0.56.1) (2019-10-26)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.56.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.55.0...@ohif/ui@0.56.0) (2019-10-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 Load spinner when selecting gcloud store. Add key on td ([#1034](https://github.com/OHIF/Viewers/issues/1034)) ([e62f403](https://github.com/OHIF/Viewers/commit/e62f403fe9e3df56713128e3d59045824b086d8d)), closes [#1057](https://github.com/OHIF/Viewers/issues/1057)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.55.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.54.0...@ohif/ui@0.55.0) (2019-10-15)
|
||||||
|
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🎸 switch ohif logo from text + font to SVG ([#1021](https://github.com/OHIF/Viewers/issues/1021)) ([e7de8be](https://github.com/OHIF/Viewers/commit/e7de8be))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.53.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.1...@ohif/ui@0.53.2) (2019-10-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.53.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.0...@ohif/ui@0.53.1) (2019-10-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Move Series Information to Separate Row ([#990](https://github.com/OHIF/Viewers/issues/990)) ([458d310](https://github.com/OHIF/Viewers/commit/458d310))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.52.0...@ohif/ui@0.53.0) (2019-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.3...@ohif/ui@0.52.0) (2019-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.2...@ohif/ui@0.51.3) (2019-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Address issues with touch devices and drag/drop causing crashes ([#982](https://github.com/OHIF/Viewers/issues/982)) ([cf40a83](https://github.com/OHIF/Viewers/commit/cf40a83))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.1...@ohif/ui@0.51.2) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Set series into active viewport by clicking on thumbnail ([#945](https://github.com/OHIF/Viewers/issues/945)) ([5551f81](https://github.com/OHIF/Viewers/commit/5551f81)), closes [#895](https://github.com/OHIF/Viewers/issues/895) [#895](https://github.com/OHIF/Viewers/issues/895)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.51.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.0...@ohif/ui@0.51.1) (2019-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Use HTML5Backend for drag-drop if the device does not support touch ([#927](https://github.com/OHIF/Viewers/issues/927)) ([6fdac4d](https://github.com/OHIF/Viewers/commit/6fdac4d))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.50.4...@ohif/ui@0.51.0) (2019-09-12)
|
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.50.4...@ohif/ui@0.51.0) (2019-09-12)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "0.51.0",
|
"version": "0.56.1",
|
||||||
"description": "A set of React components for Medical Imaging Viewers",
|
"description": "A set of React components for Medical Imaging Viewers",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@ohif/i18n": "^0.2.3",
|
"@ohif/i18n": "^0.2.3",
|
||||||
|
"browser-detect": "^0.2.28",
|
||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
@@ -48,8 +49,9 @@
|
|||||||
"prop-types": "15.6.2",
|
"prop-types": "15.6.2",
|
||||||
"react-bootstrap-modal": "4.2.0",
|
"react-bootstrap-modal": "4.2.0",
|
||||||
"react-dates": "18.4.1",
|
"react-dates": "18.4.1",
|
||||||
"react-dnd": "8.0.3",
|
"react-dnd": "9.3.4",
|
||||||
"react-dnd-touch-backend": "^0.8.3",
|
"react-dnd-html5-backend": "^9.3.4",
|
||||||
|
"react-dnd-touch-backend": "^9.3.4",
|
||||||
"react-i18next": "^10.11.0",
|
"react-i18next": "^10.11.0",
|
||||||
"react-with-direction": "1.3.0"
|
"react-with-direction": "1.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Loaded 100 of 213 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user