Compare commits

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

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

* ci: speed up PR checks

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

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

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

* docs: remove outdated docs

* Don't expose ExampleDropTarget

* Revert thumnail entry drag source removal

* Update screaming tests

* fix drag-n-drop

* fix vtk mpr2d CC: @jamesapetts

* remove setSingleLayoutData

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

* Use increasingly general pattern for restoring the cache

* Avoid running script tag e2e tests twice

* Performance improvements for CI

* Valid YAML

* Valid YAML?

* Valid YAML!!!!

* Update Node image, update yarn version

* circle

* serve
2019-10-11 11:40:21 -04:00
ohif-bot beb7083e55 chore(release): publish [skip ci]
- @ohif/viewer@1.6.1
2019-10-11 13:13:57 +00:00
Erik Ziegler 412fe4e23f fix: Switch token storage back to localStorage because in-memory was annoying for end users (#1030) 2019-10-11 14:53:49 +02:00
ohif-bot 659649e10b chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.6
 - @ohif/core@1.3.0
 - @ohif/viewer@1.6.0
2019-10-11 09:32:51 +00:00
Erik Ziegler f431b277dd ci: Disable docker layer caching because CircleCI doesn't let open source plans use it (#1028) 2019-10-11 11:14:54 +02:00
ladeirarodolfo 2bc361cdca feat: 🎸 Improve usability of Google Cloud adapter, including direct routes to studies (#989) 2019-10-11 09:59:38 +02:00
Erik Ziegler c6f306c0de Revert "ci: Disable docker layer caching because CircleCI doesn't let open source plans use it"
This reverts commit c0ea27f6b3.
2019-10-11 08:52:07 +02:00
Erik Ziegler c0ea27f6b3 ci: Disable docker layer caching because CircleCI doesn't let open source plans use it 2019-10-11 08:45:02 +02:00
ohif-bot df8d4029f1 chore(release): publish [skip ci]
- @ohif/viewer@1.5.4
2019-10-10 20:07:32 +00:00
Mirna Silva a36cb63926 tests: Test cases implemented: Cornerstone tools Clear and Eraser; Measurement panel Relabel and Description (#1014)
* Fix for test scripts that were failing in CI

* Test cases for tools: Length and Angle

* Test cases implemented: Scroll series, Levels, Pan, Reset, Zoom

* Added test cases for tools: Cine, More, Layout, Invert, Rotate, FlipH, FlipV

* Fixes made according to comments on PR review

* Fixes for failing scenarios: Layout and Cine

* Test cases implemented: Cornerstone tools Clear and Eraser; Measurement panel Relabel and Description

* Fix for CI failure
2019-10-10 15:46:41 -04:00
ohif-bot e51529e6c9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.5
 - @ohif/viewer@1.5.3
2019-10-10 16:57:21 +00:00
James Petts 750a150644 Merge pull request #1025 from JamesAPetts/vtkjsViewportPerformance
perf: ⚡️ Update react-vtkjs-viewport for performance improvement
2019-10-10 18:37:34 +02:00
James A. Petts a9d28d85da Merge remote-tracking branch 'upstream/master' into vtkjsViewportPerformance 2019-10-10 18:02:08 +02:00
James A. Petts 6ee5094cbe perf: ⚡️ Update react-vtkjs-viewport for performance improvement 2019-10-10 17:57:53 +02:00
ohif-bot 6bfb9bb140 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.4
 - @ohif/ui@0.53.3
 - @ohif/viewer@1.5.2
2019-10-10 02:41:15 +00:00
Danny Brown db5b760432 ci: skip minimize flag (#1023)
* Better css chunk filenames depending on context

* skip minimize flag to speed up builds that are timing out (short-term fix)

* make sure our custom TerserJS config is actually applied

* Use SKIP_MINIMIZE flag for netlify and demo builds
2019-10-09 22:18:06 -04:00
Igor Octaviano e7de8be2d8 fix: 🎸 switch ohif logo from text + font to SVG (#1021)
currently we are using font + text, not it was changed to use svg
instead

Closes: 723
2019-10-09 22:15:53 -04:00
ohif-bot cb2088daf7 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.3.1
 - @ohif/extension-vtk@0.52.3
 - @ohif/ui@0.53.2
 - @ohif/viewer@1.5.1
2019-10-09 19:55:40 +00:00
Igor Octaviano 2a743554b6 fix: 🐛 set current viewport as active when switching layouts (#1018)
* fix: 🐛 set current viewport as active when switching layouts

check if current viewport index is less than the current layout lenght
and set 0 if so or keep current selected cell index as active viewport

Closes: 999

* chore: 🤖 commenting out docker layer caching in the short-term

commenting out docker layer caching in the short-term

* refactor: 💡 refactor layout matrix index conditional
2019-10-09 15:37:26 -04:00
Igor Octaviano a28984ef1f fix: 🐛 set tools bidirectional, eraser and annotate command (#1020)
annotate, bidirectional and eraser had types as command but they should
be typed as setToolActive

Closes: 981
2019-10-09 15:30:00 -04:00
Rodrigo Antinarelli 95e5e11d09 test: E2E Tests - Thumbnail loading (#1017)
* fix: E2E Tests - Thumbnail loading

* rename data-cy attr
2019-10-09 12:57:24 -04:00
ohif-bot ff9f199850 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.3.0
 - @ohif/viewer@1.5.0
2019-10-09 14:45:40 +00:00
Danny Brown 1cc94f36a7 feat: Multiple fixes and implementation changes to react-cornerstone-viewport
* Update props for new react-cornerstone-viewport implementation

* Create an preRegistration hook for the cornerstone extension to setup cornerstone tools (takes configuration)

* Isolate measurements panel logic

* reorder extension registration

* remove unused setupTools

* Restore CINE connection

* fix stack prefetcher toggling

* Cleanup OHIFCornerstoneViewport props

* updated yarn lock

* bust yarn.lock to get updated react-cornerstone-viewport

* fix review comment; us isActive to better show it's influence on prefetch

* refactor: remove pass through method

* review performance optimization

* review; comment out unused variable

* shift tool under correct comment

* Use alternative csTools config, if provided

* Note regarding config options
2019-10-09 10:23:14 -04:00
ohif-bot 5c5223fcf1 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.2
 - @ohif/core@1.2.0
 - @ohif/viewer@1.4.5
2019-10-09 11:27:27 +00:00
James Petts 8513ad9ef3 perf: 🎸 Update vtk viewport for faster image reconstruction (#1016) 2019-10-09 13:13:13 +02:00
Emanuel Fiuza de Oliveira 66d8bc6d6f fix: OHIF-1002 Study lazy load should be true by default (#1004) 2019-10-07 16:31:47 -04:00
Jody Zeitler 0064a4b1cd feat: Allow a server requestOptions.auth to be a function that returns the Authorization header. (#928) 2019-10-06 21:07:44 -04:00
ohif-bot 74ce89b22b chore(release): publish [skip ci]
- @ohif/viewer@1.4.4
2019-10-07 01:07:06 +00:00
Mirna Silva c92975f555 test: Fix for test scripts that were failing in CI (#968)
* Fix for test scripts that were failing in CI
* Test cases for tools: Length and Angle
* Test cases implemented: Scroll series, Levels, Pan, Reset, Zoom
* Added test cases for tools: Cine, More, Layout, Invert, Rotate, FlipH, FlipV
* Fixes made according to comments on PR review
* Fixes for failing scenarios: Layout and Cine
2019-10-06 20:57:49 -04:00
ohif-bot ce6c0533a0 chore(release): publish [skip ci]
- @ohif/i18n@0.50.5
 - @ohif/viewer@1.4.3
2019-10-04 16:55:28 +00:00
Rodrigo Antinarelli 4df624b8c4 fix: CineDialog buttons label (#998)
* fix: CineDialog buttons label

* Revert "fix: CineDialog buttons label"

This reverts commit 48ea7a28e50b845c4939a8aaf2d95eba49d2c102.

* fix: CineDialog buttons label
2019-10-04 12:46:39 -04:00
ohif-bot e064fd353e chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.1
 - @ohif/ui@0.53.1
 - @ohif/viewer@1.4.2
2019-10-04 13:37:03 +00:00
Sanders DeNardi 458d310c78 fix: Move Series Information to Separate Row (#990)
* fix ThumbnailEntry alignment

* move series information to separate row
2019-10-04 09:26:05 -04:00
ohif-bot 73bc4d9094 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.0
 - @ohif/viewer@1.4.1
2019-10-03 14:05:24 +00:00
James Petts 7709ebb107 Merge pull request #993 from JamesAPetts/vtkViewportSubscriptionManager
feat: 🎸 Synced Window Leveling
2019-10-03 15:49:19 +02:00
James A. Petts 559b637290 feat: 🎸 Synced Window Leveling
Syncs window leveling for vtkjs viewports in MPR 2D mode.

Closes: #558
2019-10-03 15:29:56 +02:00
ohif-bot 36910f2b47 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.3
 - @ohif/core@1.1.0
 - @ohif/ui@0.53.0
 - @ohif/viewer@1.4.0
2019-10-03 08:02:00 +00:00
Emanuel Fiuza de Oliveira 9e10c2b2de feat: Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once (#953) 2019-10-03 09:51:29 +02:00
ohif-bot c5cbd77cb9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.2
 - @ohif/core@1.0.2
 - @ohif/viewer@1.3.3
2019-10-02 07:49:25 +00:00
Erik Ziegler b3b4c10e4c fix: Temporarily sort SEG files to the end of the display set list as a workaround for several metadata issues (#987) 2019-10-02 09:39:38 +02:00
ohif-bot 855020d730 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.1
 - @ohif/viewer@1.3.2
2019-10-01 20:36:02 +00:00
Zaid Safadi 8503cbce4e fix(rotate): Use new MPR rotate in react-vtkjs-viewport 0.1.2 (#964) 2019-10-01 22:21:31 +02:00
ohif-bot c8667fb2d6 chore(release): publish [skip ci]
- @ohif/viewer@1.3.1
2019-10-01 10:07:08 +00:00
Erik Ziegler 674ca9f96f fix: Exit MPR mode if Layout is changed (#984) 2019-10-01 11:55:58 +02:00
ohif-bot 7a4b97b4e8 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.0
 - @ohif/ui@0.52.0
 - @ohif/viewer@1.3.0
2019-10-01 08:16:51 +00:00
ladeirarodolfo c79c0c301d feat: 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle (#947) 2019-10-01 10:06:56 +02:00
ohif-bot ff5060f276 chore(release): publish [skip ci]
- @ohif/viewer@1.2.8
2019-10-01 07:42:58 +00:00
Erik Ziegler dafb26b34b chore: Fix dev server so that it can be accessed on LAN from other devices (#983) 2019-10-01 09:32:50 +02:00
ohif-bot c4f9d5e750 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.23
 - @ohif/ui@0.51.3
 - @ohif/viewer@1.2.7
2019-10-01 07:11:22 +00:00
Erik Ziegler cf40a8388c fix: Address issues with touch devices and drag/drop causing crashes (#982) 2019-10-01 09:00:55 +02:00
Erik Ziegler d596ab5c3d ci: Try to fix codecov threshold (#976) 2019-09-30 12:24:14 +02:00
Erik Ziegler 72198eee7d ci: Increase build timeout limit as a stopgap measure (#977) 2019-09-30 12:19:44 +02:00
Danny Brown 057f68a9b4 fix: yarn run start as alias for yarn run dev (#973) 2019-09-28 20:25:47 -04:00
Steve Pieper 034ca64d6b docs: Update node version in readme (#971)
Fixes #970
2019-09-28 19:10:20 -04:00
ohif-bot 4a0901aee9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.50.22
 - @ohif/core@1.0.1
 - @ohif/viewer@1.2.6
2019-09-27 15:42:12 +00:00
Bhavesh Pandey d0ec9cf0e0 fix: Check for Value in 00081155 sequence (Few patient protocol images doesn't have this value) and removed a duplicate declaration (#921) 2019-09-27 11:30:26 -04:00
ohif-bot 71bea0d4d8 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.2.5
 - @ohif/viewer@1.2.5
2019-09-27 13:18:31 +00:00
Danny Brown e607ed2aa0 fix: version bump issue (#963)
* Revert "fix: version bump issue (#962)"

This reverts commit c80ea17cc2.

* fix: stick
2019-09-27 09:04:29 -04:00
ohif-bot 4939f43509 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.0
 - @ohif/viewer@2.0.0
2019-09-27 12:54:25 +00:00
Danny Brown c80ea17cc2 fix: version bump issue (#962)
* fix: version bump issue

* fix: catch the cornerstone-bump as well
2019-09-27 08:43:40 -04:00
ohif-bot aa7dc1e2c1 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.0
 - @ohif/extension-dicom-html@1.0.0
 - @ohif/extension-dicom-pdf@1.0.0
 - @ohif/extension-vtk@0.50.21
 - @ohif/core@1.0.0
 - @ohif/viewer@2.0.0
2019-09-27 11:58:23 +00:00
ladeirarodolfo e7e1a8a6cd fix: 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases (#862)
* fix: 🐛 Local file: failing when retrieving segmentation data

Fix segmentation data retrieval issues for local file. Changed from
fecthing to use cornerstone loadAndCache method

BREAKING CHANGE: DICOM Seg

Closes: part of #838

* Switch SEG retrieval to WADO-RS

* Forgot a debugger

* refactor: 💡 Code refactor. Minor changes into methods

* fix: 🐛 Load local files: PDF

Items:
1. FileLoaderService: used for serveral operations on local files(load it,  get list of studies, group them, accepting dicom and pdf)
2. DicomLoaderService: used for loading dicom based on dataset and studies. Depending on type of dicom loader might change. WIP
3. Refactor PDF and handleSegmentationStorage to use DicomLoaderService

* fix: 🐛 Code review

* fix: 🐛 Code review. Changed:Folder organization and dicom file

Move fileLoaderService and others to a specific folder. When loading
dicom file change to only retrieve the file(not use cornerstone to cache
or anything else).

* fix: 🐛 Code review. Move dicomLoaderService to core

Moved dicomLoaderService to ohif/core and localFileLoaders to a specific
folder.

* fix: 🐛 Code review

Simplified method to get study for dicom file. Added error handling on
file loading. DicomLoaderService to be exposed on ohif/core/utils
instead.

* fix: 🐛 Reduce local load to one method only

Reduced local file load to one method only

* fix: 🐛 HTML to use dicomLoaderService. Prefer wadors than (uri)

* fix: 🐛 Code implementation for multiframe files

* fix: 🐛 Code review. Default local loader to dicom

Closes: 838

* fix: 🐛 Code review. Use relative path to require DICOMWeb

Closes: 838

* fix: 🐛 Code review. Fix unit test. Added DicomLoaderService mod

Closes: 838

* fix: 🐛 Code review. Add 'Seg' on left thumb

When getting/creating dataset get modality for file/image read

Closes: 838
2019-09-27 13:47:08 +02:00
ohif-bot e66c27affb chore(release): publish [skip ci]
- @ohif/viewer@1.2.3
2019-09-27 11:10:33 +00:00
Danny Brown ae26904885 Revert "* fix: 🐛 Enabling workbox to cache webfonts and fonts-stylesheets, add local fonts for UMD build (#906)" (#961)
This reverts commit 69b96478cb.
2019-09-27 06:54:47 -04:00
ohif-bot 8bb730318a chore(release): publish [skip ci]
- @ohif/viewer@1.2.2
2019-09-27 09:20:09 +00:00
Gustavo André Lelis 69b96478cb * fix: 🐛 Enabling workbox to cache webfonts and fonts-stylesheets, add local fonts for UMD build (#906)
* fix: 🐛 commonjs/umd fonts import

Importing font into index-umd.js and creating a simple font css file
with font-face

Closes: #724

* fix: 🐛 Enabling workbox to cache webfonts and fonts-stylesheets

Closes: #724

* fix: 🐛 Adding all type of fonts from google - Roboto

Closes: #724
2019-09-27 11:11:05 +02:00
ohif-bot 816666df7b chore(release): publish [skip ci]
- @ohif/viewer@1.2.1
2019-09-26 18:24:46 +00:00
Danny Brown e375a4ab11 fix: google cloud support w/ docker (via env var) (#958)
* fix: make sure google config is in output

* fix: update config sub to use new paths and file names
2019-09-26 14:08:44 -04:00
ohif-bot 7f4d71e412 chore(release): publish [skip ci]
- @ohif/viewer@1.2.0
2019-09-26 17:56:20 +00:00
ladeirarodolfo a90605c82d feat: 🎸 React custom component on toolbar button (#935)
* feat: 🎸 React custom component on toolbar button

Toolbar button can consume/render react custom components. Its only
required to expose the component into toolbar definitions
(toolbarModule)

Closes: 924

* feat: 🎸 Core review. Minor changes on documentation

Closes: 924

* feat: 🎸 Code review

* feat: 🎸 Code review. Changing function call to variable decl
2019-09-26 13:45:19 -04:00
156 changed files with 6212 additions and 3225 deletions

No files matched your search

+94 -39
View File
@@ -17,46 +17,107 @@ orbs:
defaults: &defaults
docker:
- image: circleci/node:10.16.0
- image: circleci/node:12.9.1
environment:
TERM: xterm # Enable colors in term
working_directory: ~/repo
jobs:
CHECKOUT:
UNIT_TESTS:
<<: *defaults
steps:
# Update yarn
- run: yarn -v
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-packages-v1-{{ .Branch }}-
- yarn-packages-v1-
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
# Persist :+1:
- persist_to_workspace:
root: ~/repo
paths: .
key: yarn-packages-{{ checksum "yarn.lock" }}
UNIT_TESTS:
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: yarn run test:unit:ci
# PLATFORM/VIEWER
- run:
name: 'VIEWER: Combine report output'
command: |
viewerCov="/home/circleci/repo/platform/viewer/coverage"
touch "${viewerCov}/reports"
cat "${viewerCov}/clover.xml" >> "${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
cat "${viewerCov}/lcov.info" >>"${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/viewer/coverage/reports'
flags: 'viewer'
# PLATFORM/CORE
- run:
name: 'CORE: Combine report output'
command: |
coreCov="/home/circleci/repo/platform/core/coverage"
touch "${coreCov}/reports"
cat "${coreCov}/clover.xml" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
cat "${coreCov}/lcov.info" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
UNIT_TESTS_PERSIST_WORKSPACE:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
# Update yarn
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# RUN TESTS
- run:
@@ -92,7 +153,7 @@ jobs:
flags: 'core'
# Persist :+1:
- persist_to_workspace:
- persist_to_workspace: &persist_to_workspace
root: ~/repo
paths: .
@@ -143,7 +204,7 @@ jobs:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: true
docker_layer_caching: false
- run:
name: Build and push Docker image
command: |
@@ -175,7 +236,7 @@ jobs:
- run:
name: 'Build Demo Site'
command: yarn run build:demo
no_output_timeout: 30m
no_output_timeout: 45m
- run:
name: 'Upload SourceMaps, Send Deploy Notification'
command: |
@@ -199,7 +260,7 @@ jobs:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: true
docker_layer_caching: false
- run:
name: Deploy latest version to viewer.ohif.org
command: |
@@ -217,16 +278,13 @@ workflows:
# PULL REQUESTS
PR_CHECKS:
jobs:
- CHECKOUT:
- UNIT_TESTS:
filters:
branches:
ignore:
- master
- feature/*
- hotfix/*
- UNIT_TESTS:
requires:
- CHECKOUT
# E2E: PWA
- cypress/run:
name: 'E2E: PWA'
@@ -236,10 +294,10 @@ workflows:
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: yarn run build:e2e
start: yarn run test:e2e:dist
build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
@@ -255,10 +313,10 @@ workflows:
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: yarn run build:package
start: yarn run test:e2e:dist
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
@@ -269,13 +327,10 @@ workflows:
# MERGE TO MASTER
RELEASE:
jobs:
- CHECKOUT:
- UNIT_TESTS_PERSIST_WORKSPACE:
filters:
branches:
only: master
- UNIT_TESTS:
requires:
- CHECKOUT
# E2E: PWA + Persist
- cypress/run:
name: 'E2E: PWA'
@@ -286,10 +341,10 @@ workflows:
record: true
store_artifacts: true
working_directory: platform/viewer
build: yarn run build:e2e
start: yarn run test:e2e:dist
build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
@@ -297,7 +352,7 @@ workflows:
- store_test_results:
path: cypress/results
requires:
- UNIT_TESTS
- UNIT_TESTS_PERSIST_WORKSPACE
# E2E: script-tag
- cypress/run:
name: 'E2E: Script Tag'
@@ -307,10 +362,10 @@ workflows:
yarn: true
store_artifacts: false
working_directory: platform/viewer
build: yarn run build:package
start: yarn run test:e2e:dist
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
@@ -318,7 +373,7 @@ workflows:
- store_test_results:
path: cypress/results
requires:
- UNIT_TESTS
- UNIT_TESTS_PERSIST_WORKSPACE
# Update NPM
- NPM_PUBLISH:
requires:
+3 -1
View File
@@ -8,11 +8,13 @@ coverage:
status:
project:
default:
threshold: 0.10%
threshold: 0.5%
core:
flags: core
threshold: 0.5%
viewer:
flags: viewer
threshold: 0.5%
patch: off
flags:
core:
+4 -4
View File
@@ -7,11 +7,11 @@ if [ ! -z "$CLIENT_ID" ]
echo $CLIENT_ID
echo "Updating config..."
# - Use SED to replace the CLIENT_ID that is currently in public/config/google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/config/google.js
# - Use SED to replace the CLIENT_ID that is currently in google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
# - Copy public/config/google.js to overwrite public/config/default.js
cp /usr/share/nginx/html/config/google.js /usr/share/nginx/html/config/default.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..."
+46 -4
View File
@@ -1,23 +1,46 @@
const path = require('path');
const webpack = require('webpack');
// ~~ RULES
const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
// ~~ PLUGINS
const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
const QUICK_BUILD = process.env.QUICK_BUILD;
module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
if (!process.env.NODE_ENV) {
throw new Error('process.env.NODE_ENV not set');
}
const mode =
process.env.NODE_ENV === 'production' ? 'production' : 'development';
const mode = NODE_ENV === 'production' ? 'production' : 'development';
const isProdBuild = NODE_ENV === 'production';
const isQuickBuild = QUICK_BUILD === 'true';
return {
mode,
const config = {
mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
entry: {
app: `${SRC_DIR}/index.js`,
},
optimization: {
minimize: isProdBuild,
sideEffects: true,
},
context: SRC_DIR,
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
module: {
rules: [
transpileJavaScriptRule(mode),
@@ -53,4 +76,23 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
fs: 'empty',
},
};
if (isProdBuild) {
config.optimization.minimizer = [
new TerserJSPlugin({
// Supports:
// source-map and inline-source-map
sourceMap: isProdBuild && !isQuickBuild,
parallel: true,
terserOptions: {},
}),
];
}
if (isQuickBuild) {
config.optimization.minimize = false;
config.devtool = false;
}
return config;
};
+1 -1
View File
@@ -132,7 +132,7 @@ deployment recipe.
### Requirements
- [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 config set workspaces-experimental true`
+36 -3
View File
@@ -140,8 +140,8 @@ children: PropTypes.node,
customProps: PropTypes.object
```
Viewport components are managed by the `LayoutManager`. Which Viewport component
is used depends on:
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
component is used depends on:
- The Layout Configuration
- Registered SopClassHandlers
@@ -159,7 +159,8 @@ For a complete example implementation,
An extension can register a Toolbar Module by providing a `getToolbarModule()`
method that returns a React Component. The component does not receive any props.
If you want to modify or react to state, you will need to connect to the redux
store.
store. The given toolbar must determine its set of elements and the context of
them. The set of elements will be listed on toolbar `definitions`.
![Toolbar Extension](../assets/img/extensions-toolbar.gif)
@@ -170,6 +171,38 @@ Toolbar components are rendered in the `ToolbarRow` component.
For a complete example implementation,
[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
...
+66
View File
@@ -3,6 +3,72 @@
All notable changes to this project will be documented in this file.
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)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "1.2.2",
"version": "1.3.1",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -48,6 +48,6 @@
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"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) => {
let dataFromStore;
// TODO: This may not be updated anymore :thinking:
if (state.extensions && 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.
const { viewportIndex } = ownProps; //.viewportData;
const isActive = viewportIndex === state.viewports.activeViewportIndex;
const viewportSpecificData =
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 {
layout: state.viewports.layout,
// layout: state.viewports.layout,
isActive,
// TODO: Need a cleaner and more versatile way.
// Currently justing using escape hatch + commands
// activeTool: activeButton && activeButton.command,
...dataFromStore,
enableStackPrefetch: isActive,
isStackPrefetchEnabled: isActive,
isPlaying,
frameRate,
//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 OHIF from "@ohif/core";
import PropTypes from "prop-types";
import cornerstone from "cornerstone-core";
import handleSegmentationStorage from "./handleSegmentationStorage.js";
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
import OHIF from '@ohif/core';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import handleSegmentationStorage from './handleSegmentationStorage.js';
const { StackManager } = OHIF.utils;
@@ -18,7 +18,7 @@ cornerstone.metaData.addProvider(
StackManager.setMetadataProvider(metadataProvider);
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 = {};
@@ -28,11 +28,11 @@ specialCaseHandlers[
class OHIFCornerstoneViewport extends Component {
state = {
viewportData: null
viewportData: null,
};
static defaultProps = {
customProps: {}
customProps: {},
};
static propTypes = {
@@ -40,17 +40,17 @@ class OHIFCornerstoneViewport extends Component {
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
children: PropTypes.node,
customProps: PropTypes.object
customProps: PropTypes.object,
};
static id = "OHIFCornerstoneViewport";
static id = 'OHIFCornerstoneViewport';
static init() {
console.log("OHIFCornerstoneViewport init()");
console.log('OHIFCornerstoneViewport init()');
}
static destroy() {
console.log("OHIFCornerstoneViewport destroy()");
console.log('OHIFCornerstoneViewport destroy()');
StackManager.clearStacks();
}
@@ -72,15 +72,15 @@ class OHIFCornerstoneViewport extends Component {
frameIndex = 0
) {
if (!studies || !studies.length) {
throw new Error("Studies not provided.");
throw new Error('Studies not provided.');
}
if (!studyInstanceUid) {
throw new Error("StudyInstanceUID not provided.");
throw new Error('StudyInstanceUID not provided.');
}
if (!displaySetInstanceUid) {
throw new Error("StudyInstanceUID not provided.");
throw new Error('StudyInstanceUID not provided.');
}
// Create shortcut to displaySet
@@ -89,7 +89,7 @@ class OHIFCornerstoneViewport extends Component {
);
if (!study) {
throw new Error("Study not found.");
throw new Error('Study not found.');
}
const displaySet = study.displaySets.find(set => {
@@ -97,7 +97,7 @@ class OHIFCornerstoneViewport extends Component {
});
if (!displaySet) {
throw new Error("Display Set not found.");
throw new Error('Display Set not found.');
}
// Get stack from Stack Manager
@@ -110,7 +110,7 @@ class OHIFCornerstoneViewport extends Component {
if (sopInstanceUid) {
const index = stack.imageIds.findIndex(imageId => {
const sopCommonModule = cornerstone.metaData.get(
"sopCommonModule",
'sopCommonModule',
imageId
);
if (!sopCommonModule) {
@@ -124,7 +124,7 @@ class OHIFCornerstoneViewport extends Component {
stack.currentImageIdIndex = index;
} else {
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;
}
static getViewportData = (
studies,
studyInstanceUid,
displaySetInstanceUid,
sopInstanceUid,
frameIndex
) => {
return OHIFCornerstoneViewport.getCornerstoneStack(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopInstanceUid,
frameIndex
);
};
getViewportData = async (
studies,
studyInstanceUid,
@@ -172,7 +156,7 @@ class OHIFCornerstoneViewport extends Component {
);
break;
default:
const stack = OHIFCornerstoneViewport.getViewportData(
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
studies,
studyInstanceUid,
displaySetInstanceUid,
@@ -183,7 +167,7 @@ class OHIFCornerstoneViewport extends Component {
viewportData = {
studyInstanceUid,
displaySetInstanceUid,
stack
stack,
};
break;
@@ -199,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
displaySetInstanceUid,
sopClassUids,
sopInstanceUid,
frameIndex
frameIndex,
} = displaySet;
if (!studyInstanceUid || !displaySetInstanceUid) {
@@ -208,7 +192,7 @@ class OHIFCornerstoneViewport extends Component {
if (sopClassUids && sopClassUids.length > 1) {
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
).then(viewportData => {
this.setState({
viewportData
viewportData,
});
});
}
@@ -249,25 +233,42 @@ class OHIFCornerstoneViewport extends Component {
render() {
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?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index
key: index,
});
});
}
return (
<>
{this.state.viewportData && (
<ConnectedCornerstoneViewport
viewportData={this.state.viewportData}
viewportIndex={this.props.viewportIndex}
{...this.props.customProps}
/>
)}
<ConnectedCornerstoneViewport
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={currentImageIdIndex}
// ~~ Connected (From REDUX)
// frameRate={frameRate}
// isPlaying={false}
// isStackPrefetchEnabled={true}
// onElementEnabled={() => {}}
// setViewportActive{() => {}}
{...this.props.customProps}
/>
{childrenWithProps}
</>
);
@@ -4,7 +4,7 @@ import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
const { StackManager } = OHIF.utils;
const { StackManager, DicomLoaderService } = OHIF.utils;
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
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(
studies,
studyInstanceUid,
@@ -73,8 +65,11 @@ async function handleSegmentationStorage(
studyInstanceUid,
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 dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
+15 -1
View File
@@ -1,8 +1,13 @@
import init from './init.js';
import asyncComponent from './asyncComponent.js';
import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js';
const OHIFCornerstoneViewport = asyncComponent(() => import( /* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'));
const OHIFCornerstoneViewport = asyncComponent(() =>
import(
/* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'
)
);
/**
*
@@ -13,6 +18,15 @@ export default {
*/
id: 'cornerstone',
/**
*
*
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration(configuration = {}) {
init(configuration);
},
getViewportModule() {
return OHIFCornerstoneViewport;
},
+87
View File
@@ -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', {});
}
+3 -3
View File
@@ -74,7 +74,7 @@ const definitions = [
label: 'Annotate',
icon: 'measure-non-target',
//
type: TOOLBAR_BUTTON_TYPES.COMMAND,
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'ArrowAnnotate' },
},
@@ -200,7 +200,7 @@ const definitions = [
label: 'Bidirectional',
icon: 'measure-target',
//
type: TOOLBAR_BUTTON_TYPES.COMMAND,
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'Bidirectional' },
},
@@ -209,7 +209,7 @@ const definitions = [
label: 'Eraser',
icon: 'eraser',
//
type: TOOLBAR_BUTTON_TYPES.COMMAND,
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'Eraser' },
},
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-html",
"version": "0.50.8",
"version": "1.0.0",
"description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF",
"license": "MIT",
@@ -1,46 +1,34 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import DicomHtmlViewport from './DicomHtmlViewport';
import OHIF from '@ohif/core';
const { DicomLoaderService } = OHIF.utils;
class OHIFDicomHtmlViewport extends Component {
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number
viewportIndex: PropTypes.number,
};
state = {
byteArray: null,
error: null
error: null,
};
componentDidMount() {
const { displaySet } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
} = displaySet;
const { displaySet, studies } = this.props.viewportData;
this.retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
).then(
byteArray => {
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => {
const byteArray = new Uint8Array(data);
this.setState({
byteArray
byteArray: byteArray,
});
},
error => {
this.setState({
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() {
return (
<>
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-pdf",
"version": "0.50.4",
"version": "1.0.0",
"description": "OHIF extension for Dicom PDF",
"author": "OHIF",
"license": "MIT",
+20 -15
View File
@@ -4,41 +4,46 @@ import TypedArrayProp from './TypedArrayProp';
// TODO: Should probably use dcmjs for this
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 {
state = {
fileURL: null,
error: null
error: null,
};
static propTypes = {
byteArray: TypedArrayProp.uint8
byteArray: TypedArrayProp.uint8,
};
renderPDF = dataSet => {
const sopClassUid = dataSet.string('x00080016');
renderPDF = (dataSet, byteArray) => {
let pdfByteArray = byteArray;
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
throw new Error('This is not a DICOM-encapsulated PDF');
if (dataSet) {
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 fileURL = URL.createObjectURL(PDF);
this.setState({
fileURL
fileURL,
});
};
parseByteArray = byteArray => {
const options = {
untilTag: ''
untilTag: '',
};
let dataSet;
@@ -47,7 +52,7 @@ class DicomPDFViewport extends Component {
dataSet = dicomParser.parseDicom(byteArray, options);
} catch (error) {
this.setState({
error
error,
});
}
@@ -57,7 +62,7 @@ class DicomPDFViewport extends Component {
componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
this.renderPDF(dataSet);
this.renderPDF(dataSet, this.props.byteArray);
}
render() {
@@ -1,60 +1,54 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import OHIF from "@ohif/core";
import OHIFComponentPlugin from "./OHIFComponentPlugin.js";
import DicomPDFViewport from "./DicomPDFViewport";
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import OHIF from '@ohif/core';
import OHIFComponentPlugin from './OHIFComponentPlugin.js';
import DicomPDFViewport from './DicomPDFViewport';
const { DICOMWeb } = OHIF;
const { DicomLoaderService } = OHIF.utils;
class OHIFDicomPDFViewport extends Component {
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number
viewportIndex: PropTypes.number,
};
state = {
byteArray: null,
error: null
error: null,
};
static id = "DicomPDFViewportPDF";
static id = 'DicomPDFViewportPDF';
static init() {
console.log("DicomPDFViewport init()");
console.log('DicomPDFViewport init()');
}
static destroy() {
console.log("DicomPDFViewport destroy()");
console.log('DicomPDFViewport destroy()');
}
componentDidMount() {
const { displaySet } = this.props.viewportData;
const { displaySet, studies } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
authorizationHeaders,
} = displaySet;
this.retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
).then(
byteArray => {
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => {
const byteArray = new Uint8Array(data);
this.setState({
byteArray
byteArray: byteArray,
});
},
error => {
this.setState({
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() {
const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy };
+143
View File
@@ -3,6 +3,149 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.50.20",
"version": "0.52.8",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -48,11 +48,11 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"react-vtkjs-viewport": "0.1.0"
"react-vtkjs-viewport": "^0.1.6"
},
"devDependencies": {
"@ohif/core": "^0.50.10",
"@ohif/ui": "^0.51.2",
"@ohif/core": "^1.3.2",
"@ohif/ui": "^0.53.4",
"cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
+48 -34
View File
@@ -1,16 +1,17 @@
import React, { Component } from "react";
import { getImageData, loadImageData } from "react-vtkjs-viewport";
import React, { Component } from 'react';
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
import ConnectedVTKViewport from "./ConnectedVTKViewport";
import LoadingIndicator from "./LoadingIndicator.js";
import OHIF from "@ohif/core";
import PropTypes from "prop-types";
import cornerstone from "cornerstone-core";
import handleSegmentationStorage from "./handleSegmentationStorage.js";
import vtkDataArray from "vtk.js/Sources/Common/Core/DataArray";
import vtkImageData from "vtk.js/Sources/Common/DataModel/ImageData";
import vtkVolume from "vtk.js/Sources/Rendering/Core/Volume";
import vtkVolumeMapper from "vtk.js/Sources/Rendering/Core/VolumeMapper";
import ConnectedVTKViewport from './ConnectedVTKViewport';
import LoadingIndicator from './LoadingIndicator.js';
import OHIF from '@ohif/core';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import handleSegmentationStorage from './handleSegmentationStorage.js';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
const { StackManager } = OHIF.utils;
@@ -24,7 +25,7 @@ cornerstone.metaData.addProvider(
StackManager.setMetadataProvider(metadataProvider);
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 = {};
@@ -42,7 +43,7 @@ const volumeCache = {};
*/
function createLabelMapImageData(backgroundImageData) {
const labelMapData = vtkImageData.newInstance(
backgroundImageData.get("spacing", "origin", "direction")
backgroundImageData.get('spacing', 'origin', 'direction')
);
labelMapData.setDimensions(backgroundImageData.getDimensions());
labelMapData.computeTransforms();
@@ -50,7 +51,7 @@ function createLabelMapImageData(backgroundImageData) {
const values = new Uint8Array(backgroundImageData.getNumberOfPoints());
const dataArray = vtkDataArray.newInstance({
numberOfComponents: 1, // labelmap with single component
values
values,
});
labelMapData.getPointData().setScalars(dataArray);
@@ -61,24 +62,24 @@ class OHIFVTKViewport extends Component {
state = {
volumes: null,
paintFilterLabelMapImageData: null,
paintFilterBackgroundImageData: null
paintFilterBackgroundImageData: null,
};
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
children: PropTypes.node
children: PropTypes.node,
};
static id = "OHIFVTKViewport";
static id = 'OHIFVTKViewport';
static init() {
console.log("OHIFVTKViewport init()");
console.log('OHIFVTKViewport init()');
}
static destroy() {
console.log("OHIFVTKViewport destroy()");
console.log('OHIFVTKViewport destroy()');
StackManager.clearStacks();
}
@@ -109,7 +110,7 @@ class OHIFVTKViewport extends Component {
} else if (sopInstanceUid) {
const index = stack.imageIds.findIndex(imageId => {
const sopCommonModule = cornerstone.metaData.get(
"sopCommonModule",
'sopCommonModule',
imageId
);
if (!sopCommonModule) {
@@ -151,8 +152,8 @@ class OHIFVTKViewport extends Component {
switch (sopClassUid) {
case SOP_CLASSES.SEGMENTATION_STORAGE:
throw new Error("Not yet implemented");
throw new Error('Not yet implemented');
/*
const data = handleSegmentationStorage(
stack.imageIds,
displaySetInstanceUid
@@ -164,15 +165,18 @@ class OHIFVTKViewport extends Component {
return loadImageData(imageDataObject).then(() => {
return {
data: imageDataObject.vtkImageData,
labelmap: labelmapDataObject
labelmap: labelmapDataObject,
};
});
*/
default:
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
return loadImageData(imageDataObject).then(() => {
const loadImageDataPromise = loadImageData(imageDataObject);
return loadImageDataPromise.then(() => {
return {
data: imageDataObject.vtkImageData
data: imageDataObject.vtkImageData,
};
});
}
@@ -189,10 +193,16 @@ class OHIFVTKViewport extends Component {
volumeActor.setMapper(volumeMapper);
volumeMapper.setInputData(data);
const range = data.getPointData().getScalars().getRange();
const range = data
.getPointData()
.getScalars()
.getRange();
// TODO: For PET we might want to just set this to 0-5 SUV
volumeActor.getProperty().getRGBTransferFunction(0).setRange(range[0], range[1]);
volumeActor
.getProperty()
.getRGBTransferFunction(0)
.setRange(range[0], range[1]);
// TODO: Should look into implementing autoAdjustSampleDistance in vtk
const sampleDistance =
@@ -200,7 +210,7 @@ class OHIFVTKViewport extends Component {
Math.sqrt(
data
.getSpacing()
.map((v) => v * v)
.map(v => v * v)
.reduce((a, b) => a + b, 0)
);
@@ -218,12 +228,12 @@ class OHIFVTKViewport extends Component {
displaySetInstanceUid,
sopClassUids,
sopInstanceUid,
frameIndex
frameIndex,
} = displaySet;
if (sopClassUids.length > 1) {
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.'
);
}
@@ -249,7 +259,7 @@ class OHIFVTKViewport extends Component {
this.setState({
volumes: [volumeActor],
paintFilterBackgroundImageData: data,
paintFilterLabelMapImageData: labelmap
paintFilterLabelMapImageData: labelmap,
});
}
@@ -257,6 +267,10 @@ class OHIFVTKViewport extends Component {
this.setStateFromProps();
}
componentWillUnmount() {
vtkViewportSubscriptionManager.unsubscribe(this.props.viewportIndex);
}
componentDidUpdate(prevProps) {
const { studies, displaySet } = this.props.viewportData;
const prevDisplaySet = prevProps.viewportData.displaySet;
@@ -279,12 +293,12 @@ class OHIFVTKViewport extends Component {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
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 (
<>
+60 -28
View File
@@ -2,12 +2,14 @@ import {
vtkInteractorStyleMPRCrosshairs,
vtkInteractorStyleMPRWindowLevel,
vtkInteractorStyleMPRSlice,
vtkInteractorStyleMPRRotate,
vtkSVGCrosshairsWidget,
vtkSVGWidgetManager,
} from 'react-vtkjs-viewport';
import setMPRLayout from './utils/setMPRLayout.js';
import setViewportToVTK from './utils/setViewportToVTK.js';
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
@@ -18,6 +20,7 @@ const { BlendMode } = Constants;
// TODO: Put this somewhere else
let apis = {};
let currentSlabThickness = 0.1;
let defaultSlabThickness = 0.1;
function getCrosshairCallbackForIndex(index) {
return ({ worldPos }) => {
@@ -63,7 +66,13 @@ function getCrosshairCallbackForIndex(index) {
}
async function _getActiveViewportVTKApi(viewports) {
const { layout, viewportSpecificData, activeViewportIndex } = viewports;
const {
numRows,
numColumns,
layout,
viewportSpecificData,
activeViewportIndex,
} = viewports;
const currentData = layout.viewports[activeViewportIndex];
if (currentData && currentData.plugin === 'vtk') {
@@ -81,6 +90,8 @@ async function _getActiveViewportVTKApi(viewports) {
api = await setViewportToVTK(
displaySet,
activeViewportIndex,
numRows,
numColumns,
layout,
viewportSpecificData
);
@@ -98,7 +109,7 @@ function _setView(api, sliceNormal, viewUp) {
const camera = renderer.getActiveCamera();
const istyle = renderWindow.getInteractor().getInteractorStyle();
istyle.setSliceNormal(...sliceNormal);
camera.setViewUp(...viewUp);
istyle.setViewUp(...viewUp);
renderWindow.render();
}
@@ -109,14 +120,15 @@ function switchMPRInteractors(api, istyle) {
const camera = renderer.getActiveCamera();
const currentIStyle = renderWindow.getInteractor().getInteractorStyle();
let currentNormal;
if (currentIStyle.getSliceNormal && istyle.getSliceNormal) {
currentNormal = currentIStyle.getSliceNormal();
let currentViewport;
if (currentIStyle.getViewport && istyle.getViewport) {
currentViewport = currentIStyle.getViewport();
}
let currentSlabThickness;
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
currentSlabThickness = currentIStyle.getSlabThickness();
defaultSlabThickness = currentSlabThickness;
}
renderWindow.getInteractor().setInteractorStyle(istyle);
@@ -124,11 +136,11 @@ function switchMPRInteractors(api, 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 (currentViewport) {
istyle.setViewport(currentViewport);
}
if (istyle.getVolumeMapper() !== api.volumes[0]) {
if (currentSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
@@ -161,7 +173,7 @@ const actions = {
},
enableRotateTool: () => {
apis.forEach(api => {
const istyle = vtkInteractorStyleMPRSlice.newInstance();
const istyle = vtkInteractorStyleMPRRotate.newInstance();
switchMPRInteractors(api, istyle);
});
@@ -182,7 +194,7 @@ const actions = {
switchMPRInteractors(api, istyle);
});
},
setSlabThickness: slabThickness => {
setSlabThickness: ({ slabThickness }) => {
currentSlabThickness = slabThickness;
apis.forEach(api => {
@@ -191,15 +203,6 @@ const actions = {
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();
@@ -220,6 +223,32 @@ const actions = {
renderWindow.render();
});
},
setBlendModeToComposite: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToComposite) {
mapper.setBlendModeToComposite();
}
if (istyle.setSlabThickness) {
istyle.setSlabThickness(defaultSlabThickness);
}
renderWindow.render();
});
},
setBlendModeToMaximumIntensity: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToMaximumIntensity) {
mapper.setBlendModeToMaximumIntensity();
}
renderWindow.render();
});
},
setBlendMode: ({ blendMode }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
@@ -243,16 +272,19 @@ const actions = {
apis = apiByViewport;
/*const rgbTransferFunction = apiByViewport[0].volumes[0]
const rgbTransferFunction = apiByViewport[0].volumes[0]
.getProperty()
.getRGBTransferFunction(0);
rgbTransferFunction.onModified(() => {
const onModifiedSubscription = rgbTransferFunction.onModified(() => {
apiByViewport.forEach(a => {
const renderWindow = a.genericRenderWindow.getRenderWindow();
renderWindow.render();
});
});*/
});
vtkViewportSubscriptionManager.pushSubscription(0, onModifiedSubscription);
apiByViewport.forEach((api, index) => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
@@ -284,18 +316,18 @@ const actions = {
case 0:
//Axial
istyle.setSliceNormal(0, 0, 1);
camera.setViewUp(0, -1, 0);
istyle.setViewUp(0, -1, 0);
break;
case 1:
// sagittal
istyle.setSliceNormal(1, 0, 0);
camera.setViewUp(0, 0, 1);
istyle.setViewUp(0, 0, 1);
break;
case 2:
// Coronal
istyle.setSliceNormal(0, 1, 0);
camera.setViewUp(0, 0, 1);
istyle.setViewUp(0, 0, 1);
break;
}
@@ -338,12 +370,12 @@ const definitions = {
options: {},
},
setBlendModeToComposite: {
commandFn: actions.setBlendMode,
commandFn: actions.setBlendModeToComposite,
storeContexts: [],
options: { blendMode: BlendMode.COMPOSITE_BLEND },
},
setBlendModeToMaximumIntensity: {
commandFn: actions.setBlendMode,
commandFn: actions.setBlendModeToMaximumIntensity,
storeContexts: [],
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
},
@@ -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);
+47
View File
@@ -1,3 +1,5 @@
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command',
SET_TOOL_ACTIVE: 'setToolActive',
@@ -31,6 +33,7 @@ const definitions = [
commandName: 'enableRotateTool',
commandOptions: {},
},
/*
{
id: 'setBlendModeToComposite',
label: 'Disable MIP',
@@ -68,6 +71,50 @@ const definitions = [
commandName: 'decreaseSlabThickness',
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 {
+16 -33
View File
@@ -1,60 +1,43 @@
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
import setSingleLayoutData from './setSingleLayoutData.js';
export default function setMPRLayout(displaySet) {
return new Promise((resolve, reject) => {
let viewports = [];
const rows = 1;
const columns = 3;
const numViewports = rows * columns;
const viewports = [];
const numRows = 1;
const numColumns = 3;
const numViewports = numRows * numColumns;
const viewportSpecificData = {};
for (let i = 0; i < numViewports; i++) {
viewports.push({
height: `${100 / rows}%`,
width: `${100 / columns}%`,
});
for (let i = 0; i < numViewports; i++) {
viewports.push({});
viewportSpecificData[i] = displaySet;
viewportSpecificData[i].plugin = 'vtk';
}
const layout = {
viewports,
};
const viewportIndices = [0, 1, 2];
let updatedViewports = layout.viewports;
const apis = [];
viewportIndices.forEach(viewportIndex => {
apis[viewportIndex] = null;
/*const currentData = layout.viewports[viewportIndex];
if (currentData && currentData.plugin === 'vtk') {
reject(new Error('Should not have reached this point??'));
}*/
const data = {
viewports.forEach((viewport, index) => {
apis[index] = null;
viewports[index] = Object.assign({}, viewports[index], {
// plugin: 'vtk',
vtk: {
mode: 'mpr', // TODO: not used
afterCreation: api => {
apis[viewportIndex] = api;
apis[index] = api;
if (apis.every(a => !!a)) {
resolve(apis);
}
},
},
};
updatedViewports = setSingleLayoutData(
updatedViewports,
viewportIndex,
data
);
});
});
setLayoutAndViewportData(
{ viewports: updatedViewports },
{
numRows,
numColumns,
viewports,
},
viewportSpecificData
);
});
@@ -1,12 +0,0 @@
export default function setSingleLayoutData(
originalArray,
viewportIndex,
data
) {
const viewports = originalArray.slice();
const layoutData = Object.assign({}, viewports[viewportIndex], data);
viewports[viewportIndex] = layoutData;
return viewports;
}
+11 -10
View File
@@ -1,9 +1,10 @@
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
import setSingleLayoutData from './setSingleLayoutData.js';
export default function setViewportToVTK(
displaySet,
viewportIndex,
numRows,
numColumns,
layout,
viewportSpecificData
) {
@@ -13,7 +14,9 @@ export default function setViewportToVTK(
reject(new Error('Should not have reached this point??'));
}*/
const data = {
const viewports = layout.viewports.slice();
viewports[viewportIndex] = Object.assign({}, viewports[viewportIndex], {
// plugin: 'vtk',
vtk: {
mode: 'mpr', // TODO: not used
@@ -21,18 +24,16 @@ export default function setViewportToVTK(
resolve(api);
},
},
};
const updatedViewports = setSingleLayoutData(
layout.viewports,
viewportIndex,
data
);
});
const updatedViewportData = viewportSpecificData;
setLayoutAndViewportData(
{ viewports: updatedViewports },
{
numRows,
numColumns,
viewports,
},
updatedViewportData
);
});
@@ -0,0 +1,28 @@
const subscriptions = [];
// This is pretty hacky right now, but it makes sure we don't keep adding subscriptions.
// TODO -> Nuke this and move it up a layer once we have more vigorous layout support.
const vtkViewportSubscriptionManager = {
subscriptions,
pushSubscription(viewportIndex, subscription) {
if (!Array.isArray(subscriptions[viewportIndex])) {
subscriptions[viewportIndex] = [];
}
subscriptions[viewportIndex].push(subscription);
},
unsubscribe(viewportIndex) {
if (!subscriptions[viewportIndex]) {
return;
}
while (subscriptions[viewportIndex].length) {
subscriptions[viewportIndex].pop().unsubscribe();
}
subscriptions[viewportIndex] = null;
},
};
export default vtkViewportSubscriptionManager;
+2 -1
View File
@@ -13,7 +13,6 @@
"scripts": {
"cm": "npx git-cz",
"build": "lerna run build:viewer --stream",
"build:e2e": "lerna run build:viewer:e2e --stream",
"build:ci": "lerna run build:viewer:ci --stream",
"build:demo": "lerna run build:viewer:demo --stream",
"build:package": "lerna run build:viewer:package --stream",
@@ -22,11 +21,13 @@
"dev:project": ".scripts/dev.sh",
"dev:orthanc": "lerna run dev:orthanc --stream",
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
"start": "yarn run dev",
"test": "yarn run test:unit",
"test:unit": "jest --collectCoverage",
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
"test:e2e": "lerna run test:e2e --stream",
"test:e2e:dist": "lerna run test:e2e:dist --stream",
"test:e2e:serve": "lerna run test:e2e:serve --stream",
"see-changed": "lerna changed",
"docs:publish": "chmod +x ./build-and-publish-docs.sh && ./build-and-publish-docs.sh",
"release": "yarn run lerna:version && yarn run lerna:publish",
+95
View File
@@ -3,6 +3,101 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "0.50.10",
"version": "1.3.2",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -9,7 +9,7 @@ import user from '../user';
* @export
* @param {Object} [server={}]
* @param {Object} [server.requestOptions]
* @param {string} [server.requestOptions.auth]
* @param {string|function} [server.requestOptions.auth]
* @returns {Object} { Authorization }
*/
export default function getAuthorizationHeader({ requestOptions } = {}) {
@@ -19,8 +19,13 @@ export default function getAuthorizationHeader({ requestOptions } = {}) {
const accessToken = user && user.getAccessToken && user.getAccessToken();
if (requestOptions && requestOptions.auth) {
// HTTP Basic Auth (user:password)
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
if (typeof requestOptions.auth === 'function') {
// Custom Auth Header
headers.Authorization = requestOptions.auth(requestOptions);
} else {
// HTTP Basic Auth (user:password)
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
}
} else if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
@@ -7,10 +7,7 @@ describe('getAuthorizationHeader', () => {
it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => {
const validServer = {
requestOptions: {
auth: {
user: 'dummy_user',
password: 'dummy_password',
},
auth: 'dummy_user: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', () => {
const validServerWithoutPassword = {
requestOptions: {
auth: {
user: 'dummy_user',
},
auth: 'dummy_user',
},
};
@@ -43,22 +38,19 @@ describe('getAuthorizationHeader', () => {
expect(authentication).toEqual(expectedAuthorizationHeader);
});
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no username', () => {
const validServerWithoutPassword = {
it('should return a HTTP Basic Auth when server contains requestOptions.auth custom function', () => {
const validServerCustomAuth = {
requestOptions: {
auth: {
user: 'dummy_user',
},
auth: options => `Basic ${options.token}`,
token: 'ZHVtbXlfdXNlcjpkdW1teV9wYXNzd29yZA==',
},
};
const expectedAuthorizationHeader = {
Authorization: `Basic ${btoa(
validServerWithoutPassword.requestOptions.auth
)}`,
Authorization: `Basic ${validServerCustomAuth.requestOptions.token}`,
};
const authentication = getAuthorizationHeader(validServerWithoutPassword);
const authentication = getAuthorizationHeader(validServerCustomAuth);
expect(authentication).toEqual(expectedAuthorizationHeader);
});
@@ -1,10 +1,12 @@
// import { api } from 'dicomweb-client'
const api = {
DICOMwebClient: jest.fn().mockImplementation(() => {
return {
retrieveStudyMetadata: jest.fn().mockResolvedValue([]),
};
DICOMwebClient: jest.fn().mockImplementation(function() {
this.retrieveStudyMetadata = jest.fn().mockResolvedValue([]);
this.retrieveSeriesMetadata = jest.fn(function(options) {
const { studyInstanceUID, seriesInstanceUID } = options;
return Promise.resolve([{ studyInstanceUID, seriesInstanceUID }]);
});
}),
};
-167
View File
@@ -1,167 +0,0 @@
# Table of contents
In this document, some important objects are described. In the files there are comments that can help better undestand their methods and properties.
- [ResizeViewportManager object](#the-resize-viewport-manager-object)
- [ImageSet object](#the-image-set-object)
- [Layout Manager](#the-layout-manager-object)
- [Type Safe Collections](#the-type-safe-collections)
# The Resize Viewport Manager object
This object has multiple functions to manage window resize event. It relocates Dialogs, resizes viewport elements and scrollbars and some other UI components such as Study and Series Quick Switch, when available.
## Usage
It's only necessary to bind **handleResize** function to the window resize event as follows. The **ohif:viewerbase** package needs to be imported by the referring code as well.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
const ResizeViewportManager = new Viewerbase.ResizeViewportManager();
window.addEventListener('resize', ResizeViewportManager.getResizeHandler());
```
An example os its usage can be found in **ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js**.
# The Image Set object
An object that represents a list of images that are associated by any arbitrary criteria being thus content agnostic. Besides the main attributes (**images** and **uid**) it allows additional attributes to be appended to it (currently indiscriminately, but this should be changed).
## Usage
ImageSet constructor requires an array of SOP instances like in the example below. It's necessary to import **ohif:viewerbase**.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
const imageSet = new Viewerbase.ImageSet(sopInstances);
imageSet.setAttributes({
displaySetInstanceUid: imageSet.uid,
seriesInstanceUid: seriesData.seriesInstanceUid,
seriesNumber: seriesData.seriesNumber,
seriesDescription: seriesData.seriesDescription,
numImageFrames: instances.length,
frameRate: instance.getRawValue('x00181063'),
modality: seriesData.modality,
isMultiFrame: isMultiFrame(instance)
});
// Sort instances by InstanceNumber (0020,0013)
imageSet.sortBy((a, b) => {
return (parseInt(a.getRawValue('x00200013', 0)) || 0) - (parseInt(b.getRawValue('x00200013', 0)) || 0);
});
```
Each SOP instance in this example is an instance of **OHIFInstanceMetadata** object, which is a specialization of **InstanceMetadata**. To read more about the **Metadata API** click [here](metadata/).
# The Layout Manager object
Objects of this class are responsible for creating, organizing and maintaining (manage) viewport rendering. It creates a grid, positioning viewports accordingly to it's configuration keeping all viewports data (in **viewportData** property) for easy access from other components. It support many layout configurations and some of them were fully tested: 1x1, 1x2, 1x3, 2x1, 2x2, 2x3, 3x1, 3x2, 3x3. Other configurations may work as well.
Finally it provides some useful functions to move through viewports and zoom it.
## Usage
In order to use _LayoutManager_ the **ohif:viewerbase** package needs to be imported by the referring code and instantiated as follows. An example os its usage is in **ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js**.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Get an array of studies object. This function needs to be implemented, it does not exist.
const studies = getArrayOfStudiesObjects();
const parentElement = document.getElementById('layoutManagerTarget');
const LayoutManager = new Viewerbase.LayoutManager(parentElement, studies);
```
The default configuration is 1x1, and to change it just set **layoutProps** and call **updateViewports** to update the layout as follows.
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Get an array of studies object. This function needs to be implemented, it does not exist.
const studies = getArrayOfStudiesObjects();
const parentElement = document.getElementById('layoutManagerTarget');
const LayoutManager = new LayoutManager(parentElement, studies);
// Set the layout proprerties to 2x2 layout
LayoutManager.layoutProps = {
rows: 2,
columns: 2
};
// It will render four viewports: two in each row.
LayoutManager.updateViewports();
```
The layoutManagerTarget element will have a new class **layout-2-2** (to allow further styling) and it's inner content will a new div#imageViewerViewports that has four inner elements like the following (some elements and attributes were removed for example purpose):
```html
<div class="viewportContainer active" style="height:50%; width:50%;">
<div class="removable">
<div class="imageViewerViewport">
<canvas></canvas>
</div>
<div class="imageViewerViewportOverlay"></div>
<div class="imageViewerLoadingIndicator"></div>
<div class="imageViewerErrorLoadingIndicator"></div>
<div class="viewportOrientationMarkers"></div>
</div>
</div>
```
Each of this _div.viewportContainer_ will have some classes to help CSS specific styling accordingly to the element's position in the grid: **top**, **middle** and **bottom**. This classes are added by **viewer/components/gridLayout/** component in ohif-viewerbase package.
# The Type Safe Collections
With the introduction of the new _Study Metadata API_ in which study metadata is represented by class hierarchies (using prototype-based inheritance), the usage of standard _Minimongo_ collections as a central client-side storage for this data became no longer an option. Standard _Mongo_ and _Minimongo_ collections internally _flatten_ data (in other words, data gets serialized) before storage hence no functions or prototype chains are preserved. In that scenario, when an object is restored (fetched), what is returned is actually a flattened copy of the original object with no functions or prototype (it's no longer an instance of it's original class). As an attempt to overcome this limitation a new type of collection was intruduced: the *TypeSafeCollection*.
The `TypeSafeCollection` is a simple list-like collection which tries to implement an API _similar_ but not compatible with _Mongo_'s API. It supports basic features like search by attribute map and ID, retrieval by index, sorting of result sets, insertion, removal and reactive operations but, unlike _Mongo_'s API, it (still) lacks support to advanced functionality like complex search criterea or flexible sorting options.
## Implementation
The `TypeSafeCollection` is implemented on top of the _JavaScript_ `Array` object. Each element inserted in the collection is appended to the end of its internal array as a _key-value pair (KVP)_ object where the _key_ is a unique randomly generated ID string and the _value_ is the element itself. Once the object has been successfully stored, the generated ID (its ID) is returned to the client code and can later be used to access that specific element. At this point, an important difference to the _Minimongo_ API can be highlighted: a _TypeSafeCollection_ instance will never make any changes to the stored element (e.g., no "\_id" property will ever be assigned to the original object). Another relevant feature that is supported by this design decision is that _not only objects_ can be stored in this collections, but literally _anything_.
Inside the codebase, the _value_ attribute of each _KVP_ entry in the collection is refered to as _the **payload** of the entry_ since it's what really matters to the user. Hence, this term will also be used here to refer to the _value that has been stored in the collection_. That being said, we can approach another important feature of these collections: A single _payload_ cannot be stored more than once in a given collection. When an attempt of inserting a _payload_ which is already present in the collection is detected, the insert operation will fail and `null` will be returned. In that regard, the collection behaves like `Set` object not permitting a payload to be stored more than once. Strict equality is used when comparing payloads, thus cloned objects are not considered the same. This feature adds an additional garantee that a given study/series/instance will not be listed more than once (it was designed as a replacement for central study collections which were always checked for duplicates).
Please refer to the codebase for the full `TypeSafeCollection` API.
## Usage
In order to use the `TypeSafeCollection` class, the **ohif:viewerbase** package needs to be imported by the referring code and instantiated as follows:
```javascript
import { Viewerbase } from 'meteor/ohif:viewerbase'; // i.e., Viewerbase.TypeSafeCollection
OR
import 'meteor/ohif:viewerbase'; // i.e., OHIF.viewerbase.TypeSafeCollection
// The later is preferred when the client code already makes use of the "OHIF" namespace making the second
// "import" a garantee that the ".viewerbase" namespace has been properly loaded.
```
A few usage examples:
```javascript
const Users = new OHIF.viewerbase.TypeSafeCollection();
[[ ... ]]
// Insert a User object...
let userId = Users.insert({
data: {
firstName: 'John',
lastName: 'Doe',
age: 45
},
getFullName() {
return `${this.data.firstName} ${this.data.lastName}`;
},
getAge() {
return this.data.age;
}
});
[[ ... ]]
let theUserWeJustStored = Users.findById(userId); // ;-)
[[ ... ]]
// Retrieve a single user with "Doe" as `lastName`...
let myUser = Users.findBy({ 'data.lastName': 'Doe' });
// Or all users with "Doe" as `lastName`, sorted by `firstName` in ascending
// order and using the `age` attribute to break ties in descending order...
let myUsers = Users.findAllBy({ 'data.lastName': 'Doe' }, {
sort: [ [ 'data.firstName', 'asc' ], [ 'data.age', 'desc' ] ]
});
```
@@ -8,7 +8,8 @@ import { SeriesMetadata } from './SeriesMetadata';
// - createStacks
import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from './../../utils/isImage';
import { isImage } from '../../utils/isImage';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
export class StudyMetadata extends Metadata {
constructor(data, uid) {
@@ -88,6 +89,100 @@ export class StudyMetadata extends Metadata {
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
* The series that appear in the Study Metadata must represent
@@ -101,103 +196,44 @@ export class StudyMetadata extends Metadata {
* @returns {Array} An array of series to be placed in the Study Metadata
*/
createDisplaySets(sopClassHandlerModules) {
const study = this;
const displaySets = [];
const anyDisplaySets = study.getSeriesCount();
const anySopClassHandlerModules =
sopClassHandlerModules && sopClassHandlerModules.length > 0;
const anyDisplaySets = this.getSeriesCount();
if (!anyDisplaySets) {
return displaySets;
}
// Loop through the series (SeriesMetadata)
this.forEachSeries(series => {
const anyInstances = series.getInstanceCount() > 0;
if (!anyInstances) {
return;
}
const sopClassUids = getSopClassUids(series);
if (anySopClassHandlerModules) {
const displaySet = _getDisplaySetFromSopClassModule(
this.forEachSeries(
series =>
void this._createDisplaySetsForSeries(
sopClassHandlerModules,
series,
study,
sopClassUids
);
displaySets
)
);
if (displaySet) {
displaySets.push(displaySet);
return sortDisplaySetList(displaySets);
}
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
// 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);
}
});
// TODO
displaySets.sort(_sortBySeriesNumber);
return displaySets;
/**
* 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
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
*/
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
if (this.containsSeries(series)) {
this.setDisplaySets(
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
);
return true;
}
return false;
}
/**
@@ -205,7 +241,10 @@ export class StudyMetadata extends Metadata {
* @param {Array} displaySets Array of display sets (ImageSet[])
*/
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 +253,7 @@ export class StudyMetadata extends Metadata {
* @returns {boolean} True on success, false on failure.
*/
addDisplaySet(displaySet) {
if (displaySet instanceof ImageSet) {
if (displaySet instanceof ImageSet || displaySet.sopClassModule) {
this._displaySets.push(displaySet);
return true;
}
@@ -319,6 +358,12 @@ export class StudyMetadata extends Metadata {
return found;
}
containsSeries(series) {
return (
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
);
}
/**
* Retrieve the number of series within the current study.
* @returns {number} The number of series in the current study.
@@ -649,13 +694,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 {*} 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 =
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
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
*
* @param rows
* @param columns
* @param {number} numRows
* @param {number} numColumns
*/
setLayout(rows, columns) {
if (rows < 1 && columns < 1) {
log.error(`Invalid layout ${rows} x ${columns}`);
setLayout(numRows, numColumns) {
if (numRows < 1 && numColumns < 1) {
log.error(`Invalid layout ${numRows} x ${numColumns}`);
return;
}
@@ -519,16 +519,13 @@ export default class ProtocolEngine {
}
let viewports = [];
const numViewports = rows * columns;
const numViewports = numRows * numColumns;
for (let i = 0; i < numViewports; i++) {
viewports.push({
height: `${100 / rows}%`,
width: `${100 / columns}%`,
});
viewports.push({});
}
this.options.setLayout({ viewports });
this.options.setLayout({ numRows, numColumns, viewports });
}
/**
@@ -634,14 +631,10 @@ export default class ProtocolEngine {
//console.log('renderedCallback for ' + element.id);
customSettings.forEach(customSetting => {
log.trace(
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${
customSetting.id
}`
`ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
);
log.trace(
`ProtocolEngine::currentViewportData.renderedCallback with value: ${
customSetting.value
}`
`ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
);
const setting = CustomViewportSettings[customSetting.id];
+27 -8
View File
@@ -26,9 +26,34 @@ export const setViewportActive = viewportIndex => ({
viewportIndex,
});
export const setLayout = layout => ({
/**
* @param {object} layout
* @param {number} layout.numRows
* @param {number} layout.numColumns
* @param {array} layout.viewports
*/
export const setLayout = ({ numRows, numColumns, viewports }) => ({
type: SET_VIEWPORT_LAYOUT,
layout,
numRows,
numColumns,
viewports,
});
/**
* @param {object} layout
* @param {number} layout.numRows
* @param {number} layout.numColumns
* @param {array} layout.viewports
*/
export const setViewportLayoutAndData = (
{ numRows, numColumns, viewports },
viewportSpecificData
) => ({
type: SET_VIEWPORT_LAYOUT_AND_DATA,
numRows,
numColumns,
viewports,
viewportSpecificData,
});
export const clearViewportSpecificData = viewportIndex => ({
@@ -87,12 +112,6 @@ export const setServers = servers => ({
servers,
});
export const setViewportLayoutAndData = (layout, viewportSpecificData) => ({
type: SET_VIEWPORT_LAYOUT_AND_DATA,
layout,
viewportSpecificData,
});
const actions = {
// VIEWPORT
setViewportActive,
+11 -10
View File
@@ -94,19 +94,20 @@ describe('actions', () => {
});
it('should create an action to set the viewport layout', () => {
const layout = {
viewports: [
{
height: '100%',
width: '100%',
},
],
};
const numRows = 1;
const numColumns = 2;
const viewports = [{ plugin: 'vtk' }, { plugin: 'vtk' }];
const expectedAction = {
type: types.SET_VIEWPORT_LAYOUT,
layout,
numRows,
numColumns,
viewports,
};
expect(actions.setLayout(layout)).toEqual(expectedAction);
expect(actions.setLayout({ numRows, numColumns, viewports })).toEqual(
expectedAction
);
});
});
});
+2
View File
@@ -1,11 +1,13 @@
import actions from './actions.js';
import reducers from './reducers';
import localStorage from './localStorage.js';
import sessionStorage from './sessionStorage.js';
const redux = {
reducers,
actions,
localStorage,
sessionStorage,
};
export default redux;
+1 -1
View File
@@ -2,7 +2,7 @@ import redux from './index.js';
describe('redux exports', () => {
test('have not changed', () => {
const expectedExports = ['actions', 'reducers', 'localStorage'].sort();
const expectedExports = ['actions', 'reducers', 'localStorage', 'sessionStorage'].sort();
const exports = Object.keys(redux).sort();
+4 -2
View File
@@ -1,6 +1,8 @@
const LocalStorageApi = window.localStorage;
const localStorageKey = 'state';
export const loadState = () => {
try {
const serializedState = window.localStorage.getItem('state');
const serializedState = LocalStorageApi.getItem(localStorageKey);
if (!serializedState) {
return undefined;
}
@@ -14,7 +16,7 @@ export const loadState = () => {
export const saveState = state => {
try {
const serializedState = JSON.stringify(state);
localStorage.setItem('state', serializedState);
LocalStorageApi.setItem(localStorageKey, serializedState);
} catch (e) {}
};
+23 -9
View File
@@ -12,13 +12,13 @@ import cloneDeep from 'lodash.clonedeep';
import merge from 'lodash.merge';
const defaultState = {
numRows: 1,
numColumns: 1,
activeViewportIndex: 0,
layout: {
viewports: [
{
// plugin: 'cornerstone',
height: '100%',
width: '100%',
},
],
},
@@ -38,17 +38,31 @@ const viewports = (state = defaultState, action) => {
let viewportSpecificData;
let useActiveViewport = false;
switch (action.type) {
case SET_VIEWPORT_LAYOUT_AND_DATA:
return Object.assign({}, state, {
viewportSpecificData: action.viewportSpecificData,
layout: action.layout,
});
case SET_VIEWPORT_ACTIVE:
return Object.assign({}, state, {
activeViewportIndex: action.viewportIndex,
});
case SET_VIEWPORT_LAYOUT:
return Object.assign({}, state, { layout: action.layout });
case SET_VIEWPORT_LAYOUT: {
const { numRows, numColumns, viewports } = action;
const layout = {
viewports: [...viewports],
};
return Object.assign({}, state, { numRows, numColumns, layout });
}
case SET_VIEWPORT_LAYOUT_AND_DATA: {
const { numRows, numColumns, viewports, viewportSpecificData } = action;
const layout = {
viewports: [...viewports],
};
return Object.assign({}, state, {
numRows,
numColumns,
layout,
viewportSpecificData: cloneDeep(viewportSpecificData),
});
}
case SET_VIEWPORT: {
const layout = cloneDeep(state.layout);
const hasPlugin = action.data && action.data.plugin;
@@ -6,13 +6,10 @@ describe('viewports reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual({
activeViewportIndex: 0,
numRows: 1,
numColumns: 1,
layout: {
viewports: [
{
height: '100%',
width: '100%',
},
],
viewports: [{}],
},
viewportSpecificData: {},
});
@@ -34,23 +31,25 @@ describe('viewports reducer', () => {
it('should handle SET_VIEWPORT_LAYOUT', () => {
const setViewportLayoutAction = {
type: types.SET_VIEWPORT_LAYOUT,
layout: {
viewports: [
{
height: '100%',
width: '50%',
},
{
height: '100%',
width: '50%',
},
],
},
numRows: 1,
numColumns: 2,
viewports: [
{
plugin: 'cornerstone',
},
{
plugin: 'vtk',
},
],
};
const updatedState = reducer({}, setViewportLayoutAction);
expect(updatedState.layout).toEqual(setViewportLayoutAction.layout);
expect(updatedState.numRows).toEqual(setViewportLayoutAction.numRows);
expect(updatedState.numColumns).toEqual(setViewportLayoutAction.numColumns);
expect(updatedState.layout.viewports).toEqual(
setViewportLayoutAction.viewports
);
});
// If there were previous keys, this would have
+28
View File
@@ -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,5 +1,8 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import isLowPriorityModality from '../../../utils/isLowPriorityModality';
const INFO = Symbol('INFO');
const WADOProxy = {
convertURL: (url, server) => {
@@ -94,9 +97,7 @@ function buildInstanceWadoRsUri(
seriesInstanceUid,
sopInstanceUid
) {
return `${
server.wadoRoot
}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
@@ -133,7 +134,8 @@ function getSourceImageInstanceUid(instance) {
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length
SourceImageSequence.Value.length &&
SourceImageSequence.Value[0]['00081155'].Value
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
@@ -165,9 +167,9 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0]
const byteArray = new Uint8Array(arraybuffer)
const lut = []
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
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
* Returns an object populated with study metadata, including the
* series list.
*
* @param server
* @param studyInstanceUid
* @param resultData
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
* @param {Object} server Object with server configuration paramenters
* @param {Array} sopInstanceList List of SOP Instances that build up to the study
* @resolves {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
if (!resultData.length) {
return;
async function createStudyFromSOPInstanceList(server, sopInstanceList) {
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
const firstSopInstance = sopInstanceList[0];
const study = createStudy(server, firstSopInstance);
await addInstancesToStudy(server, study, sopInstanceList);
return study;
}
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;
throw new Error('Failed to create study out of provided SOP instance list');
}
/**
* Retrieve Study MetaData from a DICOM server using a WADO call
*
* @param server
* @param studyInstanceUid
* @returns {Promise}
* 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 {Object} server Object with server configuration paramenters
* @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
* @returns {Object} A study descriptor object
*/
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,
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
studyInstanceUID: studyInstanceUid,
};
});
return dicomWeb
.retrieveStudyMetadata({ studyInstanceUID })
.then(result => createStudyFromSOPInstanceList(server, result));
}
return dicomWeb.retrieveStudyMetadata(options).then(result => {
return resultDataToStudyMetadata(server, studyInstanceUid, result);
async function lazyLoadStudyMetadata(server, studyInstanceUid) {
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;
@@ -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;
+9 -4
View File
@@ -5,11 +5,16 @@ function getWADORSImageUrl(instance, frame) {
return;
}
// We need to sum 1 because WADO-RS frame number is 1-based
frame = frame ? parseInt(frame) + 1 : 1;
// Use null to obtain an imageId which represents the instance
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}
wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`);
// Replaces /frame/1 by /frame/{frame}
wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`);
}
return wadorsuri;
}
+6
View File
@@ -8,6 +8,8 @@ import sortBy from './sortBy.js';
import studyMetadataManager from './studyMetadataManager';
import updateMetaDataManager from './updateMetaDataManager.js';
import writeScript from './writeScript.js';
import DicomLoaderService from './dicomLoaderService.js';
import * as urlUtil from './urlUtil';
const utils = {
guid,
@@ -21,6 +23,8 @@ const utils = {
// Updates WADO-RS metaDataManager
updateMetaDataManager,
DICOMTagDescriptions,
DicomLoaderService,
urlUtil,
};
export {
@@ -35,6 +39,8 @@ export {
// Updates WADO-RS metaDataManager
updateMetaDataManager,
DICOMTagDescriptions,
DicomLoaderService,
urlUtil,
};
export default utils;
+2
View File
@@ -14,6 +14,8 @@ describe('Top level exports', () => {
// Updates WADO-RS metaDataManager
'updateMetaDataManager',
'DICOMTagDescriptions',
'DicomLoaderService',
'urlUtil'
].sort();
const exports = Object.keys(utils.default).sort();
@@ -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();
};
export default function updateMetaDataManager(study) {
study.seriesList.forEach(series => {
series.instances.forEach(instance => {
// Cache just images that are going to be loaded via WADO-RS
if (
instance.imageRendering !== 'wadors' &&
instance.thumbnailRendering !== 'wadors'
) {
return;
}
/**
* Update metadata manager with instances of a specifc series
* @param {Object} study A plain study descriptor object
* @param {Object} series A Series descriptor object contaning the instances to be added to the manager
*/
function updateMetaDataManagerForSeries(study, series) {
series.instances.forEach(instance => {
// Cache just images that are going to be loaded via WADO-RS
if (
instance.imageRendering !== 'wadors' &&
instance.thumbnailRendering !== 'wadors'
) {
return;
}
const metaData = getWadoRsInstanceMetaData(study, series, instance);
const numberOfFrames = instance.numberOfFrames || 1;
const metaData = getWadoRsInstanceMetaData(study, series, instance);
const numberOfFrames = instance.numberOfFrames || 1;
// We can share the same metaData with all frames because it doesn't have
// any frame specific data, such as frameNumber, pixelData, offset, etc.
// WADO-RS frame number is 1-based
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
const imageId = getWADORSImageId(instance, frameNumber);
// We can share the same metaData with all frames because it doesn't have
// any frame specific data, such as frameNumber, pixelData, offset, etc.
// WADO-RS frame number is 1-based
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
const imageId = getWADORSImageId(instance, frameNumber);
// TODO Drop dependency on this
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
imageId,
metaData
);
}
});
// TODO Drop dependency on this
cornerstoneWADOImageLoader.wadors.metaDataManager.add(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);
});
}
}
+67
View File
@@ -0,0 +1,67 @@
import lib from 'query-string';
const PARAM_SEPARATOR = ';';
const PARAM_PATTERN_IDENTIFIER = ':';
function toLowerCaseFirstLetter(word) {
return word[0].toLowerCase() + word.slice(1);
}
const getFilters = (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 queryString = {
getQueryFilters: getFilters,
};
const paramString = {
parseParam: parseParam,
replaceParam: replaceParam,
};
export { parse, queryString, paramString };
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.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)
**Note:** Version bump only for package @ohif/i18n
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.50.4",
"version": "0.50.5",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
@@ -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)",
"Previous image": "$t(Common:Previous) $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"
}
+1 -1
View File
@@ -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",
"Previous image": "$t(Common:Image) $t(Common:Previous)",
"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)",
"Previous image": "$t(Common:Previous) $t(Common:Image)",
"Skip to first image": "$t(Common:Image)最初にスキップ",
+1 -1
View File
@@ -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)",
"Previous image": "$t(Common:Previous) $t(Common:Image)",
"Skip to first image": "Bỏ qua đến đầu $t(Common:Image)",
+71
View File
@@ -3,6 +3,77 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.51.2",
"version": "0.53.4",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -1,162 +0,0 @@
import './LayoutManager.css';
import React, { Component } from 'react';
import LayoutPanelDropTarget from './LayoutPanelDropTarget.js';
import PropTypes from 'prop-types';
function defaultViewportPlugin(props) {
return <div>{JSON.stringify(props)}</div>;
}
function EmptyViewport() {
return (
<div className="EmptyViewport">
<p>Please drag a stack here to view images.</p>
</div>
);
}
export class LayoutManager extends Component {
static className = 'LayoutManager';
static defaultProps = {
viewportData: [],
layout: {
viewports: [
{
top: 0,
left: 0,
height: '100%',
width: '100%',
},
],
},
activeViewportIndex: 0,
supportsDragAndDrop: true,
availablePlugins: {
defaultViewportPlugin,
},
defaultPlugin: 'defaultViewportPlugin',
};
static propTypes = {
viewportData: PropTypes.array.isRequired,
supportsDragAndDrop: PropTypes.bool.isRequired,
activeViewportIndex: PropTypes.number.isRequired,
layout: PropTypes.object.isRequired,
availablePlugins: PropTypes.object.isRequired,
setViewportData: PropTypes.func,
studies: PropTypes.array,
children: PropTypes.node,
};
onDrop = ({ viewportIndex, item }) => {
if (this.props.setViewportData) {
this.props.setViewportData({ viewportIndex, item });
}
};
getPluginComponent = plugin => {
const pluginComponent = this.props.availablePlugins[
plugin || this.props.defaultPlugin
];
if (!pluginComponent) {
throw new Error(
`No Viewport Plugin available for plugin ${plugin}. Available plugins: ${JSON.stringify(
this.props.availablePlugins
)}`
);
}
return pluginComponent;
};
getChildComponent(plugin, data, viewportIndex, children) {
if (data.displaySet) {
const PluginComponent = this.getPluginComponent(plugin);
return (
<PluginComponent
viewportData={data}
viewportIndex={viewportIndex}
children={[children]}
/>
);
}
return <EmptyViewport />;
}
getContent(childComponent, supportsDragAndDrop, viewportIndex) {
if (supportsDragAndDrop) {
return (
<LayoutPanelDropTarget
onDrop={this.onDrop}
viewportIndex={viewportIndex}
>
{childComponent}
</LayoutPanelDropTarget>
);
}
return <div className="LayoutPanel">{childComponent}</div>;
}
render() {
if (!this.props.viewportData.length) {
return '';
}
const { supportsDragAndDrop, studies, viewportData } = this.props;
const viewports = this.props.layout.viewports;
const viewportElements = viewports.map((layout, viewportIndex) => {
const displaySet = viewportData[viewportIndex];
const data = {
displaySet,
studies,
};
// Use whichever plugin is currently in use in the panel
// unless nothing is specified. If nothing is specified
// and the display set has a plugin specified, use that.
//
// TODO: Change this logic to:
// - Plugins define how capable they are of displaying a SopClass
// - When updating a panel, ensure that the currently enabled plugin
// in the viewport is capable of rendering this display set. If not
// then use the most capable available plugin
let plugin = layout.plugin;
if (!layout.plugin && displaySet && displaySet.plugin) {
plugin = displaySet.plugin;
}
const childComponent = this.getChildComponent(
plugin,
data,
viewportIndex,
this.props.children
);
const content = this.getContent(
childComponent,
supportsDragAndDrop,
viewportIndex
);
let className = 'viewport-container';
if (this.props.activeViewportIndex === viewportIndex) {
className += ' active';
}
return (
<div key={viewportIndex} className={className} style={{ ...layout }}>
{content}
</div>
);
});
return <div className={LayoutManager.className}>{viewportElements}</div>;
}
}
export default LayoutManager;
@@ -1,12 +0,0 @@
.LayoutPanelDropTarget {
width: 100%;
height: 100%;
opacity: 1;
transition: 0.3s all ease;
}
.LayoutPanelDropTarget.hovered {
opacity: 0.5;
cursor: copy;
}
@@ -1,74 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { DropTarget } from 'react-dnd';
import './LayoutPanelDropTarget.css';
// Drag sources and drop targets only interact
// if they have the same string type.
const Types = {
THUMBNAIL: 'thumbnail',
};
const divTarget = {
drop(props, monitor, component) {
const item = monitor.getItem();
if (props.onDrop) {
props.onDrop({
viewportIndex: props.viewportIndex,
item,
});
}
return {
id: `LayoutPanelDropTarget-${props.viewportIndex}`,
viewportIndex: props.viewportIndex,
item,
};
},
};
// TODO: Find out why we can't move this into the Example app instead.
// It looks like the context isn't properly shared.
class LayoutPanelDropTarget extends Component {
static className = 'LayoutPanelDropTarget';
static defaultProps = {
isOver: false,
canDrop: false,
};
static propTypes = {
connectDropTarget: PropTypes.func.isRequired,
canDrop: PropTypes.bool.isRequired,
isOver: PropTypes.bool.isRequired,
viewportComponent: PropTypes.object,
};
render() {
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
let className = LayoutPanelDropTarget.className;
if (isActive) {
className += ' hovered';
} else if (canDrop) {
className += ' can-drop';
}
return connectDropTarget(
<div className={className}>{this.props.children}</div>
);
}
}
const collect = (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
canDrop: monitor.canDrop(),
isOver: monitor.isOver(),
});
export default DropTarget(Types.THUMBNAIL, divTarget, collect)(
LayoutPanelDropTarget
);
+2 -2
View File
@@ -33,8 +33,8 @@ as easy as possible. You will, however, need to include either the default font
or the font you set for your theme:
```js
// Google Fonts, Sanchez & Roboto
'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700|Sanchez&display=swap';
// Google Fonts, Roboto
'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap';
```
`@ohif/ui` also looks for theme CSS variabled defined on `:root`. You can find a
+21 -18
View File
@@ -1,18 +1,19 @@
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="UTF-8">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ title }}</title>
<head>
<meta charset="UTF-8">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="/public/docz.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700|Sanchez&display=swap">
<title>{{ title }}</title>
<style>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="/public/docz.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap">
<style>
/* "Tide" theme */
/* TODO:
- Convert the other themes to CSS Variables
@@ -40,7 +41,7 @@
--table-text-primary-color: #ffffff;
--table-text-secondary-color: #91b9cd;
--large-numbers-color:#6FBDE2;
--large-numbers-color: #6FBDE2;
--state-error: #FFCCCC;
--state-error-border: #FFCCCC;
@@ -70,11 +71,13 @@
body {
font-family: 'Roboto', sans-serif !important;
}
</style>
{{ head }}
</head>
<body>
<div id="root" />
{{ footer }}
</body>
</style>
{{ head }}
</head>
<body>
<div id="root" />
{{ footer }}
</body>
</html>
@@ -6,15 +6,29 @@ export class Checkbox extends Component {
static propTypes = {
label: PropTypes.string.isRequired,
checked: PropTypes.bool,
onChange: PropTypes.func,
};
constructor(props) {
super(props);
this.state = { checked: props.checked, label: props.label };
this.state = { checked: !!props.checked, label: props.label };
}
handleChange(e) {
this.setState({ checked: e.target.checked });
const checked = e.target.checked;
this.setState({ checked });
if (this.props.onChange) this.props.onChange(checked);
}
componentDidUpdate(props) {
const { checked = false, label } = props;
if (this.state.checked !== checked || this.state.label !== label) {
this.setState({
checked,
label,
});
}
}
render() {
@@ -32,9 +46,7 @@ export class Checkbox extends Component {
<input
type="checkbox"
checked={this.state.checked}
onChange={e => {
this.handleChange(e);
}}
onChange={this.handleChange.bind(this)}
/>
{checkbox}
{this.state.label}
+3 -6
View File
@@ -1,8 +1,4 @@
import {
ExampleDropTarget,
StudyBrowser,
ThumbnailEntry,
} from './studyBrowser';
import { StudyBrowser, ThumbnailEntry } from './studyBrowser';
import { LayoutButton, LayoutChooser } from './layoutButton';
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
import { Overlay, OverlayTrigger } from './overlayTrigger';
@@ -13,6 +9,7 @@ import {
UserPreferencesModal,
} from './userPreferencesModal';
import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog';
import { QuickSwitch } from './quickSwitch';
import { RoundedButtonGroup } from './roundedButtonGroup';
@@ -23,8 +20,8 @@ import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip';
export {
Checkbox,
CineDialog,
ExampleDropTarget,
LayoutButton,
LayoutChooser,
MeasurementTable,
@@ -1,3 +1,4 @@
/* global cornerstone */
import './ImageThumbnail.styl';
import React, { PureComponent } from 'react';
@@ -12,25 +13,6 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
// - Pass in the entire load and render function as a prop?
//import cornerstone from 'cornerstone-core';
/**
* Asynchronous wrapper around Cornerstone's renderToCanvas method.
*
* @param {HTMLElement} canvasElement An HTML <canvas> element
* @param {Image} image A Cornerstone Image
*
* @return {Promise} A promise tracking the progress of the rendering. Resolves empty.
*/
function renderAsync(canvasElement, image) {
return new Promise((resolve, reject) => {
try {
cornerstone.renderToCanvas(canvasElement, image);
resolve();
} catch (error) {
reject(error);
}
});
}
export default class ImageThumbnail extends PureComponent {
static propTypes = {
imageSrc: PropTypes.string,
@@ -52,40 +34,32 @@ export default class ImageThumbnail extends PureComponent {
super(props);
this.canvas = React.createRef();
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
this.state = {
loading: renderIntoCanvas,
loading: this.shouldRenderToCanvas(),
};
}
shouldRenderToCanvas() {
return this.props.imageId && !this.props.imageSrc;
}
componentDidMount() {
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
if (renderIntoCanvas) {
const { imageId } = this.props;
const canvas = this.canvas.current;
cornerstone.loadAndCacheImage(imageId).then(
image => {
renderAsync(canvas, image).then(
() => {
this.setState({
loading: false,
});
},
error => {
// TODO: Set state?
throw new Error(error);
}
);
},
error => {
// TODO: Set state?
if (this.shouldRenderToCanvas()) {
cornerstone
.loadAndCacheImage(this.props.imageId)
.then(image => {
cornerstone.renderToCanvas(this.canvas.current, image);
this.setState({
loading: false,
});
})
.catch(error => {
this.setState({
loading: false,
error: true,
});
throw new Error(error);
}
);
});
}
}
@@ -100,12 +74,10 @@ export default class ImageThumbnail extends PureComponent {
const showStackLoadingProgressBar =
this.props.stackPercentComplete !== undefined;
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
return (
<div className="ImageThumbnail">
<div className="image-thumbnail-canvas">
{renderIntoCanvas ? (
{this.shouldRenderToCanvas() ? (
<canvas
ref={this.canvas}
width={this.props.width}
@@ -130,6 +102,9 @@ export default class ImageThumbnail extends PureComponent {
/>
</div>
)}
{this.state.loading && (
<div className="image-thumbnail-loading-indicator"></div>
)}
</div>
);
}
@@ -26,6 +26,36 @@
-webkit-background-clip: padding
background-clip: padding-box
// Thumbnail Loading Indicator
.image-thumbnail-loading-indicator
position: absolute
left: 50%
top: 50%
margin: -22px 0 0 -22px
display: block
width: 44px
height: 44px
&:after
content: " "
display: block
width: 32px
height: 32px
margin: 1px
border-radius: 50%
border: 5px solid var(--ui-border-color-dark)
border-color: var(--ui-border-color-dark) transparent var(--ui-border-color-dark) transparent
animation: image-thumbnail-loading-indicator 1.2s linear infinite
@keyframes image-thumbnail-loading-indicator
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
// The End...
.image-thumbnail-canvas
height: 100%
overflow: hidden
@@ -36,7 +66,6 @@
-webkit-user-drag: none
pointer-events: none
.thumbnailLoadingIndicator
display: none
pointer-events: none
@@ -25,7 +25,7 @@ class StudyBrowser extends Component {
if (this.props.supportsDragAndDrop) {
return (
<ThumbnailEntryDragSource
key={`${studyIndex}_${thumbIndex}`}
key={thumb.displaySetInstanceUid}
{...study}
{...thumb}
id={`${studyIndex}_${thumbIndex}`}
@@ -35,9 +35,9 @@ class StudyBrowser extends Component {
);
} else {
return (
<div className="ThumbnailEntryContainer">
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
<ThumbnailEntry
key={`${studyIndex}_${thumbIndex}`}
key={thumb.displaySetInstanceUid}
{...study}
{...thumb}
id={`${studyIndex}_${thumbIndex}`}
@@ -38,7 +38,6 @@ class ThumbnailEntry extends Component {
let className = classnames('ThumbnailEntry noselect', {
active: this.props.active,
});
const infoOnly = false;
let contents = null;
if (this.props.imageSrc || this.props.imageId) {
@@ -54,8 +53,10 @@ class ThumbnailEntry extends Component {
);
} else if (this.props.altImageText) {
contents = (
<div className={'alt-image-text p-x-1'}>
<h1>{this.props.altImageText}</h1>
<div className="p-x-1">
<div className="alt-image-text">
<h1>{this.props.altImageText}</h1>
</div>
</div>
);
}
@@ -69,23 +70,23 @@ class ThumbnailEntry extends Component {
>
{contents}
<div
className={infoOnly ? 'series-details info-only' : 'series-details'}
className="series-details"
>
<div className="series-description">
{this.props.seriesDescription}
</div>
<div className="series-information">
<div className="item item-series clearfix">
<div className="item item-series">
<div className="icon">S:</div>
<div className="value">{this.props.seriesNumber}</div>
</div>
{hasInstanceNumber && (
<div className="item item-series clearfix">
<div className="item item-series">
<div className="icon">I:</div>
<div className="value">{this.props.instanceNumber}</div>
</div>
)}
<div className="item item-frames clearfix">
<div className="item item-frames">
<div className="icon">
<div />
</div>
@@ -31,8 +31,6 @@
color: var(--text-primary-color);
.series-details
display: flex;
flex-direction: row;
color: var(--text-primary-color);
font-size: 14px
line-height: 1.3em
@@ -44,9 +42,6 @@
position: relative
word-wrap: break-word
.series-description
flex-grow: 1
&.info-only
.series-description
display: none
@@ -67,36 +62,33 @@
float: none
line-height: 25px
.value
margin-left: 0
width: auto
.series-information
padding-right: 4px
max-width: 50px
display: flex
flex-direction: row
align-items: center
.item
flex: 1
text-align: center
.item-frames .icon
height: 18px
.value
color: var(--text-secondary-color);
display: inline-block
float: right
display: inline
font-size: 12px
margin-left: 4px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
width: calc(100% - 15px)
.icon
color: var(--active-color);
display: inline-block
float: left
display: inline
font-size: 10px
font-weight: 900
text-align: right
width: 11px
div
background-color: var(--series-count-background-color);
@@ -45,7 +45,7 @@ class ThumbnailEntryDragSource extends Component {
const dropEffect = 'copy';
return connectDragSource(
<div className="ThumbnailEntryContainer">
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
<DragPreview {...this.props} />
<ThumbnailEntry {...this.props} />
</div>,
@@ -8,7 +8,8 @@ import {
onThumbnailClick,
onThumbnailDoubleClick,
} from './exampleStudies.js';
import { ExampleDropTarget, StudyBrowser } from './../index.js';
import ExampleDropTarget from './ExampleDropTarget.js';
import { StudyBrowser } from './../index.js';
class StudyBrowserContainer extends Component {
render() {
@@ -1,3 +1,2 @@
export { ExampleDropTarget } from './ExampleDropTarget.js';
export { StudyBrowser } from './StudyBrowser.js';
export { ThumbnailEntry } from './ThumbnailEntry.js';
+2
View File
@@ -53,6 +53,7 @@ import measureTargetUn from './icons/measure-target-un.svg';
import measureTemp from './icons/measure-temp.svg';
import objectGroup from './icons/object-group.svg';
import ohifLogo from './icons/ohif-logo.svg';
import ohifTextLogo from './icons/ohif-text-logo.svg';
import oval from './icons/oval.svg';
import palette from './icons/palette.svg';
import play from './icons/play.svg';
@@ -120,6 +121,7 @@ const ICONS = {
'inline-edit': inlineEdit,
list,
'ohif-logo': ohifLogo,
'ohif-text-logo': ohifTextLogo,
lock,
play,
database,
@@ -0,0 +1,3 @@
<svg viewBox="0 0 224 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.416 5.932C10.416 7.46267 9.954 8.70867 9.03 9.67C8.11533 10.622 6.958 11.098 5.558 11.098C4.16733 11.098 3.00533 10.6173 2.072 9.656C1.13867 8.68533 0.672 7.43933 0.672 5.918C0.672 4.38733 1.15267 3.16467 2.114 2.25C3.07533 1.326 4.22333 0.863999 5.558 0.863999C6.89267 0.863999 8.036 1.326 8.988 2.25C9.94 3.16467 10.416 4.392 10.416 5.932ZM3.052 8.9C3.77067 9.60933 4.606 9.964 5.558 9.964C6.51 9.964 7.34067 9.60933 8.05 8.9C8.75933 8.18133 9.114 7.20133 9.114 5.96C9.114 4.70933 8.75 3.734 8.022 3.034C7.30333 2.32467 6.482 1.97 5.558 1.97C4.634 1.97 3.808 2.32467 3.08 3.034C2.352 3.734 1.988 4.70933 1.988 5.96C1.988 7.20133 2.34267 8.18133 3.052 8.9ZM14.6296 14.15H11.8156C11.6942 14.15 11.6336 14.094 11.6336 13.982V13.38C11.6336 13.268 11.6942 13.212 11.8156 13.212H12.5996V5.05H11.8436C11.7222 5.05 11.6616 4.994 11.6616 4.882V4.28C11.6616 4.168 11.7222 4.112 11.8436 4.112H13.4816C13.6029 4.112 13.6636 4.168 13.6636 4.28L13.6496 5.386H13.6636C13.8409 5.00333 14.1256 4.68133 14.5176 4.42C14.9189 4.14933 15.4089 4.014 15.9876 4.014C16.9302 4.014 17.6909 4.336 18.2696 4.98C18.8482 5.61467 19.1376 6.464 19.1376 7.528C19.1376 8.58267 18.8529 9.44133 18.2836 10.104C17.7142 10.7667 16.9536 11.098 16.0016 11.098C15.4229 11.098 14.9376 10.9673 14.5456 10.706C14.1629 10.4353 13.8876 10.1087 13.7196 9.726H13.6776C13.7056 10.0247 13.7196 10.2393 13.7196 10.37V13.212H14.6296C14.7509 13.212 14.8116 13.268 14.8116 13.38V13.982C14.8116 14.094 14.7509 14.15 14.6296 14.15ZM13.6916 7.584C13.6916 8.396 13.9016 9.02133 14.3216 9.46C14.7509 9.89867 15.2642 10.118 15.8616 10.118C16.4682 10.118 16.9722 9.88467 17.3736 9.418C17.7842 8.942 17.9896 8.32133 17.9896 7.556C17.9896 6.78133 17.7842 6.16067 17.3736 5.694C16.9722 5.22733 16.4682 4.994 15.8616 4.994C15.2549 4.994 14.7416 5.22267 14.3216 5.68C13.9016 6.13733 13.6916 6.772 13.6916 7.584ZM26.3811 7.696H21.3411C21.3785 8.47067 21.6211 9.068 22.0691 9.488C22.5265 9.908 23.0398 10.118 23.6091 10.118C24.1785 10.118 24.6638 9.99667 25.0651 9.754C25.4758 9.51133 25.7511 9.27333 25.8911 9.04C25.9658 8.94667 26.0498 8.93267 26.1431 8.998L26.5211 9.376C26.5771 9.432 26.6051 9.48333 26.6051 9.53C26.6051 9.56733 26.5818 9.61867 26.5351 9.684C26.4978 9.74933 26.4138 9.852 26.2831 9.992C26.1525 10.132 25.9658 10.2907 25.7231 10.468C25.4898 10.6453 25.1818 10.7947 24.7991 10.916C24.4258 11.0373 24.0245 11.098 23.5951 11.098C22.6151 11.098 21.7938 10.776 21.1311 10.132C20.4778 9.488 20.1511 8.62467 20.1511 7.542C20.1511 6.45 20.4778 5.59133 21.1311 4.966C21.7845 4.33133 22.5825 4.014 23.5251 4.014C24.4771 4.014 25.2238 4.322 25.7651 4.938C26.3065 5.54467 26.5771 6.366 26.5771 7.402C26.5771 7.43933 26.5771 7.47667 26.5771 7.514C26.5771 7.63533 26.5118 7.696 26.3811 7.696ZM25.4151 6.842C25.3871 6.26333 25.2051 5.80133 24.8691 5.456C24.5331 5.11067 24.0851 4.938 23.5251 4.938C22.9651 4.938 22.4845 5.09667 22.0831 5.414C21.6818 5.73133 21.4438 6.20733 21.3691 6.842H25.4151ZM35.1323 11H33.8023C33.401 11 33.2003 10.804 33.2003 10.412V6.73C33.2003 6.18867 33.0697 5.76867 32.8083 5.47C32.547 5.17133 32.1643 5.022 31.6603 5.022C30.9417 5.022 30.3817 5.316 29.9803 5.904C29.9337 5.96 29.873 6.06267 29.7983 6.212V10.062H30.5403C30.6617 10.062 30.7223 10.118 30.7223 10.23V10.832C30.7223 10.944 30.6617 11 30.5403 11H27.8663C27.745 11 27.6843 10.944 27.6843 10.832V10.23C27.6843 10.118 27.745 10.062 27.8663 10.062H28.6363V5.05H27.8663C27.745 5.05 27.6843 4.994 27.6843 4.882V4.28C27.6843 4.168 27.745 4.112 27.8663 4.112H29.5043C29.6257 4.112 29.6863 4.168 29.6863 4.28L29.6583 5.232H29.7003C30.1857 4.42 30.937 4.014 31.9543 4.014C32.701 4.014 33.2843 4.23333 33.7043 4.672C34.1243 5.11067 34.339 5.70333 34.3483 6.45V10.062H35.1323C35.2537 10.062 35.3143 10.118 35.3143 10.23V10.832C35.3143 10.944 35.2537 11 35.1323 11ZM50.8763 11H47.5023C47.3903 11 47.3343 10.944 47.3343 10.832V10.118C47.3343 10.006 47.3903 9.95 47.5023 9.95H48.5803V6.52H43.3723V9.95H44.4503C44.5717 9.95 44.6323 10.006 44.6323 10.118V10.832C44.6323 10.944 44.5717 11 44.4503 11H41.0903C40.969 11 40.9083 10.944 40.9083 10.832V10.118C40.9083 10.006 40.969 9.95 41.0903 9.95H42.1543V1.998H41.0903C40.969 1.998 40.9083 1.942 40.9083 1.83V1.116C40.9083 1.004 40.969 0.947999 41.0903 0.947999H44.4503C44.5717 0.947999 44.6323 1.004 44.6323 1.116V1.83C44.6323 1.942 44.5717 1.998 44.4503 1.998H43.3723V5.498H48.5803V1.998H47.5023C47.3903 1.998 47.3343 1.942 47.3343 1.83V1.116C47.3343 1.004 47.3903 0.947999 47.5023 0.947999H50.8763C50.9883 0.947999 51.0443 1.004 51.0443 1.116V1.83C51.0443 1.942 50.9883 1.998 50.8763 1.998H49.7983V9.95H50.8763C50.9883 9.95 51.0443 10.006 51.0443 10.118V10.832C51.0443 10.944 50.9883 11 50.8763 11ZM58.0452 7.696H53.0052C53.0425 8.47067 53.2852 9.068 53.7332 9.488C54.1905 9.908 54.7039 10.118 55.2732 10.118C55.8425 10.118 56.3279 9.99667 56.7292 9.754C57.1399 9.51133 57.4152 9.27333 57.5552 9.04C57.6299 8.94667 57.7139 8.93267 57.8072 8.998L58.1852 9.376C58.2412 9.432 58.2692 9.48333 58.2692 9.53C58.2692 9.56733 58.245Line truncated
</svg>

After

Width:  |  Height:  |  Size: 25 KiB

@@ -1,3 +1,3 @@
<svg width="3.73em" height=".25em" viewBox="0 0 224 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 224 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.416 5.932C10.416 7.46267 9.954 8.70867 9.03 9.67C8.11533 10.622 6.958 11.098 5.558 11.098C4.16733 11.098 3.00533 10.6173 2.072 9.656C1.13867 8.68533 0.672 7.43933 0.672 5.918C0.672 4.38733 1.15267 3.16467 2.114 2.25C3.07533 1.326 4.22333 0.863999 5.558 0.863999C6.89267 0.863999 8.036 1.326 8.988 2.25C9.94 3.16467 10.416 4.392 10.416 5.932ZM3.052 8.9C3.77067 9.60933 4.606 9.964 5.558 9.964C6.51 9.964 7.34067 9.60933 8.05 8.9C8.75933 8.18133 9.114 7.20133 9.114 5.96C9.114 4.70933 8.75 3.734 8.022 3.034C7.30333 2.32467 6.482 1.97 5.558 1.97C4.634 1.97 3.808 2.32467 3.08 3.034C2.352 3.734 1.988 4.70933 1.988 5.96C1.988 7.20133 2.34267 8.18133 3.052 8.9ZM14.6296 14.15H11.8156C11.6942 14.15 11.6336 14.094 11.6336 13.982V13.38C11.6336 13.268 11.6942 13.212 11.8156 13.212H12.5996V5.05H11.8436C11.7222 5.05 11.6616 4.994 11.6616 4.882V4.28C11.6616 4.168 11.7222 4.112 11.8436 4.112H13.4816C13.6029 4.112 13.6636 4.168 13.6636 4.28L13.6496 5.386H13.6636C13.8409 5.00333 14.1256 4.68133 14.5176 4.42C14.9189 4.14933 15.4089 4.014 15.9876 4.014C16.9302 4.014 17.6909 4.336 18.2696 4.98C18.8482 5.61467 19.1376 6.464 19.1376 7.528C19.1376 8.58267 18.8529 9.44133 18.2836 10.104C17.7142 10.7667 16.9536 11.098 16.0016 11.098C15.4229 11.098 14.9376 10.9673 14.5456 10.706C14.1629 10.4353 13.8876 10.1087 13.7196 9.726H13.6776C13.7056 10.0247 13.7196 10.2393 13.7196 10.37V13.212H14.6296C14.7509 13.212 14.8116 13.268 14.8116 13.38V13.982C14.8116 14.094 14.7509 14.15 14.6296 14.15ZM13.6916 7.584C13.6916 8.396 13.9016 9.02133 14.3216 9.46C14.7509 9.89867 15.2642 10.118 15.8616 10.118C16.4682 10.118 16.9722 9.88467 17.3736 9.418C17.7842 8.942 17.9896 8.32133 17.9896 7.556C17.9896 6.78133 17.7842 6.16067 17.3736 5.694C16.9722 5.22733 16.4682 4.994 15.8616 4.994C15.2549 4.994 14.7416 5.22267 14.3216 5.68C13.9016 6.13733 13.6916 6.772 13.6916 7.584ZM26.3811 7.696H21.3411C21.3785 8.47067 21.6211 9.068 22.0691 9.488C22.5265 9.908 23.0398 10.118 23.6091 10.118C24.1785 10.118 24.6638 9.99667 25.0651 9.754C25.4758 9.51133 25.7511 9.27333 25.8911 9.04C25.9658 8.94667 26.0498 8.93267 26.1431 8.998L26.5211 9.376C26.5771 9.432 26.6051 9.48333 26.6051 9.53C26.6051 9.56733 26.5818 9.61867 26.5351 9.684C26.4978 9.74933 26.4138 9.852 26.2831 9.992C26.1525 10.132 25.9658 10.2907 25.7231 10.468C25.4898 10.6453 25.1818 10.7947 24.7991 10.916C24.4258 11.0373 24.0245 11.098 23.5951 11.098C22.6151 11.098 21.7938 10.776 21.1311 10.132C20.4778 9.488 20.1511 8.62467 20.1511 7.542C20.1511 6.45 20.4778 5.59133 21.1311 4.966C21.7845 4.33133 22.5825 4.014 23.5251 4.014C24.4771 4.014 25.2238 4.322 25.7651 4.938C26.3065 5.54467 26.5771 6.366 26.5771 7.402C26.5771 7.43933 26.5771 7.47667 26.5771 7.514C26.5771 7.63533 26.5118 7.696 26.3811 7.696ZM25.4151 6.842C25.3871 6.26333 25.2051 5.80133 24.8691 5.456C24.5331 5.11067 24.0851 4.938 23.5251 4.938C22.9651 4.938 22.4845 5.09667 22.0831 5.414C21.6818 5.73133 21.4438 6.20733 21.3691 6.842H25.4151ZM35.1323 11H33.8023C33.401 11 33.2003 10.804 33.2003 10.412V6.73C33.2003 6.18867 33.0697 5.76867 32.8083 5.47C32.547 5.17133 32.1643 5.022 31.6603 5.022C30.9417 5.022 30.3817 5.316 29.9803 5.904C29.9337 5.96 29.873 6.06267 29.7983 6.212V10.062H30.5403C30.6617 10.062 30.7223 10.118 30.7223 10.23V10.832C30.7223 10.944 30.6617 11 30.5403 11H27.8663C27.745 11 27.6843 10.944 27.6843 10.832V10.23C27.6843 10.118 27.745 10.062 27.8663 10.062H28.6363V5.05H27.8663C27.745 5.05 27.6843 4.994 27.6843 4.882V4.28C27.6843 4.168 27.745 4.112 27.8663 4.112H29.5043C29.6257 4.112 29.6863 4.168 29.6863 4.28L29.6583 5.232H29.7003C30.1857 4.42 30.937 4.014 31.9543 4.014C32.701 4.014 33.2843 4.23333 33.7043 4.672C34.1243 5.11067 34.339 5.70333 34.3483 6.45V10.062H35.1323C35.2537 10.062 35.3143 10.118 35.3143 10.23V10.832C35.3143 10.944 35.2537 11 35.1323 11ZM50.8763 11H47.5023C47.3903 11 47.3343 10.944 47.3343 10.832V10.118C47.3343 10.006 47.3903 9.95 47.5023 9.95H48.5803V6.52H43.3723V9.95H44.4503C44.5717 9.95 44.6323 10.006 44.6323 10.118V10.832C44.6323 10.944 44.5717 11 44.4503 11H41.0903C40.969 11 40.9083 10.944 40.9083 10.832V10.118C40.9083 10.006 40.969 9.95 41.0903 9.95H42.1543V1.998H41.0903C40.969 1.998 40.9083 1.942 40.9083 1.83V1.116C40.9083 1.004 40.969 0.947999 41.0903 0.947999H44.4503C44.5717 0.947999 44.6323 1.004 44.6323 1.116V1.83C44.6323 1.942 44.5717 1.998 44.4503 1.998H43.3723V5.498H48.5803V1.998H47.5023C47.3903 1.998 47.3343 1.942 47.3343 1.83V1.116C47.3343 1.004 47.3903 0.947999 47.5023 0.947999H50.8763C50.9883 0.947999 51.0443 1.004 51.0443 1.116V1.83C51.0443 1.942 50.9883 1.998 50.8763 1.998H49.7983V9.95H50.8763C50.9883 9.95 51.0443 10.006 51.0443 10.118V10.832C51.0443 10.944 50.9883 11 50.8763 11ZM58.0452 7.696H53.0052C53.0425 8.47067 53.2852 9.068 53.7332 9.488C54.1905 9.908 54.7039 10.118 55.2732 10.118C55.8425 10.118 56.3279 9.99667 56.7292 9.754C57.1399 9.51133 57.4152 9.27333 57.5552 9.04C57.6299 8.94667 57.7139 8.93267 57.8072 8.998L58.1852 9.376C58.2412 9.432 58.2692 9.48333 58.2692 9.53C58.2692 9.56733 58.245Line truncated
</svg>

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

+1
View File
@@ -2,6 +2,7 @@
margin: 0;
width: 100%;
-webkit-appearance: none;
background-color: transparent;
}
.range:focus {
+2 -1
View File
@@ -11,7 +11,7 @@ class Range extends React.Component {
handleChange = event => {
this.setState({ value: event.target.value });
if (this.props.onChange) this.props.onChange();
if (this.props.onChange) this.props.onChange(event);
};
render() {
@@ -21,6 +21,7 @@ class Range extends React.Component {
value={this.state.value}
min={this.props.min}
max={this.props.max}
step={this.props.step || 1}
onChange={this.handleChange}
id={this.props.id}
className="range"
-4
View File
@@ -34,10 +34,6 @@
}
.select-ohif:focus {
border-color: #aaa;
box-shadow: 0 0 1px 3px rgba(59, 153, 252, 0.7);
box-shadow: 0 0 0 3px -moz-mac-focusring;
color: #222;
outline: none;
}
+3 -2
View File
@@ -11,8 +11,9 @@ class Select extends Component {
}
handleChange = event => {
this.setState({ value: event.target.value });
if (this.props.onChange) this.props.onChange();
const value = event.target.value;
this.setState({ value });
if (this.props.onChange) this.props.onChange(value);
};
render() {
+5 -7
View File
@@ -1,6 +1,6 @@
import {
Checkbox,
CineDialog,
ExampleDropTarget,
LayoutButton,
LayoutChooser,
MeasurementTable,
@@ -25,10 +25,8 @@ import {
import { ICONS, Icon } from './elements';
// Alias this for now as not all dependents are using strict versioning
import { DropdownMenu as Dropdown } from './elements/form';
import { DropdownMenu as Dropdown, Range, Select } from './elements/form';
import ExpandableToolMenu from './viewer/ExpandableToolMenu.js';
import LayoutManager from './LayoutChooser/LayoutManager.js';
import LayoutPanelDropTarget from './LayoutChooser/LayoutPanelDropTarget.js';
import PlayClipButton from './viewer/PlayClipButton.js';
import { ScrollableArea } from './ScrollableArea/ScrollableArea.js';
import Toolbar from './viewer/Toolbar.js';
@@ -38,23 +36,23 @@ import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
export {
ICONS,
//
Checkbox,
CineDialog,
Dropdown,
ExpandableToolMenu,
ExampleDropTarget,
Icon,
LayoutButton,
LayoutChooser,
LayoutManager,
LayoutPanelDropTarget,
MeasurementTable,
MeasurementTableItem,
Overlay,
OverlayTrigger,
PlayClipButton,
QuickSwitch,
Range,
RoundedButtonGroup,
ScrollableArea,
Select,
SelectTree,
SimpleDialog,
StudyBrowser,
@@ -10,10 +10,11 @@ const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
//
// http://react-dnd.github.io/react-dnd/docs/api/drag-drop-context
export default function viewerbaseDragDropContext(DecoratedClass) {
const backend = isTouchDevice ? TouchBackend({ enableMouseEvents: true }) : HTML5Backend;
const backend = isTouchDevice ? TouchBackend : HTML5Backend;
const opts = isTouchDevice ? { enableMouseEvents: true } : {};
return (props) => (
<DndProvider backend={backend}>
<DndProvider backend={backend} opts={opts}>
<DecoratedClass {...props}/>
</DndProvider>
);
+18 -10
View File
@@ -108,16 +108,8 @@ class ExpandableToolMenu extends React.Component {
};
render() {
return (
<OverlayTrigger
key="menu-button"
trigger="click"
placement="bottom"
rootClose={true}
handleHide={this.onOverlayHide}
onClick={this.onExpandableToolClick}
overlay={this.toolbarMenuOverlay()}
>
const getToolBarButtonComponent = () => {
return (
<ToolbarButton
key="menu-button"
type="tool"
@@ -128,6 +120,22 @@ class ExpandableToolMenu extends React.Component {
isExpandable={true}
isExpanded={this.state.isExpanded}
/>
);
};
const toolbarComponent = getToolBarButtonComponent();
return (
<OverlayTrigger
key="menu-button"
trigger="click"
placement="bottom"
rootClose={true}
handleHide={this.onOverlayHide}
onClick={this.onExpandableToolClick}
overlay={this.toolbarMenuOverlay()}
>
{toolbarComponent}
</OverlayTrigger>
);
}
+8 -21
View File
@@ -1,8 +1,9 @@
// ~~ WebPack
const webpack = require('webpack');
const path = require('path');
const merge = require('webpack-merge');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
//
// ~~ Plugins
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const fontsToJavaScriptRule = require('./rules/fontsToJavaScript.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -12,31 +13,14 @@ const DIST_DIR = path.join(__dirname, '../dist');
const PUBLIC_DIR = path.join(__dirname, '../public');
// ~~ Env Vars
const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'script-tag.html';
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
const mergedConfig = merge(commonConfig, {
entry: {
app: `${SRC_DIR}/index-umd.js`,
},
devtool: 'source-map',
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: DIST_DIR,
library: 'OHIFViewer',
@@ -50,14 +34,17 @@ module.exports = (env, argv) => {
// Clean output.path
new CleanWebpackPlugin(),
// Generate "index.html" w/ correct includes/imports
// NOTE: We use this for E2E Tests
new HtmlWebpackPlugin({
inject: false,
template: `${PUBLIC_DIR}/html-templates/${HTML_TEMPLATE}`,
filename: 'index.html',
}),
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
maxChunks: 1,
}),
],
});
return mergedConfig;
};
+10 -27
View File
@@ -32,27 +32,11 @@ module.exports = (env, argv) => {
const hasProxy = PROXY_TARGET && PROXY_DOMAIN;
const mergedConfig = merge(baseConfig, {
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
output: {
path: DIST_DIR,
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix
},
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: isProdBuild,
sideEffects: true,
},
module: {
rules: [...extractStyleChunksRule(isProdBuild)],
},
@@ -71,6 +55,12 @@ module.exports = (env, argv) => {
// Ignore our configuration files
ignore: ['config/*', 'html-templates/*', '.DS_Store'],
},
// Short term solution to make sure GCloud config is available in output
// for our docker implementation
{
from: `${PUBLIC_DIR}/config/google.js`,
to: `${DIST_DIR}/google.js`,
},
// Copy over and rename our target app config file
{
from: `${PUBLIC_DIR}/${APP_CONFIG}`,
@@ -79,8 +69,8 @@ module.exports = (env, argv) => {
]),
// https://github.com/faceyspacey/extract-css-chunks-webpack-plugin#webpack-4-standalone-installation
new ExtractCssChunksPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
filename: isProdBuild ? '[name].[hash].css' : '[name].css',
chunkFilename: isProdBuild ? '[id].[hash].css' : '[id].css',
ignoreOrder: false, // Enable to remove warnings about conflicting order
}),
// Generate "index.html" w/ correct includes/imports
@@ -108,6 +98,8 @@ module.exports = (env, argv) => {
hot: true,
open: true,
port: 3000,
host: '0.0.0.0',
public: 'http://localhost:' + 3000,
historyApiFallback: {
disableDotRule: true,
},
@@ -121,15 +113,6 @@ module.exports = (env, argv) => {
if (!isProdBuild) {
mergedConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
//
mergedConfig.optimization.minimizer = [
new TerserJSPlugin({
sourceMap: true,
parallel: true,
}),
new OptimizeCSSAssetsPlugin({}),
];
}
return mergedConfig;
+282
View File
@@ -3,6 +3,288 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.2...@ohif/viewer@1.6.3) (2019-10-14)
**Note:** Version bump only for package @ohif/viewer
## [1.6.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.1...@ohif/viewer@1.6.2) (2019-10-11)
**Note:** Version bump only for package @ohif/viewer
## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.0...@ohif/viewer@1.6.1) (2019-10-11)
### Bug Fixes
* Switch token storage back to localStorage because in-memory was annoying for end users ([#1030](https://github.com/OHIF/Viewers/issues/1030)) ([412fe4e](https://github.com/OHIF/Viewers/commit/412fe4e))
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.4...@ohif/viewer@1.6.0) (2019-10-11)
### 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.5.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.3...@ohif/viewer@1.5.4) (2019-10-10)
**Note:** Version bump only for package @ohif/viewer
## [1.5.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.2...@ohif/viewer@1.5.3) (2019-10-10)
**Note:** Version bump only for package @ohif/viewer
## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.1...@ohif/viewer@1.5.2) (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))
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.0...@ohif/viewer@1.5.1) (2019-10-09)
### Bug Fixes
* 🐛 set current viewport as active when switching layouts ([#1018](https://github.com/OHIF/Viewers/issues/1018)) ([2a74355](https://github.com/OHIF/Viewers/commit/2a74355))
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.5...@ohif/viewer@1.5.0) (2019-10-09)
### Features
* Multiple fixes and implementation changes to react-cornerstone-viewport ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3))
## [1.4.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.4...@ohif/viewer@1.4.5) (2019-10-09)
**Note:** Version bump only for package @ohif/viewer
## [1.4.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.3...@ohif/viewer@1.4.4) (2019-10-07)
**Note:** Version bump only for package @ohif/viewer
## [1.4.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.2...@ohif/viewer@1.4.3) (2019-10-04)
**Note:** Version bump only for package @ohif/viewer
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.1...@ohif/viewer@1.4.2) (2019-10-04)
**Note:** Version bump only for package @ohif/viewer
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.0...@ohif/viewer@1.4.1) (2019-10-03)
**Note:** Version bump only for package @ohif/viewer
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.3...@ohif/viewer@1.4.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.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.2...@ohif/viewer@1.3.3) (2019-10-02)
**Note:** Version bump only for package @ohif/viewer
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.1...@ohif/viewer@1.3.2) (2019-10-01)
**Note:** Version bump only for package @ohif/viewer
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.0...@ohif/viewer@1.3.1) (2019-10-01)
### Bug Fixes
* Exit MPR mode if Layout is changed ([#984](https://github.com/OHIF/Viewers/issues/984)) ([674ca9f](https://github.com/OHIF/Viewers/commit/674ca9f))
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.8...@ohif/viewer@1.3.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))
## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.7...@ohif/viewer@1.2.8) (2019-10-01)
**Note:** Version bump only for package @ohif/viewer
## [1.2.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.6...@ohif/viewer@1.2.7) (2019-10-01)
**Note:** Version bump only for package @ohif/viewer
## [1.2.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.5...@ohif/viewer@1.2.6) (2019-09-27)
**Note:** Version bump only for package @ohif/viewer
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.4...@ohif/viewer@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/viewer@1.2.3...@ohif/viewer@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/viewer@1.2.3...@ohif/viewer@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.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.2...@ohif/viewer@1.2.3) (2019-09-27)
**Note:** Version bump only for package @ohif/viewer
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.1...@ohif/viewer@1.2.2) (2019-09-27)
**Note:** Version bump only for package @ohif/viewer
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.0...@ohif/viewer@1.2.1) (2019-09-26)
### Bug Fixes
* google cloud support w/ docker (via env var) ([#958](https://github.com/OHIF/Viewers/issues/958)) ([e375a4a](https://github.com/OHIF/Viewers/commit/e375a4a))
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.14...@ohif/viewer@1.2.0) (2019-09-26)
### Features
* 🎸 React custom component on toolbar button ([#935](https://github.com/OHIF/Viewers/issues/935)) ([a90605c](https://github.com/OHIF/Viewers/commit/a90605c))
## [1.1.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.13...@ohif/viewer@1.1.14) (2019-09-26)
@@ -0,0 +1,457 @@
describe('OHIF Cornerstone Toolbar', () => {
before(() => {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
});
beforeEach(() => {
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
//Following best practices, reset should be done before each test
cy.resetViewport();
});
it('checks if all primary buttons are being displayed', () => {
cy.get('@stackScrollBtn')
.should('be.visible')
.contains('Stack Scroll');
cy.get('@zoomBtn')
.should('be.visible')
.contains('Zoom');
cy.get('@levelsBtn')
.should('be.visible')
.contains('Levels');
cy.get('@panBtn')
.should('be.visible')
.contains('Pan');
cy.get('@lengthBtn')
.should('be.visible')
.contains('Length');
cy.get('@annotateBtn')
.should('be.visible')
.contains('Annotate');
cy.get('@angleBtn')
.should('be.visible')
.contains('Angle');
cy.get('@resetBtn')
.should('be.visible')
.contains('Reset');
cy.get('@cineBtn')
.should('be.visible')
.contains('CINE');
cy.get('@moreBtn')
.should('be.visible')
.contains('More');
cy.get('@twodmprBtn')
.should('be.visible')
.contains('2D MPR');
cy.get('@layoutBtn')
.should('be.visible')
.contains('Layout');
});
it('checks if Stack Scroll tool will navigate across all series in the viewport', () => {
//Click on button and vefiry if icon is active on toolbar
cy.get('@stackScrollBtn')
.click()
.then($stackScrollBtn => {
cy.wrap($stackScrollBtn).should('have.class', 'active');
});
//drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport')
.trigger('mousedown', 'top', { which: 1 })
.trigger('mousemove', 'center', { which: 1 })
.trigger('mouseup');
const expectedText =
'Ser: 1Img: 14 14/26256 x 256Loc: 0.00 mm Thick: 5.00 mm';
cy.get('@viewportInfoBottomLeft').should('have.text', expectedText);
});
it('checks if Zoom tool will zoom in/out an image in the viewport', () => {
//Click on button and vefiry if icon is active on toolbar
cy.get('@zoomBtn')
.click()
.then($zoomBtn => {
cy.wrap($zoomBtn).should('have.class', 'active');
});
//drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport')
.trigger('mousedown', 'top', { which: 1 })
.trigger('mousemove', 'center', { which: 1 })
.trigger('mouseup');
const expectedText = 'Zoom: 884%W: 820 L: 410Lossless / Uncompressed';
cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
});
it('checks if Levels tool will change the contrast and brightness of an image in the viewport', () => {
//Click on button and vefiry if icon is active on toolbar
cy.get('@levelsBtn')
.click()
.then($levelsBtn => {
cy.wrap($levelsBtn).should('have.class', 'active');
});
//drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport')
.trigger('mousedown', 'top', { which: 1 })
.trigger('mousemove', 'center', { which: 1 })
.trigger('mouseup')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'left', { which: 1 })
.trigger('mouseup');
const expectedText = 'Zoom: 211%W: 544 L: 626Lossless / Uncompressed';
cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
});
it('checks if Pan tool will move the image inside the viewport', () => {
//Click on button and vefiry if icon is active on toolbar
cy.get('@panBtn')
.click()
.then($panBtn => {
cy.wrap($panBtn).should('have.class', 'active');
});
//Get image position from cornerstone and check if y axis was modified
let cornerstone;
let currentPan;
cy.window()
.its('cornerstone')
.then(c => {
cornerstone = c;
currentPan = () =>
cornerstone.getEnabledElements()[0].viewport.translation;
});
cy.get('@viewport')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'bottom', { which: 1 })
.trigger('mouseup', 'bottom')
.then(() => {
expect(currentPan().y > 0).to.eq(true);
});
});
it('checks if Length annotation can be added on viewport and on measurements panel', () => {
//Click on button and vefiry if icon is active on toolbar
cy.get('@lengthBtn')
.click()
.then($lengthbtn => {
cy.wrap($lengthbtn).should('have.class', 'active');
});
//Add annotation on the viewport
const firstClick = [150, 100];
const secondClick = [130, 170];
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn')
.click()
.then($measurementsBtn => {
cy.get('@measurementsPanel').should('be.visible');
cy.get('.measurementItem')
.its('length')
.should('be.eq', 1);
cy.wrap($measurementsBtn).click();
});
});
it('checks if Angle annotation can be added on viewport and on measurements panel', () => {
//Click on button and vefiry if icon is active on toolbar
cy.get('@angleBtn')
.click()
.then($angleBtn => {
cy.wrap($angleBtn).should('have.class', 'active');
});
//Add annotation on the viewport
const initPos = [180, 390];
const midPos = [300, 410];
const finalPos = [180, 450];
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn')
.click()
.then($measurementsBtn => {
cy.get('@measurementsPanel').should('be.visible');
cy.get('.measurementItem')
.its('length')
.should('be.eq', 1);
cy.wrap($measurementsBtn).click();
});
});
it('checks if Reset tool will reset all changes made on the image', () => {
//Make some changes by zooming in and rotating the image
cy.imageZoomIn();
cy.imageContrast();
//Click on reset button
cy.get('@resetBtn').click();
const expectedText = 'Zoom: 211%W: 820 L: 410Lossless / Uncompressed';
cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
});
it('checks if CINE tool will prompt a modal with working controls', () => {
//Click on button
cy.get('@cineBtn').click();
//Vefiry if cine control overlay is being displayed
cy.get('.cine-controls')
.as('cineControls')
.should('be.visible');
//Test PLAY button
cy.get('[title="Play / Stop"]')
.click()
.wait(100)
.click();
let expectedText = 'Img: 1 1/26';
cy.get('@viewportInfoBottomLeft').should('not.have.text', expectedText);
//Test SKIP TO FIRST IMAGE button
cy.get('[title="Skip to first Image"]').click();
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
//Test NEXT IMAGE button
cy.get('[title="Next Image"]').click();
expectedText = 'Img: 2 2/26';
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
//Test SKIP TO LAST IMAGE button
cy.get('[title="Skip to last Image"]').click();
expectedText = 'Img: 27 26/26';
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
//Test PREVIOUS IMAGE button
cy.get('[title="Previous Image"]').click();
expectedText = 'Img: 26 25/26';
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
//Click on Cine button
cy.get('@cineBtn').click();
//Vefiry if cine control overlay is hidden
cy.get('@cineControls').should('not.be.visible');
});
it('checks if More button will prompt a modal with secondary tools', () => {
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay')
.as('toolbarOverlay')
.should('be.visible');
let iconName;
//Click on one of the secondary tools from the overlay
cy.get('.tooltip-inner > :nth-child(1)')
.click()
.then($magnifyBtn => {
cy.wrap($magnifyBtn)
.should('have.class', 'active')
.find('svg')
.then($icon => {
iconName = $icon.text();
});
});
//Check if More button is active and if it has same icon as the secondary tool selected
cy.get('@moreBtn')
.click()
.then($moreBtn => {
cy.wrap($moreBtn)
.should('have.class', 'active')
.contains(iconName);
});
//Verify if overlay is hidden
cy.get('@toolbarOverlay').should('not.be.visible');
});
it('checks if Layout tool will multiply the number of viewports displayed', () => {
//Click on Layout button and verify if overlay is displayed
cy.get('@layoutBtn')
.click()
.then(() => {
cy.get('.layoutChooser')
.as('layoutChooser')
.should('be.visible')
.find('td')
.its('length')
.should('be.eq', 9);
});
//verify if layout has changed to 2 viewports
cy.get('tbody > :nth-child(1) > :nth-child(2)').click();
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 2);
});
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(2) > :nth-child(1)').click();
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 2);
});
//verify if layout has changed to 3 viewports
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
cy.get('.viewport-container').then($viewport => {
cy.wait(1000);
cy.wrap($viewport)
.its('length')
.should('be.eq', 3);
});
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(3) > :nth-child(1)').click();
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 3);
});
//verify if layout has changed to 4 viewports
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(2) > :nth-child(2)').click();
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 4);
});
//verify if layout has changed to 6 viewports
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(2) > :nth-child(3)').click();
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 6);
});
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(3) > :nth-child(2)').click();
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 6);
});
//verify if layout has changed to 9 viewports
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(3) > :nth-child(3)').click();
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 9);
});
//Commented this to avoid throwing an wrong exception at the end of "Layout button" test
//this commented section should be uncommented once issue #999 is fixed. (https://github.com/OHIF/Viewers/issues/999)
//verify if layout has changed to 1 viewport
// cy.get('@layoutBtn').click();
// cy.get('tbody > :nth-child(1) > :nth-child(1)').click();
// cy.get('.viewport-container').then(($viewport) =>{
// cy.wrap($viewport)
// .its('length')
// .should('be.eq', 1);
// })
cy.reload();
cy.waitDicomImage();
});
it('checks if Clear tool will delete all measurements added in the viewport', () => {
//Add measurements in the viewport
cy.get('@lengthBtn').click();
const firstClick = [150, 100];
const secondClick = [130, 170];
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
cy.get('@angleBtn').click();
const initPos = [180, 390];
const midPos = [300, 410];
const finalPos = [180, 450];
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.eq', 2);
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay')
.as('toolbarOverlay')
.should('be.visible');
//Click on Clear button
cy.get('.tooltip-inner > :nth-child(10)').click();
//Verify if measurements were removed from the measurements panel
cy.get('.measurementItem').should('not.exist');
//Close More button overlay
cy.get('@moreBtn').click();
//Close the measurements panel
cy.get('@measurementsBtn').then($btn => {
$btn.click();
cy.get('@measurementsPanel').should('not.be.enabled');
});
});
it('checks if Eraser tool will remove the measurements added in the viewport', () => {
//Add measurements in the viewport
cy.get('@lengthBtn').click();
const firstClick = [150, 100];
const secondClick = [130, 170];
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
cy.get('@angleBtn').click();
const initPos = [180, 390];
const midPos = [300, 410];
const finalPos = [180, 450];
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.eq', 2);
cy.get('@measurementsBtn').click();
//Click More button
cy.get('@moreBtn').click();
//Click Eraser button
cy.get('.tooltip-inner > :nth-child(12)').click();
//Erase measurement #1 and Verify if it was removed from the measurements panel
const [x1, y1] = firstClick;
cy.get('@viewport').click(x1, y1, { force: true });
cy.get('.measurementItem')
.its('length')
.should('be.eq', 1);
//Erase measurement #2 and Verify if it was removed from the measurements panel
const [x2, y2] = initPos;
cy.get('@viewport').click(x2, y2, { force: true });
cy.get('.measurementItem').should('not.exist');
});
});
@@ -1,50 +1,93 @@
/*
TODO: Temporarily commented out because it is failing on CI due to timing issues
describe('OHIF Study Viewer Page', () => {
before(() => {
cy.openStudy("MISTER^MR");
});
it('checks if series thumbnails are being displayed', ()=> {
cy.get('.ThumbnailEntryContainer')
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
});
beforeEach(() => {
cy.initCommonElementsAliases();
//Following best practices, reset should be done before each test
cy.resetViewport();
});
it('checks if series thumbnails are being displayed', () => {
cy.get('.ThumbnailEntryContainer')
.its('length')
.should('be.gt', 1);
});
it('drags and drop a series thumbnail into viewport', () => {
cy.get('.ThumbnailEntryContainer:nth-child(2)') //element to be dragged
.drag('.cornerstone-canvas'); //dropzone element
const overlaySeriesInformation = 'div.ViewportOverlay > div.bottom-left.overlay-element > div';
const expectedText = 'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm';
cy.get(overlaySeriesInformation)
.should('have.text', expectedText);
});
it('checks if Series left panel can be hidden/displayed', ()=> {
const seriesButton = '.pull-left > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton';
const leftPanel = 'section.sidepanel.from-left';
cy.get(seriesButton).click();
cy.get(leftPanel).should('not.be.enabled')
cy.get(seriesButton).click();
cy.get(leftPanel).should('be.visible');
});
it('checks if Measurements right panel can be hidden/displayed', ()=> {
const measurementsButton = '.pull-right > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton';
const rightPanel = 'section.sidepanel.from-right';
cy.get(measurementsButton).click();
cy.get(rightPanel).should('be.visible');
cy.get(measurementsButton).click();
cy.get(rightPanel).should('not.be.enabled');
});
});
*/
it('drags and drop a series thumbnail into viewport', () => {
cy.get('.ThumbnailEntryContainer:nth-child(2)') //element to be dragged
.drag('.cornerstone-canvas'); //dropzone element
const expectedText =
'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm';
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
});
it('checks if Series left panel can be hidden/displayed', () => {
cy.get('@seriesBtn').click();
cy.get('@seriesPanel').should('not.be.enabled');
cy.get('@seriesBtn').click();
cy.get('@seriesPanel').should('be.visible');
});
it('checks if Measurements right panel can be hidden/displayed', () => {
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('be.visible');
cy.get('@measurementsBtn').click();
cy.get('@measurementsPanel').should('not.be.enabled');
});
it('checks if measurement item can be Relabeled under Measurements panel', () => {
cy.addLengthMeasurement(); //Adding measurement in the viewport
cy.get('@measurementsBtn').click();
cy.get('.measurementItem').click();
// Click "Relabel"
cy.get('.btnAction')
.contains('Relabel')
.click();
// Search for "Bone"
cy.get('.searchInput').type('Bone');
// Select "Bone" Result
cy.get('.treeInputs > .wrapperLabel')
.contains('Bone')
.click();
// Confirm Selection
cy.get('.checkIconWrapper').click();
//Verify if 'Bone' label was added
cy.get('.measurementLocation').should('contain.text', 'Bone');
});
//TO-DO: Test case will fail due to issue #1013: https://github.com/OHIF/Viewers/issues/1013
// it('checks if Description can be added to measurement item under Measurements panel', () => {
// cy.addLengthMeasurement(); //Adding measurement in the viewport
// cy.get('@measurementsBtn').click();
// cy.get('.measurementItem').click();
//
// // Click "Description"
// cy.get('.btnAction')
// .contains('Description')
// .click();
//
// // Enter description text
// const descriptionText = 'Adding text for description test';
// cy.get('#description')
// .type(descriptionText);
//
// // Confirm
// cy.get('.btn-confirm').click();
//
// //Verify if descriptionText was added
// cy.get('.measurementLocation')
// .should('contain.text', descriptionText);
// });
});
@@ -0,0 +1,34 @@
//Creating aliases for Cornerstone tools buttons
export function initCornerstoneToolsAliases() {
cy.get('.ToolbarRow > :nth-child(2)').as('stackScrollBtn');
cy.get('.ToolbarRow > :nth-child(3)').as('zoomBtn');
cy.get('.ToolbarRow > :nth-child(4)').as('levelsBtn');
cy.get('.ToolbarRow > :nth-child(5)').as('panBtn');
cy.get('.ToolbarRow > :nth-child(6)').as('lengthBtn');
cy.get('.ToolbarRow > :nth-child(7)').as('annotateBtn');
cy.get('.ToolbarRow > :nth-child(8)').as('angleBtn');
cy.get('.ToolbarRow > :nth-child(9)').as('resetBtn');
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
cy.get('.expandableToolMenu').as('moreBtn');
cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
}
//Creating aliases for Common page elements
export function initCommonElementsAliases() {
cy.get(
'.pull-right > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton'
).as('measurementsBtn');
cy.get('.viewport-element').as('viewport');
cy.get('section.sidepanel.from-right').as('measurementsPanel');
cy.get(
'.pull-left > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton'
).as('seriesBtn');
cy.get('section.sidepanel.from-left').as('seriesPanel');
cy.get('div.ViewportOverlay > div.bottom-left.overlay-element > div').as(
'viewportInfoBottomLeft'
);
cy.get('div.ViewportOverlay > div.bottom-right.overlay-element > div').as(
'viewportInfoBottomRight'
);
}
+172 -11
View File
@@ -1,4 +1,8 @@
import { DragSimulator } from "../helpers/DragSimulator.js";
import { DragSimulator } from '../helpers/DragSimulator.js';
import {
initCornerstoneToolsAliases,
initCommonElementsAliases,
} from './aliases.js';
// ***********************************************
// This example commands.js shows you how to
@@ -26,16 +30,173 @@ import { DragSimulator } from "../helpers/DragSimulator.js";
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('openStudy', (patientName) => {
cy.visit('/');
cy.get('#patientName')
.type(patientName);
cy.get('.studylistStudy > .patientName')
.contains(patientName)
.click();
/**
* Command to search for a patient name and open his/her study.
*
* @param {string} PatientName - Patient name that we would like to search for
*/
Cypress.Commands.add('openStudy', patientName => {
cy.visit('/');
cy.get('#patientName').type(patientName);
cy.get('.studylistStudy > .patientName')
.as('patientResult')
.then({ timeout: 5000 }, $patientResult => {
cy.contains(patientName).click();
});
});
/**
* Command to perform a drag and drop action. Before using this command, we must get the element that should be dragged first.
* Example of usage: cy.get(element-to-be-dragged).drag(dropzone-element)
*
* @param {*} element - Selector for element that we want to use as dropzone
*/
Cypress.Commands.add('drag', { prevSubject: 'element' }, (...args) =>
DragSimulator.simulate(...args)
);
/**
* Command to perform two clicks into two different positions. Each position must be [x, y].
* The positions are considering the element as reference, therefore, top-left of the element will be (0, 0).
*
* @param {*} viewport - Selector for viewport we would like to interact with
* @param {number[]} firstClick - Click position [x, y]
* @param {number[]} secondClick - Click position [x, y]
*/
Cypress.Commands.add('addLine', (viewport, firstClick, secondClick) => {
cy.get(viewport).then($viewport => {
const [x1, y1] = firstClick;
const [x2, y2] = secondClick;
cy.wrap($viewport)
.click(x1, y1, { force: true })
.trigger('mousemove', { clientX: x2, clientY: y2 })
.click(x2, y2, { force: true });
});
});
/**
* Command to perform three clicks into three different positions. Each position must be [x, y].
* The positions are considering the element as reference, therefore, top-left of the element will be (0, 0).
*
* @param {*} viewport - Selector for viewport we would like to interact with
* @param {number[]} firstClick - Click position [x, y]
* @param {number[]} secondClick - Click position [x, y]
* @param {number[]} thirdClick - Click position [x, y]
*/
Cypress.Commands.add(
'addAngle',
(viewport, firstClick, secondClick, thirdClick) => {
cy.get(viewport).then($viewport => {
const [x1, y1] = firstClick;
const [x2, y2] = secondClick;
const [x3, y3] = thirdClick;
cy.wrap($viewport)
.click(x1, y1, { force: true })
.trigger('mousemove', { clientX: x2, clientY: y2 })
.click(x2, y2, { force: true })
.trigger('mousemove', { clientX: x3, clientY: y3 })
.click(x3, y3, { force: true });
});
}
);
Cypress.Commands.add('drag', {prevSubject: 'element',},
(...args) => DragSimulator.simulate(...args)
);
//Command to wait DICOM image to load into the viewport
Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
cy.window()
.its('cornerstone')
.then({ timeout }, $cornerstone => {
return new Cypress.Promise(resolve => {
const onEvent = renderedEvt => {
const element = renderedEvt.detail.element;
element.removeEventListener('cornerstoneimagerendered', onEvent);
$cornerstone.events.removeEventListener(
'cornerstoneimagerendered',
onEvent
);
resolve();
};
const onEnabled = enabledEvt => {
const element = enabledEvt.detail.element;
element.addEventListener('cornerstoneimagerendered', onEvent);
};
$cornerstone.events.addEventListener(
'cornerstoneelementenabled',
onEnabled
);
});
});
});
//Command to reset and clear all the changes made to the viewport
Cypress.Commands.add('resetViewport', () => {
cy.initCornerstoneToolsAliases();
cy.get('@resetBtn').click();
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('body').then(body => {
if (body.find('.tooltip-toolbar-overlay').length == 0) {
cy.get('@moreBtn').click();
}
});
//Click on Clear button
cy.get('.tooltip-inner > :nth-child(10)')
.as('clearBtn')
.click();
});
Cypress.Commands.add('imageZoomIn', () => {
cy.initCornerstoneToolsAliases();
cy.get('@zoomBtn').click();
//drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport')
.trigger('mousedown', 'top', { which: 1 })
.trigger('mousemove', 'center', { which: 1 })
.trigger('mouseup');
});
Cypress.Commands.add('imageContrast', () => {
cy.initCornerstoneToolsAliases();
cy.get('@levelsBtn').click();
//drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup');
});
//Initialize aliases for Cornerstone tools buttons
Cypress.Commands.add('initCornerstoneToolsAliases', () => {
initCornerstoneToolsAliases();
});
//Initialize aliases for Common page elements
Cypress.Commands.add('initCommonElementsAliases', () => {
initCommonElementsAliases();
});
//Add measurements in the viewport
Cypress.Commands.add('addLengthMeasurement', () => {
cy.initCornerstoneToolsAliases();
cy.get('@lengthBtn').click();
const firstClick = [150, 100];
const secondClick = [130, 170];
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
});
//Add measurements in the viewport
Cypress.Commands.add('addAngleMeasurement', () => {
cy.initCornerstoneToolsAliases();
cy.get('@angleBtn').click();
const initPos = [180, 390];
const midPos = [300, 410];
const finalPos = [180, 450];
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
});
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more