Compare commits

...
Author SHA1 Message Date
ohif-bot 77a421cdc0 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.5.2
 - @ohif/core@2.9.2
 - @ohif/viewer@3.11.4
2020-05-04 08:15:19 +00:00
Sanders DeNardiandSanders DeNardi 7033886697 fix: use bit-appropriate array for palette lookup tables (#1698)
Co-authored-by: Sanders DeNardi <sdenardi@summusglobal.com>
2020-05-04 10:13:04 +02:00
ohif-bot 256563d7ee chore(release): publish [skip ci]
- @ohif/viewer@3.11.3
2020-04-29 12:56:38 +00:00
Erik Ziegler f7162ce617 fix: Add IHEInvokeImageDisplay routes back into viewer (#1695) 2020-04-29 08:54:40 -04:00
ohif-bot 027ef4f93c chore(release): publish [skip ci]
- @ohif/extension-dicom-html@1.2.1
 - @ohif/extension-dicom-rt@0.1.1
 - @ohif/extension-dicom-segmentation@0.2.2
 - @ohif/extension-vtk@1.5.1
 - @ohif/core@2.9.1
 - @ohif/viewer@3.11.2
2020-04-28 10:52:19 +00:00
Khalid Williams f09ebbe2e8 chore: cleanup dcmjs imports
* Update imports in /platform

* Update dcmjs imports in /extentions
2020-04-28 06:49:57 -04:00
David Connaughtonandeolasd 51b4e60161 docs: updated deployment section with info for using JSON (#1676)
* update deploy section with non-dicomweb info

* fix typo and add info re. case sensitive json

* Add CORS info and reformat inline links

Co-authored-by: eolasd <pacsradwp@gmail.com>
2020-04-27 16:11:35 -04:00
ohif-bot 08de987711 chore(release): publish [skip ci]
- @ohif/extension-dicom-segmentation@0.2.1
 - @ohif/viewer@3.11.1
2020-04-27 08:41:40 +00:00
Igor Octaviano 20e366ffbd fix: 🐛 Dropdown not updating when selecting segment (#1675)
Observe selectedSegmentation changes to refresh segment list

✅ Closes: #1669
2020-04-27 10:39:11 +02:00
ohif-bot 561911056a chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.7.0
 - @ohif/extension-dicom-html@1.2.0
 - @ohif/extension-dicom-rt@0.1.0
 - @ohif/extension-dicom-segmentation@0.2.0
 - @ohif/extension-vtk@1.5.0
 - @ohif/core@2.9.0
 - @ohif/viewer@3.11.0
2020-04-24 10:29:19 +00:00
Igor OctavianoandJames Petts 835f64d47a feat: 🎸 Seg jump to slice + show/hide
* Add segment part 2 (jump to frame and visibility toggle)

* Cr updates

* Filter displaysets with images

* feat: 🎸 Seg jump to slice + show/hide

Co-authored-by: James Petts <jamesapetts@gmail.com>
2020-04-24 12:25:05 +02:00
ohif-bot fc40d84092 chore(release): publish [skip ci]
- @ohif/viewer@3.10.2
2020-04-23 16:20:51 +00:00
Danny Brown 709f14708e fix: undefined errorHandler in cornerstoneWadoImageLoader configuration (#1664) 2020-04-23 12:17:49 -04:00
ohif-bot 5f4c4ab5e7 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.6.1
 - @ohif/extension-vtk@1.4.1
 - @ohif/core@2.8.1
 - @ohif/viewer@3.10.1
2020-04-23 10:04:47 +00:00
James Petts 71205618ec fix: 🐛 Multiframe fix (#1661) 2020-04-23 12:02:51 +02:00
ohif-bot 5014ba864b chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.6.0
 - @ohif/extension-dicom-p10-downloader@0.1.0
 - @ohif/extension-vtk@1.4.0
 - @ohif/core@2.8.0
 - @ohif/viewer@3.10.0
2020-04-23 02:06:44 +00:00
James Pettsanddannyrb e96205de35 feat: configuration to hook into XHR Error handling
* Error handling.

* Fix error handling, add redirect example.

* remove debugger

* docs: whitelabeling and httpErrorHandler

* bump dicomweb-client version

* bump cornerstone-wado-image-loader version

* Add more documentation around httpErrorHandler callback

* test: add errorHandler as an expected top level export

Co-authored-by: dannyrb <danny.ri.brown@gmail.com>
2020-04-22 22:01:34 -04:00
ohif-bot 273f31ad56 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.14
 - @ohif/core@2.7.1
 - @ohif/viewer@3.9.2
2020-04-22 20:19:12 +00:00
Danny Brown 2093a00365 fix: whiteLabeling should support component creation by passing React to defined fn (#1659) 2020-04-22 16:16:53 -04:00
dependabot[bot] b1dad6b61b chore(deps): bump https-proxy-agent from 2.2.1 to 2.2.4 in /docs/latest (#1641)
Bumps [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) from 2.2.1 to 2.2.4.
- [Release notes](https://github.com/TooTallNate/node-https-proxy-agent/releases)
- [Commits](https://github.com/TooTallNate/node-https-proxy-agent/compare/2.2.1...2.2.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-04-18 22:05:44 -04:00
ohif-bot 3a587f4e4b chore(release): publish [skip ci]
- @ohif/viewer@3.9.1
2020-04-17 16:28:57 +00:00
Danny Brown d9fc7bbb0e fix: showStudyList config (#1647)
* fix: `showStudyList` config

* showStudyList should default to true if not defined
2020-04-17 12:26:09 -04:00
ohif-bot b0cfc45f83 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.13
 - @ohif/core@2.7.0
 - @ohif/viewer@3.9.0
2020-04-17 14:16:04 +00:00
Danny Brown 450c80b9d5 feat: set the authorization header for DICOMWeb requests if provided in query string (#1646) 2020-04-17 10:14:13 -04:00
ohif-bot d3eefb50f5 chore(release): publish [skip ci]
- @ohif/extension-dicom-p10-downloader@0.0.2
 - @ohif/extension-vtk@1.3.12
 - @ohif/core@2.6.11
 - @ohif/viewer@3.8.21
2020-04-15 17:00:38 +00:00
Emanuel Fiuza de OliveiraandJames Petts 29d4955980 Feat #1130 [WIP] Adding support for downloading and zipping files (#1501)
* Feat #1130 Adding support for downloading and zipping files

@ This is a combination of 12 commits.
 - Feat #1130 Initial Commit
 - Feat #1130 Adding support for hierachical lists
 - Feat #1130 [WIP] Adding support for downloading and zipping files
 - Feat #1130 Adding support for hierarchical folder structure study, series, instances on generated zip file
 - Feat #1130 Adding unit tests and addressing code review feedbacks
 - Feat #1130 POC (adding module to support on progress handling)
 - Feat #1130 Improving logic from progress tracking utils
 - Feat #1130 Renaming module with progress tracking utils
 - Feat #1130 Beta version of Progress Tracking utils
 - Feat #1130 Adding unit tests for Progress Tracking Utils
 - Feat #1130 Documenting public methods from progress tracking utils module
 - Feat #1130 Adding support for download and zip progress reporting

* Feat #1130 Addressing code review feedback

* Feat #1130 Preventing multiple downloads from executing at once

* Feat #1130 Improving error message for concurrent downloads

* Remove downloadAndZipSeriesOnViewports

Co-authored-by: James Petts <jamesapetts@gmail.com>
2020-04-15 18:57:57 +02:00
Nisal Upendra 2412c08595 docs: Embedding in existing react App (#1626)
Added how to embed OHIF viewer in an existing React application
2020-04-10 08:28:37 -04:00
ohif-bot d769e5c296 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.11
 - @ohif/core@2.6.10
 - @ohif/viewer@3.8.20
2020-04-09 11:08:07 +00:00
Danny Brown d21af3f133 fix: Revert "refactor: Reduce bundle size (#1575)" (#1622)
This reverts commit 81dcbe294d.
2020-04-09 07:06:41 -04:00
ohif-bot 70672ed663 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.5.2
 - @ohif/extension-dicom-rt@0.0.2
 - @ohif/extension-dicom-segmentation@0.1.2
 - @ohif/extension-vtk@1.3.10
 - @ohif/core@2.6.9
 - @ohif/ui@1.4.3
 - @ohif/viewer@3.8.19
2020-04-09 10:25:11 +00:00
858484f250 Support of RTSTRUCT visualization (#1524)
* Filter segs and fetch correct metadata.

* Initiate seg loading logic on series load into viewport. Need to write.

* Automatic consumption of SEG functional.

* Pull in master.

* Fix issues after pulling in master.

* Clean up old segmentation handlers.

* WIP

* WIP

* Poppulate viewport with metadata.

* Show hide/segmentation in react-vtkjs-viewport.

* feat: 🎸 View segmentations in vtkjs viewport

* Filter segs and fetch correct metadata.

* Initiate seg loading logic on series load into viewport. Need to write.

* Automatic consumption of SEG functional.

* Pull in master.

* Fix issues after pulling in master.

* Clean up old segmentation handlers.

* WIP

* WIP

* Poppulate viewport with metadata.

* Show hide/segmentation in react-vtkjs-viewport.

* feat: 🎸 View segmentations in vtkjs viewport

* Rebase on master.

* format format format

* add basic extension parts

* Updated yarn lock

* Fetch derived/referenced displaysets in sidepanel

* Add buttons, expose seriesDescription, write notes for Danny.

* Add growcut tool extention. -> Move to a seperate repo.

* Change some text fields.

* expose dialog provider

* hide draggable area

* No pointer events for now; need to revisit

* Don't filter seg from the outset for now

* fiddling about

* fix path to series data

* fix logic to determine next available labelmapindex

* Ability to swtich between labelmaps

* Return a promise so we can make sure our labelmap is loaded before pulling metadata

* Better UI to toggle labelmaps

* Fix accidental untoggle of active labelmap for ui

* Remove brush tool from cornerstone extension

* Add cstools in preInit for seg extension

* Add buttons for seg magic

* Hide the measurements panel for now

* Slightly better seg/scissor icons

* Add eraser

* ability to switch segments

* seg color / switcher

* Fix vtk labelmap cache.

* sidepanel should scroll if there's overflow

* Use a fancier range slider

* Slightly prettier buttons

* Push in progress

* A bit more reactive; handle no-meta

* Update to vtk.js 11.14.0 and react-vtkjs-viewport 0.6.0

* chore: kickstart preview

* Fix conflict issues

* Refactor panel

* Isolate increment functions

* Wip segment table

* Hide selector and add scroll to segmentation

* Increase width of segment description

* Hide not used items

* Add new select for segmentations

* Align ui

* Fix default segmentation

* Fix broken import test

* Get measurements panel back and add spacing to grouped buttons

* Hide brush and selector and refresh when switching segmentations

* Add pointer events back and fix broken tests for measurement table

* Update segment list when switching humbnails

* CR Update: Minor UI adjustments

* CR Update: Set active segment

* Move components to components folder

* Add new settings ui

* Integrate configuration

* Improve styles

* Float numbers

* Improve css

* Refresh settings menu if side panel changed

* Fix settings rendering logic and remove comments

* Extract components

* Remove not used tools

* Remove non used extension

* Use style variables and remove comments

* Remove non used dep from lock

* Remove tools

* Remove segmentation from toolbar

* Add todo and brushradius from config

* Fix packagejson growcut import

* Remove webpack config already defined in parent

* Update package json description

* Change component name to british english to us

* Improve prop types

* Rename cornerstone label map event

* Scope css

* Extract and add doc to helper

* Update jsdocs for getDerivedDatasets

* Add proptypes and cr updates

* Update tablistitem proptypes

* config is readonly, rollback destructuring

* Remove event for side panel

* Add jsdocs for util

* Extract getimageid to studymetada

* Use ohif log instead of console

* Remove specific style from side panel

* Fix default props for brush radius

* Descructure configuration props from settings component to avoid invalid props

* Update proptypes to be required (settings)

* Update readme

* Remove commandsmodule

* Get configuration module inside the component

* Set selected segment

* Rename extension and use modality to filter

* Fix for failing tests (#1423)

* hello world

* Finish rebase for Igor.

* Re-fix cornerstoneTools version that got messed up in merge.

* Fix reactivity of isDisabled, write implementation for the SEG panel.

* Nuke yarn.lock to stack CST to 4.12.0

* Remove debugger statements.

* WIP consume RTSTRUCT

* Doc API and add endpoints.

* Rebase on SEG PR.

* Update DICOM Keyword variables.

* Fix metadata vars format

* Add basic RT panel along with configuration

* Wire up configuration with settings component

* Mock structure set tables

* Improve design of settings

* Integrate list of contours

* Extract panel section

* Filter by seriesuid

* Fix slow settings

* Add jump to image

* Fix expandable items

* Improvements and CR update

* CR Update: Remove google config

* CR Update: Remove erase tool test

* CR Update: Remove instance

* CR Update: Fix issue with undefined prop

* CR Update: Update yarn lock

* Hide actions of structure item

Co-authored-by: James A. Petts <jamesapetts@gmail.com>
Co-authored-by: dannyrb <danny.ri.brown@gmail.com>
Co-authored-by: igoroctaviano] <igoroctaviano@users.noreply.github.com>
Co-authored-by: Mirna Silva <mirna.silva@radicalimaging.com>
2020-04-09 12:21:43 +02:00
ohif-bot 83f9478977 chore(release): publish [skip ci]
- @ohif/i18n@0.52.8
 - @ohif/viewer@3.8.18
2020-04-07 08:33:40 +00:00
Erik Ziegler b5b851ac7c chore(i18n): Pull latest translations from Locize to obtain French translation (thanks @salimkanoun!) (#1613)
* Pull latest translations from Locize to obtain French translation

* Fix newlines
2020-04-07 10:32:20 +02:00
ohif-bot e5175bf3e6 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.9
 - @ohif/core@2.6.8
 - @ohif/ui@1.4.2
 - @ohif/viewer@3.8.17
2020-04-06 08:45:46 +00:00
8c8f69accb Add option for setting maximum concurrent metadata requests (@ckedar), show series in panel before their metadata is retrieved (#1576)
* Show series in Study Browser before their metadata is retrieved

* Switch to decreasing for loop after code review (thanks @ckedar!)

* feat: Limit concurrent series metadata requests (#1581)

* Limit concurrent series metadata requests

* Switch to decreasing for loop after code review (thanks @ckedar!)

* Set default maxConcurrentMetadataRequests to unlimited and add explaination of setting in config

Co-authored-by: kedar.netelixir <kedar.choudhary@netelixir.com>
Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>

* Address comments from code review

* Comment out misbehaving test

Co-authored-by: Kedar <ckedar@gmail.com>
Co-authored-by: kedar.netelixir <kedar.choudhary@netelixir.com>
2020-04-06 10:43:35 +02:00
ohif-bot f9037f831d chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.8
 - @ohif/core@2.6.7
 - @ohif/viewer@3.8.16
2020-04-02 17:58:41 +00:00
James PettsandDanny Brown 9e0bd52c6a fix: 🐛 Fix multiframe images (#1595)
Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-04-02 13:56:53 -04:00
ohif-bot f1b017dc08 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.7
 - @ohif/core@2.6.6
 - @ohif/viewer@3.8.15
2020-04-02 17:32:56 +00:00
81dcbe294d refactor: Reduce bundle size (#1575)
* async load cornerstoneWADOImageLoader chunk

* Lazy initializing of CornerstoneWADIImageLoader - delayed till Viewer creation

* Async loading of react-vtkjs-viewport

* withCornerstone HOC to wrap routes requiring CornerstoneWADOImageLoader initiazation

* removed dead code in comments

* initWeWorkers is no more. Remove al its references.

Co-authored-by: kedar.netelixir <kedar.choudhary@netelixir.com>
Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-04-02 13:31:14 -04:00
Pierre-Antoine Ganaye e09a3d2e93 docs: Remove dead links in docs and improve SOP Class Handler docs (#1593)
* fix docs dead links

* add examples to SOP handler docs
2020-04-02 09:24:49 -04:00
ohif-bot 978a86bf12 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.5.1
 - @ohif/extension-dicom-html@1.1.2
 - @ohif/extension-dicom-microscopy@0.50.9
 - @ohif/extension-dicom-pdf@1.0.3
 - @ohif/viewer@3.8.14
2020-04-02 07:01:58 +00:00
0139a32213 chore: Switch asyncComponent to React Lazy/Suspense (Closes #1577) (#1582)
* Attempted Lazy load for OHIFCornerStoneViewport

* Converted dicom-(html, microscopy, pdf) & cornerstone extensions to React Lazy components

* Fixed incorrect import from OHIFDicomPDFSopClassHandler to ConnectedOHIFDicomPDFViewer

Co-authored-by: Ankit Mohan <ankit.mohan@live.in>
Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
2020-04-02 08:59:17 +02:00
ohif-bot d57158b59d chore(release): publish [skip ci]
- @ohif/extension-dicom-segmentation@0.1.1
 - @ohif/extension-vtk@1.3.6
 - @ohif/core@2.6.5
 - @ohif/viewer@3.8.13
2020-04-01 03:55:23 +00:00
Igor Octaviano 4a7ce1c093 fix: segmentation not loading (#1566)
* Fix bug

* Refactor the state of the component RTPanel
2020-03-31 23:54:02 -04:00
ohif-bot 333677c40e chore(release): publish [skip ci]
- @ohif/extension-dicom-microscopy@0.50.8
 - @ohif/viewer@3.8.12
2020-03-31 13:30:26 +00:00
James Petts b459b894bd Sm fix (#1585)
* Fix broken SM viewport.

* fix: 🐛 Fix broken SM viewport
2020-03-31 15:28:11 +02:00
Andrey Fedorov c5a4860209 update stale.yml to not close IDC related issues (#1573) 2020-03-29 16:10:11 -04:00
ohif-bot 4f79bda7c9 chore(release): publish [skip ci]
- @ohif/viewer@3.8.11
2020-03-26 03:28:31 +00:00
7d22bb7d5a fix: #1312 Cine dialog remains on screen (#1540)
Co-authored-by: kedar.netelixir <kedar.choudhary@netelixir.com>
Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-03-25 23:25:04 -04:00
ohif-bot 36822cc6ce chore(release): publish [skip ci]
- @ohif/viewer@3.8.10
2020-03-26 01:47:51 +00:00
Mirna Silva e676f0b5b5 test: E2E tests for W/L Presets (#1557)
* E2E tests for W/L Presets

* CI Fix
2020-03-25 21:46:28 -04:00
ohif-bot 57de66a65f chore(release): publish [skip ci]
- @ohif/viewer@3.8.9
2020-03-25 17:55:57 +00:00
Kedarandkedar.netelixir 99022f2bac fix: Load measurement in active viewport. (#1558)
Possible solution for request https://github.com/OHIF/Viewers/issues/1522 ?

Co-authored-by: kedar.netelixir <kedar.choudhary@netelixir.com>
2020-03-25 13:53:47 -04:00
ohif-bot 47b1e2837f chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.5
 - @ohif/core@2.6.4
 - @ohif/viewer@3.8.8
2020-03-25 17:04:29 +00:00
Kedarandkedar.netelixir d604ebaffd fix: disable autoFreeze of immer, even in dev mode (#1560)
Co-authored-by: kedar.netelixir <kedar.choudhary@netelixir.com>
2020-03-25 12:56:46 -04:00
Erik ZieglerandDanny Brown c1a0d3c662 fix: Add support for single entries in SequenceOfUltrasoundRegions. M… (#1559)
* fix: Add support for single entries in SequenceOfUltrasoundRegions. More than one region will need lower-level changes

* Update platform/core/src/utils/metadataProvider/getPixelSpacingInformation.js

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-03-25 12:55:39 -04:00
ohif-bot 16866a47a0 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.4
 - @ohif/core@2.6.3
 - @ohif/viewer@3.8.7
2020-03-24 17:13:15 +00:00
Erik Ziegler 77ab0ad9a1 fix: Ensure we take into account pixel spacing fields properly (#1555)
* fix: Ensure we take into account pixel spacing fields properly

* Rename to getPixelSpacingInformation
2020-03-24 18:10:58 +01:00
ohif-bot 1c306dd243 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.3
 - @ohif/core@2.6.2
 - @ohif/viewer@3.8.6
2020-03-24 15:19:55 +00:00
Erik Ziegler 404d52fe5c fix: OverlayPlane module usage for ArrayBuffer, BulkDataURI, and InlineBinary cases, as well as PaletteColor LUTs for ArrayBuffer (i.e. local drag/drop) case (#1546) 2020-03-24 16:18:13 +01:00
ohif-bot d9a50b6b08 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.2
 - @ohif/core@2.6.1
 - @ohif/viewer@3.8.5
2020-03-23 18:34:20 +00:00
Kedarandkedar.netelixir e41d339f5f fix: avoid-wasteful-renders (#1544)
* Avoid un-necessary redux state update for frequent actions

* Avoid un-necessary re-render when no change in props

Co-authored-by: kedar.netelixir <kedar.choudhary@netelixir.com>
2020-03-23 14:31:39 -04:00
ohif-bot e374fe2118 chore(release): publish [skip ci]
- @ohif/viewer@3.8.4
2020-03-19 12:26:24 +00:00
Jack Cosgrove 9feadd3c6d fix: Only permit web workers to be initialized once. (#1535) 2020-03-19 13:21:20 +01:00
ohif-bot 3b5f71b0be chore(release): publish [skip ci]
- @ohif/extension-vtk@1.3.1
 - @ohif/ui@1.4.1
 - @ohif/viewer@3.8.3
2020-03-17 22:49:26 +00:00
Darren b269415048 fix: rendering delete button when text is too wide for parent div (#1526) 2020-03-17 18:48:06 -04:00
ohif-bot c9e421cff3 chore(release): publish [skip ci]
- @ohif/viewer@3.8.2
2020-03-17 18:19:35 +00:00
Mirna SilvaandDanny Brown d9999c1ae4 ci: Added verification on Before hook to avoid navigating again to viewer page (#1505)
* Added verification on Before hook to avoid navigating again to viewer page

* Removed unecessary screenshots and duplicated test cases

* Navigation Fix for HTML extension and improved Flip button verification

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-03-17 14:14:04 -04:00
ohif-bot 31ba042121 chore(release): publish [skip ci]
- @ohif/viewer@3.8.1
2020-03-17 13:08:24 +00:00
Anth Lubic 2747effd9e fix: resolves #1483 (#1527) 2020-03-17 14:05:56 +01:00
ohif-bot 6ed1e47a3d chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.5.0
 - @ohif/extension-dicom-segmentation@0.1.0
 - @ohif/extension-vtk@1.3.0
 - @ohif/core@2.6.0
 - @ohif/ui@1.4.0
 - @ohif/viewer@3.8.0
2020-03-13 19:47:28 +00:00
e8842cf8ae feat: Segmentations Settings UI - Phase 1 #1391 (#1392)
* Filter segs and fetch correct metadata.

* Initiate seg loading logic on series load into viewport. Need to write.

* Automatic consumption of SEG functional.

* Pull in master.

* Fix issues after pulling in master.

* Clean up old segmentation handlers.

* WIP

* WIP

* Poppulate viewport with metadata.

* Show hide/segmentation in react-vtkjs-viewport.

* feat: 🎸 View segmentations in vtkjs viewport

* Filter segs and fetch correct metadata.

* Initiate seg loading logic on series load into viewport. Need to write.

* Automatic consumption of SEG functional.

* Pull in master.

* Fix issues after pulling in master.

* Clean up old segmentation handlers.

* WIP

* WIP

* Poppulate viewport with metadata.

* Show hide/segmentation in react-vtkjs-viewport.

* feat: 🎸 View segmentations in vtkjs viewport

* Rebase on master.

* format format format

* add basic extension parts

* Updated yarn lock

* Fetch derived/referenced displaysets in sidepanel

* Add buttons, expose seriesDescription, write notes for Danny.

* Add growcut tool extention. -> Move to a seperate repo.

* Change some text fields.

* expose dialog provider

* hide draggable area

* No pointer events for now; need to revisit

* Don't filter seg from the outset for now

* fiddling about

* fix path to series data

* fix logic to determine next available labelmapindex

* Ability to swtich between labelmaps

* Return a promise so we can make sure our labelmap is loaded before pulling metadata

* Better UI to toggle labelmaps

* Fix accidental untoggle of active labelmap for ui

* Remove brush tool from cornerstone extension

* Add cstools in preInit for seg extension

* Add buttons for seg magic

* Hide the measurements panel for now

* Slightly better seg/scissor icons

* Add eraser

* ability to switch segments

* seg color / switcher

* Fix vtk labelmap cache.

* sidepanel should scroll if there's overflow

* Use a fancier range slider

* Slightly prettier buttons

* Push in progress

* A bit more reactive; handle no-meta

* Update to vtk.js 11.14.0 and react-vtkjs-viewport 0.6.0

* chore: kickstart preview

* Fix conflict issues

* Refactor panel

* Isolate increment functions

* Wip segment table

* Hide selector and add scroll to segmentation

* Increase width of segment description

* Hide not used items

* Add new select for segmentations

* Align ui

* Fix default segmentation

* Fix broken import test

* Get measurements panel back and add spacing to grouped buttons

* Hide brush and selector and refresh when switching segmentations

* Add pointer events back and fix broken tests for measurement table

* Update segment list when switching humbnails

* CR Update: Minor UI adjustments

* CR Update: Set active segment

* Move components to components folder

* Add new settings ui

* Integrate configuration

* Improve styles

* Float numbers

* Improve css

* Refresh settings menu if side panel changed

* Fix settings rendering logic and remove comments

* Extract components

* Remove not used tools

* Remove non used extension

* Use style variables and remove comments

* Remove non used dep from lock

* Remove tools

* Remove segmentation from toolbar

* Add todo and brushradius from config

* Fix packagejson growcut import

* Remove webpack config already defined in parent

* Update package json description

* Change component name to british english to us

* Improve prop types

* Rename cornerstone label map event

* Scope css

* Extract and add doc to helper

* Update jsdocs for getDerivedDatasets

* Add proptypes and cr updates

* Update tablistitem proptypes

* config is readonly, rollback destructuring

* Remove event for side panel

* Add jsdocs for util

* Extract getimageid to studymetada

* Use ohif log instead of console

* Remove specific style from side panel

* Fix default props for brush radius

* Descructure configuration props from settings component to avoid invalid props

* Update proptypes to be required (settings)

* Update readme

* Remove commandsmodule

* Get configuration module inside the component

* Set selected segment

* Rename extension and use modality to filter

* Fix for failing tests (#1423)

* hello world

* Finish rebase for Igor.

* Re-fix cornerstoneTools version that got messed up in merge.

* Fix reactivity of isDisabled, write implementation for the SEG panel.

* Nuke yarn.lock to stack CST to 4.12.0

* Remove debugger statements.

* fix: bump cornerstone-tools version to address toFixed issue

* test: force tests for MISTER^MR to navigate to study directly

* test: remove eraser tool test

* test: count is off by one

* test: don't save SR state for the time being

* test: disable more tests

Co-authored-by: James A. Petts <jamesapetts@gmail.com>
Co-authored-by: dannyrb <danny.ri.brown@gmail.com>
Co-authored-by: igoroctaviano] <igoroctaviano@users.noreply.github.com>
Co-authored-by: Mirna Silva <mirna.silva@radicalimaging.com>
2020-03-13 15:45:26 -04:00
ohif-bot d426b78967 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.4.3
 - @ohif/extension-dicom-html@1.1.1
 - @ohif/extension-dicom-microscopy@0.50.7
 - @ohif/extension-dicom-pdf@1.0.2
 - @ohif/extension-vtk@1.2.24
 - @ohif/core@2.5.3
 - @ohif/i18n@0.52.7
 - @ohif/ui@1.3.3
 - @ohif/viewer@3.7.8
2020-03-09 19:04:54 +00:00
James PettsandDanny 1d85f7a74e Instance metadata/metadata providers overhaul (#1481)
* Instance metadata plus metadata provider overhaul.

Fix consumption of wado-uri urls

fallbacks + datatype agnosticism.

WIP DICOMify things.

fix various issues with naturalized variable naming migration.

Remove metadata provider.

Fix consumption of multiframe images and addition of CWIL metadata.

Fix strange build issues.

Fix CWIL style windowWidth to array from naturalized DICOM.

Fix PT, CT, CR and DX issues for cornerstone + DX issues for vtkjs.

Move color palette fetching down to the natuaralized JSON level.

Remove unused StudyMetadataSummary

Remove redundant dicom metadata dictionary.

Working local + json routes.

Fix SR read.

Finished first round of testing + cleaned up debugging etc.

* data => metadata for instance naturalizedJSON

* Update dcmjs version

* Correct github isssues.

* Fix erroneously replaced files.

* Danny's recommended changes.

* Instance metadata plus metadata provider overhaul.

Fix consumption of wado-uri urls

fallbacks + datatype agnosticism.

WIP DICOMify things.

fix various issues with naturalized variable naming migration.

Remove metadata provider.

Fix consumption of multiframe images and addition of CWIL metadata.

Fix strange build issues.

Fix CWIL style windowWidth to array from naturalized DICOM.

Fix PT, CT, CR and DX issues for cornerstone + DX issues for vtkjs.

Move color palette fetching down to the natuaralized JSON level.

Remove unused StudyMetadataSummary

Remove redundant dicom metadata dictionary.

Working local + json routes.

Fix SR read.

Finished first round of testing + cleaned up debugging etc.

* data => metadata for instance naturalizedJSON

* Update dcmjs version

* Correct github isssues.

* Fix erroneously replaced files.

* Danny's recommended changes.

* Update JSON CI

* Update casing of import.

* Fix jump for SR.

* Fix unit tests for measurements service

* Fix json CI test.

* fix: update yarn lock

* Fix local non-encapsulated pdf view

* CI updated to new sucess message.

Co-authored-by: Danny <danny.ri.brown@gmail.com>
2020-03-09 19:03:23 +00:00
ohif-bot 8833731ad3 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.4.2
 - @ohif/extension-vtk@1.2.23
 - @ohif/ui@1.3.2
 - @ohif/viewer@3.7.7
2020-03-09 13:39:36 +00:00
Danny Brown bb7cce4716 feat: Remove Eraser
feat: Remove Eraser
2020-03-09 09:36:26 -04:00
Danny Brown 4ad3ab93b0 Merge branch 'master' into fix-toolbar-tools 2020-03-09 09:35:27 -04:00
ohif-bot 3e074da64e chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.22
 - @ohif/core@2.5.2
 - @ohif/viewer@3.7.6
2020-03-06 09:53:40 +00:00
d66bad664a Fix for correct reading of enableStudyLazyLoad setting (#1333)
Co-authored-by: Rutman <victor.rutman@changehealthcare.com>
Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
2020-03-06 10:50:16 +01:00
Rodrigo Antinarelli fa47db4a54 Merge branch 'master' into fix-toolbar-tools 2020-03-05 12:13:53 -03:00
ohif-bot e1c21a7ce1 chore(release): publish [skip ci]
- @ohif/viewer@3.7.5
2020-03-05 15:08:13 +00:00
Danny Brown c785a5e975 feat: is disabled flag for sidepanels (#1461)
* chore: updated autofix settings

* chore: remove dead code

* chore: further identify study schema/props

* chore: sketch out beginnings of isDisabled method for sidePanel menuOptions

* chore: set API as studies object

* docs: update docs to include isDisabled computed

* chore: note regarding reactivity

* chore: handle function not set (preserve non-breaking)

* chore: clarify how props are added

* chore: specify left side for built-in series button (thumbnails)

* chore: remove 'from', and push to correct key
2020-03-05 10:05:51 -05:00
Danny Brown f133be75d1 Merge branch 'master' into feat/is-disabled-flag-for-sidepanels 2020-03-05 09:39:58 -05:00
Rodrigo Antinarelli b880663818 add ROI Window 2020-03-04 12:57:27 -03:00
Rodrigo Antinarelli e160889605 add ROI Window 2020-03-04 12:56:04 -03:00
Rodrigo Antinarelli 2f0d1b651b Merge branch 'master' of https://github.com/OHIF/Viewers into fix-toolbar-tools 2020-03-04 12:13:23 -03:00
Rodrigo Antinarelli d034006294 unremove ROI Window 2020-03-04 12:13:14 -03:00
ohif-bot 2f459a0f64 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.4.1
 - @ohif/viewer@3.7.4
2020-03-03 05:46:36 +00:00
Danny Brown ee80e02661 fix: bump react-cornerstone-viewport version to address critical issue (#1473) 2020-03-03 00:42:01 -05:00
ohif-bot 4e53fbfe9d chore(release): publish [skip ci]
- @ohif/viewer@3.7.3
2020-03-02 20:45:50 +00:00
Gustavo André LelisandDanny Brown 64dfbeab7a fix: GCloud dataset picker dialog broken (#1453)
* Replace class with className

* Start implementing UIDialogService

* Including UIModalService on dicomStorePicker

* Cleanup

* Remove unecessary divs

* Remove props not used

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-03-02 15:43:39 -05:00
ohif-bot 9c1112f768 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.21
 - @ohif/ui@1.3.1
 - @ohif/viewer@3.7.2
2020-02-29 14:01:25 +00:00
arthur-miller 9b3be9b0c0 fix: prevent the native context menu from appearing when right-clicking on a measurement or angle (https://github.com/OHIF/Viewers/issues/1406) (#1469) 2020-02-29 08:59:47 -05:00
dannyrb 6829542c00 chore: remove 'from', and push to correct key 2020-02-24 11:36:10 -05:00
dannyrb 846c9c1c54 chore: specify left side for built-in series button (thumbnails) 2020-02-24 11:35:34 -05:00
dannyrb 85fed49eb1 chore: clarify how props are added 2020-02-24 11:28:35 -05:00
dannyrb 298da03e2f chore: handle function not set (preserve non-breaking) 2020-02-24 11:22:41 -05:00
dannyrb 141e1cf648 chore: note regarding reactivity 2020-02-24 11:20:10 -05:00
dannyrb ea2ab1a25e docs: update docs to include isDisabled computed 2020-02-24 11:18:55 -05:00
dannyrb 3d0357c9c0 chore: set API as studies object 2020-02-24 11:16:24 -05:00
dannyrb f68aaf973c chore: sketch out beginnings of isDisabled method for sidePanel menuOptions 2020-02-21 16:09:55 -05:00
dannyrb f04d459a63 chore: further identify study schema/props 2020-02-21 16:09:05 -05:00
dannyrb 4cf9f2c9cb chore: remove dead code 2020-02-21 16:08:48 -05:00
dannyrb c0cc9752c6 chore: updated autofix settings 2020-02-21 16:08:15 -05:00
Rodrigo Antinarelli 2910d38668 Remove E2E test for Eraser tool 2020-02-21 16:56:30 -03:00
Rodrigo Antinarelli 6c950a9669 fix: Remove Eraser and ROI Window 2020-02-21 16:27:32 -03:00
ohif-bot 06075ecb51 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.20
 - @ohif/core@2.5.1
 - @ohif/viewer@3.7.1
2020-02-21 02:44:01 +00:00
Igor Octaviano 509a7b7e57 fix: 💡 Update log to warn (#1454)
Update measurement service log to warn

✅ Closes: #1451
2020-02-20 21:41:01 -05:00
ohif-bot 4bbf24b250 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.4.0
 - @ohif/extension-vtk@1.2.19
 - @ohif/core@2.5.0
 - @ohif/ui@1.3.0
 - @ohif/viewer@3.7.0
2020-02-20 22:04:03 +00:00
Gustavo André Lelis ebc01a8ca2 feat: #1342 - Window level tab (#1429)
* WindowLevel preset code changes

* Remove localStorage code as its already being saved and some small refactor on reducer/action

* Creating unit tests for preferences reducer

* Fix cypress after class naming change

* Make hidden false as default for tab components

* Remove addUserPreferences

* Small refactor to use commandsManager from getCommandsModule instead of getting it from App.js
2020-02-20 16:25:26 -05:00
ohif-bot 4fd0c6dfcb chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.3.1
 - @ohif/viewer@3.6.3
2020-02-14 20:56:06 +00:00
Gustavo André Lelis 06a4af06fa fix: Creating 2 commands to activate zoom tool and also to move between displaySets (#1446) 2020-02-14 15:54:08 -05:00
ohif-bot 6cba41dd8b chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.18
 - @ohif/core@2.4.1
 - @ohif/i18n@0.52.6
 - @ohif/ui@1.2.1
 - @ohif/viewer@3.6.2
2020-02-12 20:36:52 +00:00
2f30e7a821 fix: Combined Hotkeys for special characters (#1233)
* fix: Combined Hotkeys for special characters

* add record method to hotkey manager

* fix record plugin

* remove unused component

* add record to modal props

* rename record method

* replace handlers to use hotkeyRecord

* fix combined keys

* change expected result count from 18 to 17

* autoformat

* Remove duplicate test, that was testing the wrong things; fix label; update configs

* Revert "Remove duplicate test, that was testing the wrong things; fix label; update configs"

This reverts commit 4292f4fe67351962d61cae623b920dcbd87dd71d.

* Fix the record plugin's registration

* fix exposed record method usage

* adding logging for info level items

* Hotkey definitions don't need to be globally reactive; use localstorage/appconfig as sources of truth; not redux

* Tidy up test

* Remove unused code from UserPreferencesForm

* Log info when we run a command

* fix hotkey preference restore

* use application configured hotkeys if there are no user preferred

* Avoid logging circular ref

* Fix callouts

* Fix small issue with array

* Fix langua issue after refactor and merge

* Refactor on recordCurrentCombo as Rodrigo did before

* Separating components in 2 files

* WIP Refactor to simplify the user preferences and move into each form the save and controll functionalities

* Remove context

* Remove unused import

* Initial work on Field treatment

* Refactor General preferences

* Small refactor removing type from HotkeyField

* small update on style

* Refactor and layout fixed

* Make hotkeys preferences working with old hotkeys row

* Move error handling out of hotkey row/input component

* WIP custom form

* Moving validation function to component

* Exposing hotkeyRecord as it does not depend on HotkeyManager Class

* Making hotkeyField as much detached possible from parent component

* Small refactors

* Refactor on user preferences

* Clean up into the changes

* Small fix to let save working

* Style finish

* move about docs into about folder

* Fix double tap on single keys

* Style refactor

* Remove log

* Fix log issues on unit tests

* Fix unit test breaking on ohif/core index

* Fixing hotkeys unpause unit test issue

* Rename file to adopt lowercase

* Rename file to adopt lowercase

* Fixing callouts

* Big refactor miving some of the components into viewer and creating small components into ohif/ui

* Typo on folder name

* Updating ohif ui docs

* Remove comments

* Fix binding of combo keys

* Fix some cypress tests failures

* Fixing onCancel button

* Fixing e2e tests

* Small style update

* Fixing unit tests failing after fix issue

* Remove some not used code

* Remove left over after debug

* Adding prevent default on hotkeys events

* Fixinf existing hotkeys validator with 3 keys pressed

* Exposing hotkeys as root level on ohif-core

* Clean up

* Exposing all availableLanguages with labels and fixing an issue on language switcher

* Fixing e2e cypress tests

* Preveinting some simple errors

* Treating error once we try to set hotkey definitions

* Adding ui notification on setHotkeys errors

* Implementing a service queue request to hold until functions are implemented

* Making sure toFixed is only called on Numbers

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
Co-authored-by: Gustavo André Lelis <galelis@gmail.com>
2020-02-12 15:35:04 -05:00
ohif-bot 9a62c28b3f chore(release): publish [skip ci]
- @ohif/extension-lesion-tracker@0.2.0
 - @ohif/extension-vtk@1.2.17
 - @ohif/ui@1.2.0
 - @ohif/viewer@3.6.1
2020-02-10 20:36:28 +00:00
Igor Octaviano 98a649b455 feat: Lesion tracker right panel (#1428)
* WIP

* Finish mock

* Update name, remove vendor prefix

* Remove lesion tracker from defaults

* Refactor measurement table changes

* Remove unused code

* Update naming of collections
2020-02-10 15:33:06 -05:00
ohif-bot 3529474a16 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.3.0
 - @ohif/extension-vtk@1.2.16
 - @ohif/core@2.4.0
 - @ohif/viewer@3.6.0
2020-02-10 19:17:19 +00:00
Igor OctavianoandDanny Brown 0c37a406d9 feat: 🎸 MeasurementService (#1314)
* feat: 🎸 MeasurementService

Work in progress...

* Remove cornerstone tools import

* Second iteration

* CR Update: Add update / added events example

* Add new props to measurements

* Update event log

* Add new measurementid to annotation

* Add context support

* Add value types

* Add area

* Add todo

* Wip measurement to annotation map

* Change points representation

* Add props to annotation mapping

* Add tests

* Extract formatter from init and add tests

* Sketch matchers

* Fix events and valuetypes imports

* Remove context support

* Rename formatter to mappings

* Sketching source and source definitions

* Adjust matching criteria in addOrUpdate

* CR Updates: Extract private functions and rename variables

* Fix broken tests

* Add more measurement service tests

* Update broken mapping tests

* Update test description

* Update getAnnotation to get mapping based on def and source

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2020-02-10 14:15:05 -05:00
ohif-bot e8d56c11f0 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.15
 - @ohif/core@2.3.9
 - @ohif/viewer@3.5.1
2020-02-07 16:11:02 +00:00
Erik Ziegler a6b92a4505 Fix issues with isDisplaySetReconstructable when we have empty DICOM tags (closes #1280) (#1426) 2020-02-07 17:09:37 +01:00
ohif-bot a4c9a86772 chore(release): publish [skip ci]
- @ohif/extension-lesion-tracker@0.1.0
 - @ohif/viewer@3.5.0
2020-02-06 19:07:34 +00:00
Danny Brown 73e440968c feat: lesion-tracker extension (#1420)
* chore: init lesion-tracker extension

* chore: dev build for lesion-tracker extension

* fix: a more appropriate library name

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

* Imprelemnted more unit tests

* Remove missing comment

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

* feat: listen for vtkscrollevent in wrapper component

* fix: definitions can skip empty storeContexts key

* hoc to set/pass in commandsManager

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

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

* fix: make sure we include @JamesAPetts bug fix

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

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

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

* fix filename error

* Create variable to track erros

* Fix small console error

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

* Small improvement on select and textinput labels

* Add new icon for unlink

* Refactor on download image modal

* Adding loading screen

* Fix translation issue

* Fixing aspect Ratio and E2E tests

* Allow empty value without setting to 0

* Remove eslint comments

* Fixing typos

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

* fix test title

* update yarn.lock

* Increased timeout for thumbnails

* Removing PDF file test from script-tag server

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

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

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

Clean up JSON launch with DICOMWeb server

✅ Closes: #1387

* Remove test file

* parse seriesinstanceuids in query

* Add default studies

* Remove test file

* Cleanup retrievedata changes

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

* Fixed bug in script.

* Fixed PR remark.

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

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

Closes: #1241

* Move setCornerstoneViewport command into cornerstone extension

* Exposing extensionManager into extension's get modules functions

* Remove Plugin switch

* Updating set cornerstonelayout to work on VIEWER context

* Creating a customComponent for 2D MPR button

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

* Make buttons hidden depending on extension registration

* Fix unit test after changes on extensionManager

* Remove workaround for extensionManager

* Use studyMeatadataManager and remove TODO's

* Remove storeContext not needed

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

* Remove PluginSwitch files

* Fix E2E tests

* Adding cypress screenshots into gitignore

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

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 15:59:36 -05:00
ohif-bot a436436def chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.2
 - @ohif/ui@1.1.7
 - @ohif/viewer@3.3.2
2019-12-20 19:10:48 +00:00
Mirna Silva d6d92afbda test: Refactoring toolbar data-cy selectors (#1323) 2019-12-20 14:07:00 -05:00
ohif-bot e1142910e4 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.1
 - @ohif/ui@1.1.6
 - @ohif/viewer@3.3.1
2019-12-20 18:57:38 +00:00
Mirna Silva 067e6c769b test: Save Measurements Functionality (#1300)
* E2E Tests: Save Measurements Functionality

* Moving test file to PWA only. Script-tag doesn't have the required Study

* Removing data-cy selector from cornerstone toolbar
2019-12-20 13:54:59 -05:00
ohif-bot d375d02a4b chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.0
 - @ohif/extension-vtk@1.2.0
 - @ohif/core@2.3.0
 - @ohif/viewer@3.3.0
2019-12-20 17:18:56 +00:00
Igor OctavianoandDanny Brown 63594d36b0 feat: 🎸 Configuration so viewer tools can nix handles (#1304)
* feat: 🎸 Configuration so viewer tools can nix handles

Adds a key to cornerstone configuration that allows to toggle on/off
handle rendering

Closes: #1223

* Update extension docs

* Update doc

* docs: include hideHandles configuration in our configuration docs

* chore: default to hiding handles for annotations

* chore: switch to drawHandlesOnHover to mimic legacy viewer functionality

* Flip logic for drawHandlesOnHover; reverse of hideHandles

* Don't hide the handles of ellipse or rectangle roi

* invert check

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 12:15:40 -05:00
ohif-bot 6c160f517f chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.7
 - @ohif/ui@1.1.5
 - @ohif/viewer@3.2.2
2019-12-19 15:15:49 +00:00
Gustavo André Lelis 23305cec9c fix: 🐛 Fix drag-n-drop of local files into OHIF (#1319)
* fix: 🐛 Fix drag-n-drop of local files into OHIF

Closes: #1307

* CR Update: Centralize dialogs
2019-12-19 10:14:05 -05:00
ohif-bot 36a741f0bc chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.6
 - @ohif/core@2.2.1
 - @ohif/viewer@3.2.1
2019-12-18 18:29:28 +00:00
Igor Octaviano 695ad732e1 refactor: 💡 Move DICOMSR code to @ohif/core project (#1310)
* Move DICOMSR to @ohif/core

* refactor: 💡 Move DICOMSR code to @ohif/core project

We've just merged support for DICOM Structured Reports. Today, support
for this feature lives in the @ohif/viewer project in it's lib folder.
Ideally, it should be moved to @ohif/core and given a clear public API
and unit tests.

Closes: #1282

* Update imports

* CR Update: Update imports
2019-12-18 13:25:53 -05:00
ohif-bot 73a3f07e2b chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.5
 - @ohif/core@2.2.0
 - @ohif/viewer@3.2.0
2019-12-16 19:11:59 +00:00
Igor Octaviano 4ea239a953 feat: 🎸 Expose extension config to modules (#1279)
* feat: 🎸 Expose extension config to modules

Currently, only the preRegistration hook receives the extension's
configuration as a parameter. Providing getModuleFn's with the
extension's configuration, and all lifecycle/modules with the
application's configuration as rootConfig should open the doors to more
configurable extensions.

Closes: #1268

* CR Update: Pass extension and service config through extension manager preinit/getmodule

* CR Update: Remove appConfig from servicesManager

* CR Update: Remove appconfig variable
2019-12-16 14:07:53 -05:00
ohif-bot 625d5d70a7 chore(release): publish [skip ci]
- @ohif/viewer@3.1.12
2019-12-16 18:30:12 +00:00
Igor Octaviano 5c3d8b37b6 fix: 🐛 Dismiss all dialogs if leaving viewer route #1242 (#1301)
* fix: 🐛 #1242

Dismiss all dialogs if route change

Closes: #1242

* CR Update: Dismiss dialogs in videwer
2019-12-16 13:26:31 -05:00
ohif-bot 8bbbf3a889 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.1.1
 - @ohif/viewer@3.1.11
2019-12-16 16:49:50 +00:00
arthur-miller d5bf72851a fix: 🐛 add WwwcRegionTool to cornerstone tools initialization (#1302) 2019-12-16 11:47:02 -05:00
ohif-bot 2914c83032 chore(release): publish [skip ci]
- @ohif/extension-dicom-pdf@1.0.1
 - @ohif/viewer@3.1.10
2019-12-16 16:13:00 +00:00
Igor Octaviano 31e542dfd9 fix: 🐛 Update ohif pdf extension to optionally use pdfjs (#1162)
* fix: 🐛 Update ohif pdf extension to optionally use pdfjs

Update ohif pdf extension of optionally use pdfjs

Closes: #1049

* Update branch and fix scrolling

* Dynamically import component

* Fix wrong file change

* Fix import

* Add multi opened pdf configuration

* Refactor connected component

* Specify worker explicitly

* Specify worker explicitly

* CR Update: Remove dead code

* CR Update: Refactor state

* CR Update: use refs

* CR Update: use refs
2019-12-16 11:10:10 -05:00
ohif-bot 25599a2e2a chore(release): publish [skip ci]
- @ohif/i18n@0.52.4
 - @ohif/viewer@3.1.9
2019-12-16 15:13:14 +00:00
Danny Brown 1cf7015511 docs: stub environment variables docs 2019-12-16 09:47:12 -05:00
Igor Octaviano 756f70cc54 refactor: 💡 Use env variables to set locize (#1299)
Instead of relying in dynamic configuration, use dedicated env variables
to set locize api key / configuration

Closes: #1271
2019-12-16 09:41:41 -05:00
ohif-bot ded2e61913 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.4
 - @ohif/core@2.1.1
 - @ohif/ui@1.1.4
 - @ohif/viewer@3.1.8
2019-12-16 14:28:33 +00:00
Igor Octaviano b58aa4575a refactor: Remove LabelingFlow/ContextMenu services and reimplement on top of the Dialog service #1264 (#1292)
* Remove ContextMenu and Labelling services and provider to use DialogService directly

* Refactor init to display labelling

* Cleanup labelling flow in measurements init

* Cleanup viewer imports

* Add classname to toolcontextmenu

* Add error message
2019-12-16 09:19:40 -05:00
Danny Brown f78abe2afa docs: stub viewer configuration 2019-12-16 09:15:10 -05:00
Danny Brown f48157117f docs: hide empty pages 2019-12-16 09:00:24 -05:00
Danny Brown 6f8bd0dff8 docs: fix scope of diagram path 2019-12-16 08:57:43 -05:00
498 changed files with 19807 additions and 13006 deletions

No files matched your search

+2 -1
View File
@@ -328,6 +328,7 @@ workflows:
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
@@ -352,7 +353,7 @@ workflows:
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*'
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
+23 -9
View File
@@ -1,17 +1,31 @@
#!/bin/bash
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
if [ ! -z "$CLIENT_ID" ]
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
echo "Google Cloud Healthcare $CLIENT_ID has been provided: "
echo $CLIENT_ID
echo "Updating config..."
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
if [ -n "$CLIENT_ID" ]
then
echo "Google Cloud Healthcare \$CLIENT_ID has been provided: "
echo "$CLIENT_ID"
echo "Updating config..."
# - Use SED to replace the CLIENT_ID that is currently in google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
# - Use SED to replace the CLIENT_ID that is currently in google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
fi
# - Copy google.js to overwrite app-config.js
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
# If HEALTHCARE_API_ENDPOINT is specified, use the google.js configuration with the modified endpoint
if [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
echo "Google Cloud Healthcare \$HEALTHCARE_API_ENDPOINT has been provided: "
echo "$HEALTHCARE_API_ENDPOINT"
echo "Updating config..."
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
sed -i -e "s+https://healthcare.googleapis.com/v1beta1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
fi
# - Copy google.js to overwrite app-config.js
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
fi
echo "Starting Nginx to serve the OHIF Viewer..."
+5 -1
View File
@@ -12,11 +12,15 @@ exemptLabels:
- 'Task: CI/Tooling :robot:'
- 'Task: Docs 📖'
- 'Task: Docs :book:'
- 'Task: Refactor 🛠'
- 'Task: Refactor :hammer_and_wrench:'
- 'Task: Tests :microscope:'
- 'PR: Awaiting Review 👀'
- 'Triage :white_flag:'
- 'Extension: Discussion'
- 'Announcement 🎉'
- 'IDC:priority'
- 'IDC:candidate'
- 'IDC:collaboration'
# Label to use when marking an issue as stale
staleLabel: 'Stale :baguette_bread:'
# Comment to post when marking an issue as stale. Set to `false` to disable
+1
View File
@@ -30,6 +30,7 @@ docker/dcm4che/dcm4che-arc
# Cypress test results
videos/
screenshots/
# Locize settings
.locize
+4 -1
View File
@@ -28,5 +28,8 @@
],
"prettier.disableLanguages": ["html"],
"prettier.endOfLine": "lf",
"workbench.colorCustomizations": {}
"workbench.colorCustomizations": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
+5
View File
@@ -72,12 +72,17 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
},
plugins: [
new webpack.DefinePlugin({
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
}),
],
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
+1 -1
View File
@@ -48,7 +48,7 @@ Check out these helpful links:
-->
<!-- prettier-ignore-start -->
[ohif-org]: https://www.ohif.org
[ohif-org]: http://www.ohif.org
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
[gh-issues]: https://github.com/OHIF/Viewers/issues
<!-- prettier-ignore-end -->
+1 -2
View File
@@ -1,11 +1,11 @@
# OHIF Viewers
- [Our Process](our-process.md)
- [Quick Start](quick-start.md)
- Development
- [Getting Started](development/getting-started.md)
- [Contributing](development/contributing.md)
- [Continuous Integration](development/continous-integration.md)
- [Testing](development/testing.md)
- [Configuring](configuring/index.md)
- [Data Source](configuring/data-source.md)
@@ -31,7 +31,6 @@
- [OHIF Maintained](extensions/index.md#maintained-extensions)
- [Services](services/index.md)
- [Default](services/default/index.md)
- [Measurements](services/default/measurements.md)
- [UI](services/ui/index.md)
- [Dialog Service](services/ui/ui-dialog-service.md)
- [Modal Service](services/ui/ui-modal-service.md)
+5 -10
View File
@@ -13,7 +13,6 @@ architected, and the role each of it's dependent libraries plays.
- [Overview](#overview)
- [Business Logic](#business-logic)
- [Component Library](#react-component-library)
- [Internationalization](#internationalization-i18n)
- [Extensions & Configuration](#extensions--configuration)
- [Common Questions](#common-questions)
@@ -74,10 +73,10 @@ This diagram is a conceptual illustration of how the Viewer is architected.
The [`@ohif/core`][core-github] project offers pre-packaged solutions for
features common to Web-based medical imaging viewers. For example:
- [Hotkeys](#)
- Hotkeys
- DICOM Web requests
- Hanging Protocols
- [Managing a study's measurements](../services/default/measurements.md)
- Managing a study's measurements
- Managing a study's DICOM metadata
- [A flexible pattern for extensions](../extensions/index.md)
- And many others
@@ -99,10 +98,6 @@ application.
[Check out our component library!](https://react.ohif.org/)
## Internationalization (i18n)
...
## Extensions & Configuration
While OHIF maintains several high value and commonly requested features in its
@@ -120,7 +115,7 @@ the viewer. Some common use cases include:
We expose common integration points via [extensions](../extensions/index.md) to
make this possible. The viewer and many of our own extensions also offer
[configuration][configuration]. For a list of extensions maintained by OHIF,
[check out this helpful table](../extensions/index.mdl#maintained-extensions).
[check out this helpful table](../extensions/index.md#maintained-extensions).
If you find yourself thinking "I wish the Viewer could do X", and you can't
accomplish it with an extension today, create a GitHub issue! We're actively
@@ -147,12 +142,12 @@ you would need to recreate.
-->
<!-- prettier-ignore-start -->
[monorepo]: #
[monorepo]: https://github.com/OHIF/Viewers/issues/768
[viewers-project]: https://github.com/OHIF/Viewers
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
[pwa]: https://developers.google.com/web/progressive-web-apps/
[configuration]: ../configuring/index.md
[extensions]: ../extensions/index.md
[core-github]: https://github.com/OHIF/viewers/platform/core
[ui-github]: https://github.com/OHIF/Viewers/platform/ui
[ui-github]: https://github.com/OHIF/Viewers/tree/master/platform/ui
<!-- prettier-ignore-end -->
+107 -10
View File
@@ -123,14 +123,111 @@ There are two important steps to making sure this setup works:
Most image archives do not provide either of these features "out of the box".
It's common to use IIS, Nginx, or Apache to route incoming requests and append
appropriate headers. You can find an example of this setup in our
[Nginx + Image Archive Deployment Recipe](deployment/recipes/nginx--image-archive.md).
[Nginx + Image Archive Deployment Recipe](./recipes/nginx--image-archive.md).
#### What if my archive doesn't support DicomWeb?
> This is possible to do with the OHIF Viewer, but not as straightforward. Look
> out for documentation on this subject in the near future.
It's possible to supply all Study data via JSON format, in the event you do not have a DicomWeb endpoint.
You can host all of the relevant files on any web accessible server (Amazon S3, Azure Blob Storage, Local file server etc.)
This JSON is supplied via the '?url=' query parameter.
It should reference an endpoint that returns **application/json** formatted text.
If you do not have an API, you can simply return a text file containing the JSON from any web server.
You tell the OHIF viewer to use JSON by appending the `'?url='` query to the `/Viewer` route:
eg. `https://my-test-ohif-server/viewer?url=https://my-json-server/study-uid.json`
The returned JSON object must contain a single root object with a 'studies' array.
*Sample JSON format:*
```JSON
{
"studies": [
{
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78",
"StudyDescription": "BRAIN SELLA",
"StudyDate": "20010108",
"StudyTime": "120022",
"PatientName": "MISTER^MR",
"PatientId": "832040",
"series": [
{
"SeriesDescription": "SAG T-1",
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
"SeriesNumber": 2,
"SeriesDate": "20010108",
"SeriesTime": "120318",
"Modality": "MR",
"instances": [
{
"metadata": {
"Columns": 512,
"Rows": 512,
"InstanceNumber": 3,
"AcquisitionNumber": 0,
"PhotometricInterpretation": "MONOCHROME2",
"BitsAllocated": 16,
"BitsStored": 16,
"PixelRepresentation": 1,
"SamplesPerPixel": 1,
"PixelSpacing": [0.390625, 0.390625],
"HighBit": 15,
"ImageOrientationPatient": [0,1,0,0,0,-1],
"ImagePositionPatient": [11.600000,-92.500000, 98.099998],
"FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470",
"ImageType": ["ORIGINAL","PRIMARY","OTHER"],
"Modality": "MR",
"SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.124",
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78"
},
"url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.124.dcm"
}
]
}
]
}
]
}
```
More info on this JSON format can be found here [Issue #1500](https://github.com/OHIF/Viewers/issues/1500)
**Implementation Notes:**
1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route.
2. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomweb:` in order for the cornerstone image loader to retrieve it correctly.
eg. `https://image-server/my-image.dcm` ---> `dicomweb:https://image-server/my-image.dcm`
3. The JSON format above is compatible with >= v3.7.8 of the application. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet.
4. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500).
*CORS Issues (Cross-Origin Resource Sharing)*
If you host a JSON API or Images on a different domain from the the app itself, you will likely have CORS issues. This will also happen when testing from Localhost and reaching out to remote servers.
Even if the domain is the same, different ports, subdomains or protocols (https vs http) will also cause CORS errors.
You will to need add a configuration on each server hosting these assets to allow your App server origin.
For example:
Lets assume your application is hosted on `https://my-ohif-server.com`.
Your JSON API is hosted on `https://my-json-api.aws.com`
And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com`
When you first start your application, browsing to `https://my-ohif-server.com/viewer?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`.
Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this.
Next, you will likely get a similar CORS error, as the browser tries to go to `https://my-s3-bucket.aws.com`.
You will need to go to the S3 bucket configuration, and add a CORS setting to allow origin = `https://my-ohif-server.com`.
Essentially, whenever the application connects to a remote resource, you will need to add the applications url to the allowed CORS Origins on that resource. Adding an origin similar to https://localhost:3000 will also allow for local testing.
...
### Securing Your Data
@@ -166,7 +263,7 @@ window.config = {
```
You can find an example of this setup in our
[User Account Control Deployment Recipe](deployment/recipes/user-account-control.md).
[User Account Control Deployment Recipe](./recipes/user-account-control.md).
#### Choosing a Flow for the Viewer
@@ -183,12 +280,12 @@ many possible configurations, so please don't feel limited to these setups.
Please feel free to suggest or contribute your own recipes.
- Script Include
- [Embedding the Viewer](deployment/recipes/embedded-viewer.md)
- [Embedding the Viewer](./recipes/embedded-viewer.md)
- Stand-Alone
- [Build for Production](deployment/recipes/build-for-production.md)
- [Static](deployment/recipes/static-assets.md)
- [Nginx + Image Archive](deployment/recipes/nginx--image-archive.md)
- [User Account Control](deployment/recipes/user-account-control.md)
- [Build for Production](./recipes/build-for-production.md)
- [Static](./recipes/static-assets.md)
- [Nginx + Image Archive](./recipes/nginx--image-archive.md)
- [User Account Control](./recipes/user-account-control.md)
<!--
Links
@@ -50,7 +50,7 @@ window.config = {
```
To learn more about how you can configure the OHIF Viewer, check out our
[Configuration Guide](./index.md).
[Configuration Guide](../../configuring/index.md).
<ol start="3"><li>
Render the viewer in the web page's target <code>div</code>
@@ -126,12 +126,39 @@ globally defined styles/scripts.
> We're trying to embed the OHIF Viewer into an existing React App, but seeing
> react-dom and react conflicts. What can we do?
If you are installing OHIF viewer inside another react app, you may use `installViewer` as follows:
```
import { installViewer } from '@ohif/viewer'
const ohifViewerConfig = window.config // or set it here
const containerId = 'ohif'
const componentRenderedOrUpdatedCallback = function() {
console.log('OHIF Viewer rendered/updated');
};
componentDidMount() {
installViewer(
ohifViewerConfig,
containerId,
componentRenderedOrUpdatedCallback
);
}
render () {
...
//you can render in any element you wish
<AnyTag id={containerId}/>
}
```
`installViewer` is a convenience method that pulls in some dependencies that may
not be compatible with existing `react` apps. `@ohif/viewer` also exports `App`
which is a react component that takes the `configuration` outlined above as
props. You can use it as a reusable component, and to avoid `react` version
conflict issues.
<!--
LINKS
-->
@@ -49,9 +49,7 @@ We can solve this one of two ways:
1. Have our Image Archive located at the same domain as our Web App
2. Add appropriate `Access-Control-Allow-*` HTTP headers
This solution uses the first approach, but you can see an example of the second
in the `docker-compose` bundled with this project for local development:
[HERE](https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/Nginx-Orthanc/docker-compose.yml)
**This solution uses the first approach.**
You can read more about CORS in this Medium article: [Understanding
CORS][understanding-cors]
@@ -121,7 +119,10 @@ likely want to update:
#### OHIF Viewer
The OHIF Viewer's configuration is imported from a static `.js` file. The configuration we use is set to a specific file when we build the viewer, and determined by the env variable: `APP_CONFIG`. You can see where we set its value in the `dockerfile` for this solution:
The OHIF Viewer's configuration is imported from a static `.js` file. The
configuration we use is set to a specific file when we build the viewer, and
determined by the env variable: `APP_CONFIG`. You can see where we set its value
in the `dockerfile` for this solution:
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
@@ -138,11 +139,11 @@ Viewer's configuration, you can run:
All other files are found in: `/docker/OpenResty-Orthanc/`
| Service | Configuration | Docs |
| ----------------- | ---------------------------------------------- | ------------------------------------------- |
| OHIF Viewer | [dockerfile][dockerfile] / [config.js][config] | You're reading them now! |
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
| Service | Configuration | Docs |
| ----------------- | --------------------------------- | ------------------------------------------- |
| OHIF Viewer | [dockerfile][dockerfile] | You're reading them now! |
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
## Next Steps
@@ -218,10 +219,11 @@ following resources helpful:
- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/)
- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/)
For a different take on this setup, check out the repository one of our
community members put together:
For a different take on this setup, check out the repositories our community
members put together:
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
<!--
Links
@@ -234,8 +236,7 @@ community members put together:
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
<!-- SRC -->
[config]: #
[dockerfile]: #
[config-nginx]: #
[config-orthanc]: #
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
<!-- prettier-ignore-end -->
@@ -122,7 +122,10 @@ likely want to update:
#### OHIF Viewer
The OHIF Viewer's configuration is imported from a static `.js` file. The configuration we use is set to a specific file when we build the viewer, and determined by the env variable: `APP_CONFIG`. You can see where we set its value in the `dockerfile` for this solution:
The OHIF Viewer's configuration is imported from a static `.js` file. The
configuration we use is set to a specific file when we build the viewer, and
determined by the env variable: `APP_CONFIG`. You can see where we set its value
in the `dockerfile` for this solution:
`ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js`
@@ -146,8 +149,8 @@ All other files are found in: `/docker/OpenResty-Orthanc-Keycloak/`
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
| Keycloak | [`/ohif-keycloak-realm.json`][config-keycloak]\* | |
- \* These are the seed values for Keycloak. They can be manually updated at
`http://127.0.0.1/auth/admin`
\* These are the seed values for Keycloak. They can be manually updated at
`http://127.0.0.1/auth/admin`
#### Keycloak Themeing
@@ -261,12 +264,12 @@ for OAuth:
- [Diagrams of OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660)
- [KeyCloak: OpenID Connect Flows](https://www.keycloak.org/docs/latest/securing_apps/index.html#authorization-code)
- [Good description on SSO Protocols](https://www.keycloak.org/docs/2.5/server_admin/topics/sso-protocols/oidc.html)
For a different take on this setup, check out the repository one of our
community members put together:
For a different take on this setup, check out the repositories our community
members put together:
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
<!--
Links
@@ -278,8 +281,8 @@ community members put together:
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
<!-- SRC -->
[config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/src/config.js
[dockerfile]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/.recipes
[config-nginx]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/.recipes
[config-orthanc]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/.recipes
[config-keycloak]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/.recipes
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/dockerfile
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/nginx.conf
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/orthanc.json
[config-keycloak]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json
<!-- prettier-ignore-end -->
+2 -2
View File
@@ -114,8 +114,8 @@ the tips above will help ensure your changes are reviewed.
## Testing contribution pull requests
OHIF uses [netlify](netlify.com) so that pull requests are autogenerated and
available for testing.
OHIF uses [netlify](https://www.netlify.com/) so that pull requests are
autogenerated and available for testing.
For example, [this url][example-url] allows you to test [pull request 237, the
request that created this FAQ entry,][pr-237] using data pulled from Amazon S3.
+1 -1
View File
@@ -132,7 +132,7 @@ feature `X` or scenario `Y`? Open an issue and let's discuss.
[flow-org]: https://flow.org/
<!-- Talks -->
[assert-js-talks]: https://www.youtube.com/playlist?list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw
[kent-talk]: https://www.youtube.com/watch?list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf
[kent-talk]: https://www.youtube.com/watch?v=Fha2bVoC8SE
[gleb-talk]: https://www.youtube.com/watch?v=5FnalKRjpZk
[kent-blog]: https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests
<!-- Images -->
@@ -31,6 +31,7 @@
</td>
<td>Viewport, Toolbar</td>
</tr>
<!-- dicom-html -->
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-html">DICOM HTML</a>
@@ -40,6 +41,7 @@
</td>
<td>Viewport, SopClassHandler</td>
</tr>
<!-- dicom-pdf -->
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a>
@@ -49,6 +51,7 @@
</td>
<td>Viewport, SopClassHandler</td>
</tr>
<!-- dicom-microscopy -->
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-microscopy">DICOM Microscopy</a>
@@ -58,5 +61,15 @@
</td>
<td>Viewport, SopClassHandler</td>
</tr>
<!-- dicom-segmentation -->
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-segmentation">DICOM Segmentation</a>
</td>
<td>
Renders segmentation images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7">specific SopClassUID</a>.
</td>
<td>Panel, Toolbar</td>
</tr>
</tbody>
</table>
+11 -7
View File
@@ -74,7 +74,8 @@ export default {
### Registering an Extension
There are two different ways to register and configure extensions: At
[runtime](#runtime-extensions) and at [build time](#bundled-extensions).
[runtime](#registering-at-runtime) and at
[build time](#registering-at-build-time).
You can leverage one or both strategies. Which one(s) you choose depend on your
application's requirements. Each [module](#modules) defined by the extension
@@ -82,9 +83,9 @@ becomes available to the core application via the `ExtensionManager`.
#### Registering at Runtime
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
that file includes an `Extensions` key that supports an array of extensions to
register.
The `@ohif/viewer` uses a [configuration file](../viewer/configuration.md) at
startup. The schema for that file includes an `Extensions` key that supports an
array of extensions to register.
```js
// prettier-ignore
@@ -193,7 +194,8 @@ today, create a GitHub issue!
The `ExtensionManager` is a class made available to us via the `@ohif/core`
project (platform/core). Our application instantiates a single instance of it,
and provides a `ServicesManager` and `CommandsManager`.
and provides a `ServicesManager` and `CommandsManager` along with the
application's configuration through the appConfig key (optional).
```js
const commandsManager = new CommandsManager();
@@ -201,6 +203,7 @@ const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
appConfig,
});
```
@@ -210,8 +213,9 @@ The `ExtensionManager` only has a few public members:
- `registerExtensions` - Registers an array of extensions
- `modules` - An object containing registered extensions by `MODULE_TYPE`
During registration, lifecycle hooks and modules have access to
`ExtensionManager`'s `ServicesManager` and `CommandsManager`.
During registration, lifecycle hooks and modules have access to the extension's
config, the application's config and `ExtensionManager`'s `ServicesManager` and
`CommandsManager` instances.
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
appropriate places in our application.
+4
View File
@@ -40,6 +40,10 @@ export default {
from: 'right',
// The target component to toggle open/close
target: 'target-component-id',
// UI Hint; If the target panel is in a "disabled" state
isDisabled: studies => {
return false;
},
// Overrides `defaultContext`, if specified
context: ['ACTIVE_VIEWPORT:MAGIC'],
},
@@ -1,11 +1,11 @@
# Module: SOP Class Handler
An extension can register a SOP Class Handler Module by defining a
`getSopClassHandlerModule` method. The SOP Class Handler is a bit different from
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
own components. It instead defines:
An extension can register a [SOP Class][sop-class-link] Handler Module by
defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link]
Handler is a bit different from the other modules, as it doesn't provide a `1:1`
schema for UI or provide it's own components. It instead defines:
- `sopClassUids`: an array of string SOP Class UIDs that the
- `sopClassUIDs`: an array of string SOP Class UIDs that the
`getDisplaySetFromSeries` method should be applied to.
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
display set
@@ -15,13 +15,13 @@ A `displaySet` has the following shape:
```js
return {
plugin: 'html',
modality: 'SR',
displaySetInstanceUid: 0,
Modality: 'SR',
displaySetInstanceUID: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
authorizationHeaders,
};
```
@@ -50,7 +50,7 @@ export default {
getSopClassHandlerModule({ servicesManager, commandsManager }) {
return {
id: 'OHIFDicomHtmlSopClassHandler',
sopClassUids: Object.values(SOP_CLASS_UIDS),
sopClassUIDs: Object.values(SOP_CLASS_UIDS),
/**
* @param {object} series -
@@ -63,12 +63,12 @@ getSopClassHandlerModule({ servicesManager, commandsManager }) {
return {
plugin: 'html',
displaySetInstanceUid: 0,
displaySetInstanceUID: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
authorizationHeaders,
};
},
@@ -76,6 +76,13 @@ getSopClassHandlerModule({ servicesManager, commandsManager }) {
};
```
### More examples :
- [Dicom-HTML SOP][dicom-html-sop]
- [Dicom-PDF SOP][dicom-pdf-sop]
- [Dicom-Microscopy SOP][dicom-micro-sop]
- [Dicom-Segmentation SOP][dicom-seg-sop]
## `@ohif/viewer` usage
We use the `sopClassHandlerModule`s in three different places:
@@ -88,3 +95,11 @@ Each time, it is used to map study and series data to `displaySets`. It does
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
manager has the method `createDisplaySets` that takes an array of
`sopClassHandlerModules`.
<!-- prettier-ignore-start -->
[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html
[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12
[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6
[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7
[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7
<!-- prettier-ignore-end -->
+1 -1
View File
@@ -13,7 +13,7 @@ like user preferences and previous query paramaters. Because of this, the Viewer
has been built to be highly configurable to work with almost any web accessible
data source.
![scope-of-project diagram](./assets/img/scope-of-project.png)
![scope-of-project diagram](./../assets/img/scope-of-project.png)
To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files.
These can be delivered to your end users however you would like:
+2 -2
View File
@@ -75,7 +75,7 @@ items to our ["Active Development" GitHub Project Board][gh-board]. Items on
this project board are either in active development by Core Team members, or
queued up for development as in-progress items are completed.
> :pencil: Want to contribute but not sure where to start? Check out [Up for
> 🖋 Want to contribute but not sure where to start? Check out [Up for
> grabs][label-grabs] issues and our [Contributing
> documentation][contributing-docs]
@@ -156,5 +156,5 @@ addressed and a Stable version is released.
[ohif-prod]: https://viewer.ohif.org
[gh-board]: https://github.com/OHIF/Viewers/projects/4
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
[contributing-docs]: https://docs.ohif.org/contributing/
[contributing-docs]: ./development/contributing.md
<!-- prettier-ignore-end -->
+1 -1
View File
@@ -62,7 +62,7 @@ able to fail or be removed. Related patterns that may reduce coupling include:
-->
<!-- prettier-ignore-start -->
[commands]: #/
[commands]: ../extensions/modules/commands.md
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
+2 -2
View File
@@ -2,8 +2,8 @@
- [Overview](#overview)
- [Example](#example)
- [Tips & Tricks](#/)
- [Maintained Services](#/)
- [Tips & Tricks](#tips--tricks)
- [Maintained Services](#maintained-services)
## Overview
+117 -1
View File
@@ -1,3 +1,119 @@
# Viewer: Configuration
...
We maintain a number of common viewer application configurations at
[`<root>/platform/viewer/public/configs`][config-dir]. How these values are
passed to the viewer depend on how it's deployed, but the two most common paths
are:
- `index.html` looks for `https://your-website.com/app-config.js` OR
- `index.html` passes the values to `OHIF.installViewer()`
```js
window.config = {
routerBasename: '/',
/**
* "White Labeling" is used to change the branding, look, and feel of the OHIF
* Viewer. These settings, and the color variables that are used by our components,
* are the easiest way to rebrand the application.
*
* More extensive changes are made possible through swapping out the UI library,
* Viewer project, or extensions.
*/
whiteLabeling: {
/* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
createLogoComponentFn: function(React) {
return React.createElement('a', {
target: '_self',
rel: 'noopener noreferrer',
className: 'header-brand',
href: '/',
style: {
display: 'block',
textIndent: '-9999px',
background: 'url(/svg-file-hosted-at-domain-root.svg)',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
width: '200px',
},
});
},
},
/**
* Internally, the OHIF Viewer fetches data primarily with the
* `cornerstoneWADOImageLoader` and the `DICOMWebClient`. If either of these
* receive a non-200 response, this method allows you to handle that error.
*
* Common use cases include:
* - Showing a notification with the UINotificationService
* - Redirecting the user
* - Refreshing an auth token
*
* @param {Object} error - JS new Error()
* @param {XMLHttpRequest} error.request - The XHR request that's onreadystate change triggered this callback
* @param {string} error.response - The XHR's response property
* @param {number} error.status - The XHR's status property
*/
httpErrorHandler: error => {
const { request: xhr, response, status } = err;
const { responseType, statusText } = xhr;
// In local files, status is 0 upon success in Firefox
if (xhr.readyState === XMLHttpRequest.DONE) {
console.log(statusText, response, responseType);
} else {
console.warn('Likely CORS error');
}
},
extensions: [],
showStudyList: true,
filterQueryParam: false,
servers: {
dicomWeb: [
{
name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
},
],
},
// Supported Keys: https://craig.is/killing/mice
hotkeys: [
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportVertical',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportHorizontal',
label: 'Flip Vertically',
keys: ['v'],
},
],
/* Configuration passed to the bundled cornerstone extension
*
* The cornerstone extension is currently tightly coupled to the platform.
* Until we're able to decouple it, this key will serve as a workaround to
* pass it configuration.
*/
cornerstoneExtensionConfig: {
/* Whether to show/hide annotation "handles" */
hideHandles: true,
},
};
```
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[config-dir]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
<!-- prettier-ignore-end -->
@@ -0,0 +1,23 @@
# Environment Variables
There are a number of environment variables we use at build time to influence the output application's behavior.
```bash
# Application
NODE_ENV=< production | development >
DEBUG=< true | false >
APP_CONFIG=< relative path to application configuration file >
PUBLIC_URL=<>
VERSION_NUMBER=<Set by CircleCI>
BUILD_NUM=<Set by CircleCI>
# i18n
USE_LOCIZE=<false>
LOCIZE_PROJECTID=<ProjectID to pull translations for>
LOCIZE_API_KEY=<To enable Locize live editing of translations>
```
## Setting Environment Variables
- `npx cross-env`
- `.env` files
- env variables on build machine, or for terminal session
+4 -4
View File
@@ -135,7 +135,7 @@ console.log(T('$t(Common:Play) my translated text'));
# Main Concepts While Translating
## - Namespaces
## Namespaces
Namespaces are being used to organize translations in smaller portions, combined
semantically or by use. Each `.json` file inside `@ohif/i18n` npm package
@@ -157,7 +157,7 @@ NameSpace, like this following example getting data from `Common` NameSpace:
$t(Common:Reset)
```
## - Extending Languages in @ohif/i18n
## Extending Languages in @ohif/i18n
Sometimes, even using the same language, some nouns or jargons can change
according to the country, states or even from Hospital to Hospital.
@@ -212,7 +212,7 @@ object like this:
Please check the `index.js` files inside locales folder for an example of this
exporting structure.
### - Extending languages dynamically
### Extending languages dynamically
You have access to the i18next instance, so you can use the
[addResourceBundle](https://www.i18next.com/how-to/add-or-load-translations#add-after-init)
@@ -314,4 +314,4 @@ REACT_APP_I18N_DEBUG=true yarn run dev
### Contributing with new languages
Contributions of any kind are welcome! Please check the
[instructions](https://docs.ohif.org/contributing.html).
[instructions](../development/contributing.md).
+4 -4
View File
@@ -66,11 +66,11 @@ open to pull requests and discussion issues.
Current white-labeling options are limited. We expose the ability to replace the
"Logo" section of the application with a custom "Logo" component. You can do
this by adding a `whiteLabelling` key to your
this by adding a `whiteLabeling` key to your
[configuration file](./configuration.md).
```js
function RadicalImagingLogo() {
function RadicalImagingLogo(React) {
return React.createElement(
'a',
{
@@ -83,8 +83,8 @@ function RadicalImagingLogo() {
);
}
props.whiteLabelling = {
logoComponent: RadicalImagingLogo(),
props.whiteLabeling = {
createLogoComponentFn: RadicalImagingLogo,
};
```
+17 -12
View File
@@ -20,10 +20,10 @@ abbrev@~1.0.9:
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU=
agent-base@4, agent-base@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
agent-base@4, agent-base@^4.1.0, agent-base@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
dependencies:
es6-promisify "^5.0.0"
@@ -773,9 +773,9 @@ err-code@^1.0.0:
prr "~1.0.1"
es6-promise@^4.0.3:
version "4.2.6"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f"
integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
es6-promisify@^5.0.0:
version "5.0.0"
@@ -1289,11 +1289,11 @@ http-signature@~1.2.0:
sshpk "^1.7.0"
https-proxy-agent@^2.1.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
version "2.2.4"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==
dependencies:
agent-base "^4.1.0"
agent-base "^4.3.0"
debug "^3.1.0"
humanize-ms@^1.2.1:
@@ -1807,11 +1807,16 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@^2.0.0, ms@^2.1.1:
ms@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
ms@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
mute-stream@~0.0.4:
version "0.0.8"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
+2 -1
View File
@@ -1,6 +1,7 @@
# Lesion Tracker - Audit Trail
1. To view Audit Trails, select **View Audit Log** on the Configuration menu.
2. To filter audit logs for a column or multiple columns, type or select in the related column or columns.
2. To filter audit logs for a column or multiple columns, type or select in the
related column or columns.
![Audit Trails](../assets/img/LesionTracker/LT_Audit_Trails.png)
@@ -2,50 +2,74 @@
![Study List](../assets/img/LesionTracker/LT_StudyList.png)
1. Study List allows to sort each column. To sort studies, click the desired column header.
2. Study List is filterable for each column and you can filter by more than one column by typing in the column field. To filter a column, type the text in the desired column header field and press Enter.
3. Pagination is provided for the Study List at the bottom of the page. You can change the number of rows per page or the current page.
1. Study List allows to sort each column. To sort studies, click the desired
column header.
2. Study List is filterable for each column and you can filter by more than one
column by typing in the column field. To filter a column, type the text in
the desired column header field and press Enter.
3. Pagination is provided for the Study List at the bottom of the page. You can
change the number of rows per page or the current page.
## Study List Context Menu
When you right-click on the study row, Study Context Menu will pop up. Study Context Menu includes operations at the study level.
When you right-click on the study row, Study Context Menu will pop up. Study
Context Menu includes operations at the study level.
There are a couple of ways to view a study:
* Simply double-click on the study row.
* Select **View** option on the Study Context Menu, then you will be redirected to Viewer page.
- Simply double-click on the study row.
- Select **View** option on the Study Context Menu, then you will be redirected
to Viewer page.
![View Study](../assets/img/LesionTracker/LT_View_Study.png)
### Timepoint Association
1. To link a study with a timepoint, right-click on the study row and select **Associate** option on the Study Context Menu. You will see Study Association dialog.
2. To link multiple studies at the same time, hold Shift key and select studies you would like to associate, then right-click and select Associate on the Study Context Menu
1. To link a study with a timepoint, right-click on the study row and select
**Associate** option on the Study Context Menu. You will see Study
Association dialog.
2. To link multiple studies at the same time, hold Shift key and select studies
you would like to associate, then right-click and select Associate on the
Study Context Menu
![Select Associate](../assets/img/LesionTracker/LT_Select_Associate.png)
The Study Association dialog allows you to select and unselect Timepoint Type for the selected study or studies. Also, it will include all studies within 14 days of your selected studies, in case you forgot to select a study. After you identify timepoints for the selected study or studies, click **Save** to save the association.
The Study Association dialog allows you to select and unselect Timepoint Type
for the selected study or studies. Also, it will include all studies within 14
days of your selected studies, in case you forgot to select a study. After you
identify timepoints for the selected study or studies, click **Save** to save
the association.
![Association Dialog](../assets/img/LesionTracker/LT_Association_Dialog.png)
### Remove Timepoint Association
To unlink a timepoint association with a study, right-click on the study row and select **Remove Association** option on the Study Context Menu. Remove Association will be disabled if the study is already dissociated.
To unlink a timepoint association with a study, right-click on the study row and
select **Remove Association** option on the Study Context Menu. Remove
Association will be disabled if the study is already dissociated.
![Remove Associate](../assets/img/LesionTracker/LT_Remove_Associate.png)
### View Series Details
To view details of related series, right-click on the study row and select **View Series Details** option on the Study Context Menu, then Series Details dialog will pop up. Series Details dialog gives a summary of the selected study.
To view details of related series, right-click on the study row and select
**View Series Details** option on the Study Context Menu, then Series Details
dialog will pop up. Series Details dialog gives a summary of the selected study.
![View Series Details](../assets/img/LesionTracker/LT_View_Series_Details.png)
### Anonymize (Under Development)
...
### Send (Under Development)
...
### Export (Under Development)
...
### Delete (Under Development)
...
+9 -6
View File
@@ -14,13 +14,13 @@ export default {
preRegistration({
servicesManager = {},
commandsManager = {},
appConfig = {},
configuration = {},
}) {},
/**
* MODULE GETTERS
*/
getViewportModule() {
return '... react component ...';
},
@@ -63,7 +63,7 @@ const commandsModule = {
*/
const sopClassHandlerModule = {
id: 'OHIFDicomHtmlSopClassHandler',
sopClassUids: Object.values({
sopClassUIDs: Object.values({
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
@@ -77,12 +77,12 @@ const sopClassHandlerModule = {
return {
plugin: 'html',
displaySetInstanceUid: 0, //utils.guid(),
displaySetInstanceUID: 0, //utils.guid(),
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
authorizationHeaders,
};
},
@@ -97,6 +97,9 @@ const panelModule = {
icon: 'th-list',
label: 'Segments',
target: 'segment-panel',
isDisabled: studies => {
return false;
},
},
],
components: [
+164
View File
@@ -3,6 +3,170 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.1...@ohif/extension-cornerstone@2.7.0) (2020-04-24)
### Features
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.0...@ohif/extension-cornerstone@2.6.1) (2020-04-23)
### Bug Fixes
* 🐛 Multiframe fix ([#1661](https://github.com/OHIF/Viewers/issues/1661)) ([7120561](https://github.com/OHIF/Viewers/commit/71205618ecb8b592247c5acb32284bfe7e18fce5))
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.2...@ohif/extension-cornerstone@2.6.0) (2020-04-23)
### Features
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.1...@ohif/extension-cornerstone@2.5.2) (2020-04-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.0...@ohif/extension-cornerstone@2.5.1) (2020-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.3...@ohif/extension-cornerstone@2.5.0) (2020-03-13)
### Features
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
## [2.4.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.2...@ohif/extension-cornerstone@2.4.3) (2020-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.4.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.1...@ohif/extension-cornerstone@2.4.2) (2020-03-09)
### Bug Fixes
* Remove Eraser and ROI Window ([6c950a9](https://github.com/OHIF/Viewers/commit/6c950a9669f7fbf3c46e48679fa26ee514824156))
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.0...@ohif/extension-cornerstone@2.4.1) (2020-03-03)
### Bug Fixes
* bump react-cornerstone-viewport version to address critical issue ([#1473](https://github.com/OHIF/Viewers/issues/1473)) ([ee80e02](https://github.com/OHIF/Viewers/commit/ee80e026610442e94caf5e4e3e4d193220cd0ece))
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.1...@ohif/extension-cornerstone@2.4.0) (2020-02-20)
### Features
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.0...@ohif/extension-cornerstone@2.3.1) (2020-02-14)
### Bug Fixes
* Creating 2 commands to activate zoom tool and also to move between displaySets ([#1446](https://github.com/OHIF/Viewers/issues/1446)) ([06a4af0](https://github.com/OHIF/Viewers/commit/06a4af06faaecf6fa06ccd90cdfa879ee8d53053))
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.2...@ohif/extension-cornerstone@2.3.0) (2020-02-10)
### Features
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.1...@ohif/extension-cornerstone@2.2.2) (2020-01-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.0...@ohif/extension-cornerstone@2.2.1) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.1...@ohif/extension-cornerstone@2.2.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.0...@ohif/extension-cornerstone@2.1.1) (2019-12-16)
### Bug Fixes
* 🐛 add WwwcRegionTool to cornerstone tools initialization ([#1302](https://github.com/OHIF/Viewers/issues/1302)) ([d5bf728](https://github.com/OHIF/Viewers/commit/d5bf72851a32dff9fd3fc09332ea5250bc7e6114))
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
+14
View File
@@ -85,6 +85,20 @@ Tools can be configured through extension configuration using the tools key:
...
```
## Annotate Tools Configuration
*We currently support one property for annotation tools.*
### Hide handles
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
```js
...
cornerstoneExtensionConfig: {
hideHandles: true,
},
...
## Resources
### Repositories
+1
View File
@@ -0,0 +1 @@
module.exports = require("../../babel.config.js");
+13
View File
@@ -0,0 +1,13 @@
const base = require('../../jest.config.base.js');
const pkg = require('./package');
module.exports = {
...base,
name: pkg.name,
displayName: pkg.name,
// rootDir: "../.."
// testMatch: [
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
// "<rootDir>/platform/viewer/**/*.test.js"
// ]
};
+8 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.1.0",
"version": "2.7.0",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -25,16 +25,18 @@
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"prepublishOnly": "yarn run build",
"start": "yarn run dev"
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.8.2",
"cornerstone-tools": "4.12.5",
"cornerstone-wado-image-loader": "^3.1.0",
"dcmjs": "^0.12.2",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -49,7 +51,6 @@
"classnames": "^2.2.6",
"lodash.merge": "^4.6.2",
"lodash.throttle": "^4.1.1",
"query-string": "^6.8.3",
"react-cornerstone-viewport": "2.x.x"
"react-cornerstone-viewport": "^2.3.6"
}
}
@@ -4,28 +4,9 @@ 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;
// Metadata configuration
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
cornerstone.metaData.addProvider(
metadataProvider.provider.bind(metadataProvider)
);
StackManager.setMetadataProvider(metadataProvider);
const SOP_CLASSES = {
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
};
const specialCaseHandlers = {};
specialCaseHandlers[
SOP_CLASSES.SEGMENTATION_STORAGE
] = handleSegmentationStorage;
class OHIFCornerstoneViewport extends Component {
state = {
viewportData: null,
@@ -58,34 +39,34 @@ class OHIFCornerstoneViewport extends Component {
* Obtain the CornerstoneTools Stack for the specified display set.
*
* @param {Object[]} studies
* @param {String} studyInstanceUid
* @param {String} displaySetInstanceUid
* @param {String} [sopInstanceUid]
* @param {String} StudyInstanceUID
* @param {String} displaySetInstanceUID
* @param {String} [SOPInstanceUID]
* @param {Number} [frameIndex=1]
* @return {Object} CornerstoneTools Stack
*/
static getCornerstoneStack(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
SOPInstanceUID,
frameIndex = 0
) {
if (!studies || !studies.length) {
throw new Error('Studies not provided.');
}
if (!studyInstanceUid) {
if (!StudyInstanceUID) {
throw new Error('StudyInstanceUID not provided.');
}
if (!displaySetInstanceUid) {
if (!displaySetInstanceUID) {
throw new Error('StudyInstanceUID not provided.');
}
// Create shortcut to displaySet
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
study => study.StudyInstanceUID === StudyInstanceUID
);
if (!study) {
@@ -93,7 +74,7 @@ class OHIFCornerstoneViewport extends Component {
}
const displaySet = study.displaySets.find(set => {
return set.displaySetInstanceUid === displaySetInstanceUid;
return set.displaySetInstanceUID === displaySetInstanceUID;
});
if (!displaySet) {
@@ -107,17 +88,14 @@ class OHIFCornerstoneViewport extends Component {
const stack = Object.assign({}, storedStack);
stack.currentImageIdIndex = frameIndex;
if (sopInstanceUid) {
if (SOPInstanceUID) {
const index = stack.imageIds.findIndex(imageId => {
const sopCommonModule = cornerstone.metaData.get(
'sopCommonModule',
const imageIdSOPInstanceUID = cornerstone.metaData.get(
'SOPInstanceUID',
imageId
);
if (!sopCommonModule) {
return;
}
return sopCommonModule.sopInstanceUID === sopInstanceUid;
return imageIdSOPInstanceUID === SOPInstanceUID;
});
if (index > -1) {
@@ -134,44 +112,26 @@ class OHIFCornerstoneViewport extends Component {
getViewportData = async (
studies,
studyInstanceUid,
displaySetInstanceUid,
sopClassUid,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
SOPInstanceUID,
frameIndex
) => {
let viewportData;
switch (sopClassUid) {
case SOP_CLASSES.SEGMENTATION_STORAGE:
const specialCaseHandler =
specialCaseHandlers[SOP_CLASSES.SEGMENTATION_STORAGE];
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
studies,
StudyInstanceUID,
displaySetInstanceUID,
SOPInstanceUID,
frameIndex
);
viewportData = await specialCaseHandler(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopInstanceUid,
frameIndex
);
break;
default:
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopInstanceUid,
frameIndex
);
viewportData = {
studyInstanceUid,
displaySetInstanceUid,
stack,
};
break;
}
viewportData = {
StudyInstanceUID,
displaySetInstanceUID,
stack,
};
return viewportData;
};
@@ -179,31 +139,28 @@ class OHIFCornerstoneViewport extends Component {
setStateFromProps() {
const { studies, displaySet } = this.props.viewportData;
const {
studyInstanceUid,
displaySetInstanceUid,
sopClassUids,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
sopClassUIDs,
SOPInstanceUID,
frameIndex,
} = displaySet;
if (!studyInstanceUid || !displaySetInstanceUid) {
if (!StudyInstanceUID || !displaySetInstanceUID) {
return;
}
if (sopClassUids && sopClassUids.length > 1) {
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.'
);
}
const sopClassUid = sopClassUids && sopClassUids[0];
this.getViewportData(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopClassUid,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
SOPInstanceUID,
frameIndex
).then(viewportData => {
this.setState({
@@ -217,13 +174,13 @@ class OHIFCornerstoneViewport extends Component {
}
componentDidUpdate(prevProps) {
const { studies, displaySet } = this.props.viewportData;
const { displaySet } = this.props.viewportData;
const prevDisplaySet = prevProps.viewportData.displaySet;
if (
displaySet.displaySetInstanceUid !==
prevDisplaySet.displaySetInstanceUid ||
displaySet.sopInstanceUid !== prevDisplaySet.sopInstanceUid ||
displaySet.displaySetInstanceUID !==
prevDisplaySet.displaySetInstanceUID ||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
displaySet.frameIndex !== prevDisplaySet.frameIndex
) {
this.setStateFromProps();
@@ -1,37 +0,0 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
+62 -16
View File
@@ -2,10 +2,14 @@ import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll');
const { studyMetadataManager } = OHIF.utils;
const { setViewportSpecificData } = OHIF.redux.actions;
const commandsModule = ({ servicesManager }) => {
const actions = {
rotateViewport: ({ viewports, rotation }) => {
@@ -73,10 +77,6 @@ const commandsModule = ({ servicesManager }) => {
}
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
},
updateViewportDisplaySet: ({ direction }) => {
// TODO
console.warn('updateDisplaySet: ', direction);
},
clearAnnotations: ({ viewports }) => {
const element = getEnabledElement(viewports.activeViewportIndex);
if (!element) {
@@ -242,9 +242,51 @@ const commandsModule = ({ servicesManager }) => {
cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element);
},
setCornerstoneLayout: () => {
setCornerstoneLayout();
},
setWindowLevel: ({ viewports, window, level }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.voi = {
windowWidth: Number(window),
windowCenter: Number(level),
};
cornerstone.setViewport(enabledElement, viewport);
}
},
jumpToImage: ({
StudyInstanceUID,
SOPInstanceUID,
frameIndex,
activeViewportIndex
}) => {
const study = studyMetadataManager.get(StudyInstanceUID);
const displaySet = study.findDisplaySet(ds => {
return ds.images && ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
});
displaySet.SOPInstanceUID = SOPInstanceUID;
displaySet.frameIndex = frameIndex;
window.store.dispatch(setViewportSpecificData(activeViewportIndex, displaySet));
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
}
};
const definitions = {
jumpToImage: {
commandFn: actions.jumpToImage,
storeContexts: [],
options: {},
},
getNearbyToolData: {
commandFn: actions.getNearbyToolData,
storeContexts: [],
@@ -330,24 +372,28 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: ['viewports'],
options: {},
},
// TODO: First/Last image
// Next/Previous series/DisplaySet
nextViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: 1 },
},
previousViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: -1 },
},
// TOOLS
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
options: {},
},
setZoomTool: {
commandFn: actions.setToolActive,
storeContexts: [],
options: { toolName: 'Zoom' },
},
setCornerstoneLayout: {
commandFn: actions.setCornerstoneLayout,
storeContexts: [],
options: {},
context: 'VIEWER',
},
setWindowLevel: {
commandFn: actions.setWindowLevel,
storeContexts: ['viewports'],
options: {},
},
};
return {
@@ -1,128 +0,0 @@
import * as dcmjs from 'dcmjs';
import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
const { StackManager, DicomLoaderService } = OHIF.utils;
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
);
const displaySet = study.displaySets.find(set => {
return set.displaySetInstanceUid === displaySetInstanceUid;
});
return displaySet;
}
function getDisplaySetsBySeries(studies, studyInstanceUid, seriesInstanceUid) {
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
);
return study.displaySets.filter(set => {
return set.seriesInstanceUid === seriesInstanceUid;
});
}
function parseSeg(arrayBuffer, imageIds) {
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
imageIds,
arrayBuffer,
cornerstone.metaData
);
}
function addSegMetadataToCornerstoneToolState(
segMetadata,
toolState,
displaySetInstanceUid
) {
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
toolState
);
const brushModule = cornerstoneTools.store.modules.brush;
for (let i = 0; i < segMetadata.length; i++) {
brushModule.setters.metadata(displaySetInstanceUid, i, segMetadata[i]);
}
}
async function handleSegmentationStorage(
studies,
studyInstanceUid,
displaySetInstanceUid
) {
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
);
const displaySet = getDisplaySet(
studies,
studyInstanceUid,
displaySetInstanceUid
);
const arrayBuffer = await DicomLoaderService.findDicomDataPromise(
displaySet,
studies
);
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
const seriesInstanceUid = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
const displaySets = getDisplaySetsBySeries(
studies,
studyInstanceUid,
seriesInstanceUid
);
if (displaySets.length > 1) {
console.warn(
'More than one display set with the same seriesInstanceUid. This is not supported yet...'
);
}
const referenceDisplaySet = displaySets[0];
const imageIds = referenceDisplaySet.images.map(image => image.getImageId());
const results = parseSeg(arrayBuffer, imageIds);
if (!results) {
throw new Error('Fractional segmentations are not supported');
}
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
const { setters } = cornerstoneTools.getModule('segmentation');
setters.labelmap3DByFirstImageId(
imageIds[0],
labelmapBuffer,
0, // TODO -> Can define a color LUT based on colors in the SEG later.
segMetadata,
imageIds.length,
segmentsOnFrame
);
const cachedStack = StackManager.findOrCreateStack(
study,
referenceDisplaySet
);
const stack = Object.assign({}, cachedStack);
stack.currentImageIdIndex = 0;
return {
studyInstanceUid,
displaySetInstanceUid,
stack,
};
}
export default handleSegmentationStorage;
+12 -6
View File
@@ -1,14 +1,20 @@
import React from 'react';
import init from './init.js';
import asyncComponent from './asyncComponent.js';
import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const OHIFCornerstoneViewport = asyncComponent(() =>
import(
/* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'
)
);
const Component = React.lazy(() => {
return import('./OHIFCornerstoneViewport');
});
const OHIFCornerstoneViewport = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
/**
*
+190 -56
View File
@@ -1,29 +1,10 @@
import OHIF from '@ohif/core';
import { SimpleDialog } from '@ohif/ui';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
import queryString from 'query-string';
import { SimpleDialog } from '@ohif/ui';
import merge from 'lodash.merge';
function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) {
return;
}
// If you call for an WADO-URI imageId and get no
// metadata, try reformatting to WADO-RS imageId
const qs = queryString.parse(imageId);
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
qs.seriesUID
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
return cornerstone.metaData.get(type, wadoRsImageId);
}
// Add this fallback provider with a low priority so it is handled last
cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
import initCornerstoneTools from './initCornerstoneTools.js';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
/**
*
@@ -32,9 +13,9 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ servicesManager, configuration }) {
const callInputDialog = (data, event, callback) => {
const { UIDialogService } = servicesManager.services;
const { UIDialogService, MeasurementService } = servicesManager.services;
const callInputDialog = (data, event, callback) => {
if (UIDialogService) {
let dialogId = UIDialogService.create({
centralize: true,
@@ -57,16 +38,13 @@ export default function init({ servicesManager, configuration }) {
};
const { csToolsConfig } = configuration;
const { StackManager } = OHIF.utils;
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
const metadataProvider = OHIF.cornerstone.metadataProvider;
// ~~ Set our MetadataProvider
cornerstone.metaData.addProvider(
metadataProvider.provider.bind(metadataProvider)
metadataProvider.get.bind(metadataProvider),
9999
);
StackManager.setMetadataProvider(metadataProvider);
// ~~
const defaultCsToolsConfig = csToolsConfig || {
globalToolSyncEnabled: true,
@@ -76,30 +54,37 @@ export default function init({ servicesManager, configuration }) {
initCornerstoneTools(defaultCsToolsConfig);
// ~~ Toooools 🙌
const tools = [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
// Touch
csTools.PanMultiTouchTool,
csTools.ZoomTouchPinchTool,
// Annotations
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
// Segmentation
csTools.BrushTool,
];
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
other: [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
csTools.OverlayTool,
],
};
let tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
/* Measurement Service */
_connectToolsToMeasurementService(MeasurementService);
/* Add extension tools configuration here. */
const internalToolsConfig = {
@@ -113,13 +98,48 @@ export default function init({ servicesManager, configuration }) {
},
};
/* Abstract tools configuration using extension configuration. */
const parseToolProps = (props, tool) => {
const { annotations } = toolsGroupedByType;
// An alternative approach would be to remove the `drawHandlesOnHover` config
// from the supported configuration properties in `cornerstone-tools`
const toolsWithHideableHandles = annotations.filter(
tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name)
);
let parsedProps = { ...props };
/**
* drawHandles - Never/Always show handles
* drawHandlesOnHover - Only show handles on handle hover (pointNearHandle)
*
* Does not apply to tools where handles aren't placed in predictable
* locations.
*/
if (
configuration.hideHandles !== false &&
toolsWithHideableHandles.includes(tool)
) {
if (props.configuration) {
parsedProps.configuration.drawHandlesOnHover = true;
} else {
parsedProps.configuration = { drawHandlesOnHover: true };
}
}
return parsedProps;
};
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const externalToolsConfig = configuration.tools || {};
const externalToolProps = externalToolsConfig[toolName] || {};
const internalToolProps = internalToolsConfig[toolName] || {};
const props = merge(internalToolProps, externalToolProps);
const props = merge(
internalToolProps,
parseToolProps(externalToolProps, tool)
);
csTools.addTool(tool, props);
});
@@ -132,11 +152,125 @@ export default function init({ servicesManager, configuration }) {
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
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', {});
csTools.setToolEnabled('Overlay', {});
}
const _initMeasurementService = measurementService => {
/* Initialization */
const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory(
measurementService
);
const csToolsVer4MeasurementSource = measurementService.createSource(
'CornerstoneTools',
'4'
);
/* Matching Criterias */
const matchingCriteria = {
valueType: measurementService.VALUE_TYPES.POLYLINE,
points: 2,
};
/* Mappings */
measurementService.addMapping(
csToolsVer4MeasurementSource,
'Length',
matchingCriteria,
toAnnotation,
toMeasurement
);
return csToolsVer4MeasurementSource;
};
const _connectToolsToMeasurementService = measurementService => {
const csToolsVer4MeasurementSource = _initMeasurementService(
measurementService
);
const {
id: sourceId,
addOrUpdate,
getAnnotation,
} = csToolsVer4MeasurementSource;
/* Measurement Service Events */
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
event => {
const {
MEASUREMENT_ADDED,
MEASUREMENT_UPDATED,
} = measurementService.EVENTS;
measurementService.subscribe(
MEASUREMENT_ADDED,
({ source, measurement }) => {
if (![sourceId].includes(source.id)) {
const annotation = getAnnotation('Length', measurement.id);
console.log(
'Measurement Service [Cornerstone]: Measurement added',
measurement
);
console.log('Mapped annotation:', annotation);
}
}
);
measurementService.subscribe(
MEASUREMENT_UPDATED,
({ source, measurement }) => {
if (![sourceId].includes(source.id)) {
const annotation = getAnnotation('Length', measurement.id);
console.log(
'Measurement Service [Cornerstone]: Measurement updated',
measurement
);
console.log('Mapped annotation:', annotation);
}
}
);
const addOrUpdateMeasurement = csToolsAnnotation => {
try {
const { toolName, toolType, measurementData } = csToolsAnnotation;
const csTool = toolName || measurementData.toolType || toolType;
csToolsAnnotation.id = measurementData._measurementServiceId;
const measurementServiceId = addOrUpdate(csTool, csToolsAnnotation);
if (!measurementData._measurementServiceId) {
addMeasurementServiceId(measurementServiceId, csToolsAnnotation);
}
} catch (error) {
console.warn('Failed to add or update measurement:', error);
}
};
const addMeasurementServiceId = (id, csToolsAnnotation) => {
const { measurementData } = csToolsAnnotation;
Object.assign(measurementData, { _measurementServiceId: id });
};
[
csTools.EVENTS.MEASUREMENT_ADDED,
csTools.EVENTS.MEASUREMENT_MODIFIED,
].forEach(csToolsEvtName => {
event.detail.element.addEventListener(
csToolsEvtName,
({ detail: csToolsAnnotation }) => {
console.log(`Cornerstone Element Event: ${csToolsEvtName}`);
addOrUpdateMeasurement(csToolsAnnotation);
}
);
});
}
);
};
@@ -2,6 +2,9 @@ import Hammer from 'hammerjs';
import cornerstone from 'cornerstone-core';
import cornerstoneMath from 'cornerstone-math';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
const { log } = OHIF;
export default function(configuration = {}) {
// For debugging
@@ -12,6 +15,13 @@ export default function(configuration = {}) {
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
cornerstoneTools.init(configuration);
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(
(element, imageId, error) => {
log.error(imageId);
throw error;
}
);
// Set the tool font and font size
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
const fontFamily =
+9 -9
View File
@@ -215,15 +215,6 @@ const definitions = [
commandName: 'setToolActive',
commandOptions: { toolName: 'Bidirectional' },
},
{
id: 'Eraser',
label: 'Eraser',
icon: 'eraser',
//
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'Eraser' },
},
{
id: 'Download',
label: 'Download',
@@ -237,6 +228,15 @@ const definitions = [
},
],
},
{
id: 'Exit2DMPR',
label: 'Exit 2D MPR',
icon: 'times',
//
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'setCornerstoneLayout',
context: 'ACTIVE_VIEWPORT::VTK',
},
];
export default {
@@ -0,0 +1,141 @@
import cornerstone from 'cornerstone-core';
const SUPPORTED_TOOLS = [
'Length',
'EllipticalRoi',
'RectangleRoi',
'ArrowAnnotate',
];
const measurementServiceMappingsFactory = measurementService => {
/**
* Maps measurement service format object to cornerstone annotation object.
*
* @param {Measurement} measurement The measurement instance
* @param {string} definition The source definition
* @return {Object} Cornerstone annotation data
*/
const toAnnotation = (measurement, definition) => {
const {
id,
label,
description,
points,
unit,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID,
} = measurement;
return {
toolName: definition,
measurementData: {
sopInstanceUid: SOPInstanceUID,
frameOfReferenceUID: FrameOfReferenceUID,
SeriesInstanceUID: referenceSeriesUID,
unit,
text: label,
description,
handles: _getHandlesFromPoints(points),
_measurementServiceId: id,
},
};
};
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
const toMeasurement = csToolsAnnotation => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!validToolType(tool)) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
} = _getAttributes(element);
const points = [];
points.push(measurementData.handles);
return {
id: measurementData._measurementServiceId,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
label: measurementData.text,
description: measurementData.description,
unit: measurementData.unit,
area:
measurementData.cachedStats &&
measurementData.cachedStats
.area /* TODO: Add concept names instead (descriptor) */,
type: _getValueTypeFromToolType(tool),
points: _getPointsFromHandles(measurementData.handles),
};
};
const _getAttributes = element => {
const enabledElement = cornerstone.getEnabledElement(element);
const imageId = enabledElement.image.imageId;
const instance = cornerstone.metaData.get('instance', imageId);
return {
SOPInstanceUID: instance.SOPInstanceUID,
FrameOfReferenceUID: instance.FrameOfReferenceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
};
};
const _getValueTypeFromToolType = toolType => {
const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES;
/* TODO: Relocate static value types */
const TOOL_TYPE_TO_VALUE_TYPE = {
Length: POLYLINE,
EllipticalRoi: ELLIPSE,
RectangleRoi: POLYLINE,
ArrowAnnotate: POINT,
};
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
};
const _getPointsFromHandles = handles => {
let points = [];
Object.keys(handles).map(handle => {
if (['start', 'end'].includes(handle)) {
let point = {};
if (handles[handle].x) point.x = handles[handle].x;
if (handles[handle].y) point.y = handles[handle].y;
points.push(point);
}
});
return points;
};
const _getHandlesFromPoints = points => {
return points
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
};
return {
toAnnotation,
toMeasurement,
};
};
export default measurementServiceMappingsFactory;
@@ -0,0 +1,83 @@
import measurementServiceMappingsFactory from './measurementServiceMappingsFactory';
jest.mock('cornerstone-core', () => ({
...jest.requireActual('cornerstone-core'),
getEnabledElement: () => ({
image: { imageId: 123 },
}),
metaData: {
...jest.requireActual('cornerstone-core').metaData,
get: () => ({
SOPInstanceUID: '123',
FrameOfReferenceUID: '123',
SeriesInstanceUID: '123',
}),
},
}));
describe('measurementServiceMappings.js', () => {
let mappings;
let handles;
let points;
let csToolsAnnotation;
let measurement;
let measurementServiceMock;
let definition = 'Length';
beforeEach(() => {
measurementServiceMock = {
VALUE_TYPES: {
POLYLINE: 'value_type::polyline',
POINT: 'value_type::point',
ELLIPSE: 'value_type::ellipse',
MULTIPOINT: 'value_type::multipoint',
CIRCLE: 'value_type::circle',
},
};
mappings = measurementServiceMappingsFactory(measurementServiceMock);
handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } };
points = [{ x: 1, y: 2 }, { x: 1, y: 2 }];
csToolsAnnotation = {
toolName: definition,
measurementData: {
_measurementServiceId: 1,
sopInstanceUid: '123',
frameOfReferenceUID: '123',
SeriesInstanceUID: '123',
handles,
text: 'Test',
description: 'Test',
unit: 'mm',
},
};
measurement = {
id: 1,
SOPInstanceUID: '123',
FrameOfReferenceUID: '123',
referenceSeriesUID: '123',
label: 'Test',
description: 'Test',
unit: 'mm',
type: measurementServiceMock.VALUE_TYPES.POLYLINE,
points: points,
};
jest.clearAllMocks();
});
describe('toAnnotation()', () => {
it('map measurement service format to annotation', async () => {
const mappedMeasurement = await mappings.toAnnotation(
measurement,
definition
);
expect(mappedMeasurement).toEqual(csToolsAnnotation);
});
});
describe('toMeasurement()', () => {
it('map annotation to measurement service format', async () => {
const mappedAnnotation = await mappings.toMeasurement(csToolsAnnotation);
expect(mappedAnnotation).toEqual(measurement);
});
});
});
@@ -0,0 +1,22 @@
import { redux } from '@ohif/core';
const { setLayout } = redux.actions;
/**
* Update the current layout with a simple Cornerstone one
*
* @return void
*/
const setCornerstoneLayout = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
const action = setLayout(layout);
window.store.dispatch(action);
}
export default setCornerstoneLayout;
+35
View File
@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.0...@ohif/extension-dicom-html@1.2.1) (2020-04-28)
**Note:** Version bump only for package @ohif/extension-dicom-html
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.2...@ohif/extension-dicom-html@1.2.0) (2020-04-24)
### Features
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.1...@ohif/extension-dicom-html@1.1.2) (2020-04-02)
**Note:** Version bump only for package @ohif/extension-dicom-html
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.0...@ohif/extension-dicom-html@1.1.1) (2020-03-09)
**Note:** Version bump only for package @ohif/extension-dicom-html
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-html",
"version": "1.1.0",
"version": "1.2.1",
"description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "^0.8.2",
"dcmjs": "^0.12.2",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as dcmjs from 'dcmjs';
import dcmjs from 'dcmjs';
import TypedArrayProp from './TypedArrayProp';
import './DicomHtmlViewport.css';
@@ -11,26 +11,26 @@ const SOP_CLASS_UIDS = {
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
};
const sopClassUids = Object.values(SOP_CLASS_UIDS);
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
// TODO: Handle the case where there is more than one SOP Class Handler for the
// same SOP Class
const OHIFDicomHtmlSopClassHandler = {
id: 'OHIFDicomHtmlSopClassHandler',
type: MODULE_TYPES.SOP_CLASS_HANDLER,
sopClassUids,
sopClassUIDs,
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
return {
plugin: 'html',
modality: 'SR',
displaySetInstanceUid: utils.guid(),
Modality: 'SR',
displaySetInstanceUID: utils.guid(),
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
authorizationHeaders,
};
},
@@ -1,37 +0,0 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
+12 -6
View File
@@ -1,11 +1,17 @@
import asyncComponent from './asyncComponent.js';
import React from 'react';
import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js';
const OHIFDicomHtmlViewport = asyncComponent(() =>
import(
/* webpackChunkName: "OHIFDicomHtmlViewport" */ './OHIFDicomHtmlViewport.js'
)
);
const Component = React.lazy(() => {
return import('./OHIFDicomHtmlViewport');
});
const OHIFDicomHtmlViewport = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
export default {
/**
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.8...@ohif/extension-dicom-microscopy@0.50.9) (2020-04-02)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.7...@ohif/extension-dicom-microscopy@0.50.8) (2020-03-31)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.6...@ohif/extension-dicom-microscopy@0.50.7) (2020-03-09)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.5...@ohif/extension-dicom-microscopy@0.50.6) (2019-09-26)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-microscopy",
"version": "0.50.6",
"version": "0.50.9",
"description": "OHIF extension for Dicom Microscopy",
"author": "OHIF",
"license": "MIT",
@@ -8,7 +8,7 @@ const SOP_CLASS_UIDS = {
const DicomMicroscopySopClassHandler = {
id: 'DicomMicroscopySopClassHandlerPlugin',
sopClassUids: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
sopClassUIDs: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
getDisplaySetFromSeries(series, study, dicomWebClient) {
const instance = series.getFirstInstance();
@@ -16,12 +16,12 @@ const DicomMicroscopySopClassHandler = {
return {
plugin: 'microscopy',
modality: 'SM',
displaySetInstanceUid: utils.guid(),
Modality: 'SM',
displaySetInstanceUID: utils.guid(),
dicomWebClient,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
};
},
};
@@ -25,8 +25,8 @@ class DicomMicroscopyViewport extends Component {
const dicomWebClient = displaySet.dicomWebClient;
const searchInstanceOptions = {
studyInstanceUID: displaySet.studyInstanceUid,
seriesInstanceUID: displaySet.seriesInstanceUid,
studyInstanceUID: displaySet.StudyInstanceUID,
seriesInstanceUID: displaySet.SeriesInstanceUID,
};
dicomWebClient
@@ -35,17 +35,18 @@ class DicomMicroscopyViewport extends Component {
const promises = [];
for (let i = 0; i < instances.length; i++) {
const sopInstanceUID = instances[i]['00080018']['Value'][0];
const retrieveInstanceOptions = {
studyInstanceUID: displaySet.studyInstanceUid,
seriesInstanceUID: displaySet.seriesInstanceUid,
studyInstanceUID: displaySet.StudyInstanceUID,
seriesInstanceUID: displaySet.SeriesInstanceUID,
sopInstanceUID,
};
const promise = dicomWebClient
.retrieveInstanceMetadata(retrieveInstanceOptions)
.then(metadata => {
const imageType = metadata[0]['00080008']['Value'];
if (imageType[2] === 'VOLUME') {
const ImageType = metadata[0]['00080008']['Value'];
if (ImageType[2] === 'VOLUME') {
return metadata[0];
}
});
@@ -56,13 +57,15 @@ class DicomMicroscopyViewport extends Component {
.then(async metadata => {
metadata = metadata.filter(m => m);
const { api } = await import(/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer');
const { api } = await import(
/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer'
);
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
this.viewer = new microscopyViewer({
client: dicomWebClient,
metadata,
retrieveRendered: false
retrieveRendered: false,
});
this.viewer.render({ container });
@@ -1,37 +0,0 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
+12 -6
View File
@@ -1,11 +1,17 @@
import asyncComponent from './asyncComponent.js';
import React from 'react';
import DicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler.js';
const DicomMicroscopyViewport = asyncComponent(() =>
import(
/* webpackChunkName: "DicomMicroscopyViewport" */ './DicomMicroscopyViewport.js'
)
);
const Component = React.lazy(() => {
return import('./DicomMicroscopyViewport');
});
const DicomMicroscopyViewport = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
export default {
/**
@@ -0,0 +1,8 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
};
@@ -0,0 +1,38 @@
const merge = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'OHIFExtDicomP10Downloader',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
});
};
@@ -0,0 +1,28 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-p10-downloader@0.0.2...@ohif/extension-dicom-p10-downloader@0.1.0) (2020-04-23)
### Features
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
## 0.0.2 (2020-04-15)
**Note:** Version bump only for package @ohif/extension-dicom-p10-downloader
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1 @@
# @ohif/extension-dicom-p10-downloader
@@ -0,0 +1,40 @@
{
"name": "@ohif/extension-dicom-p10-downloader",
"version": "0.1.0",
"description": "OHIF extension for downloading DICOM P10 files",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=10",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"files": [
"dist",
"README.md"
],
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"prepublishOnly": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^2.6.0",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.5.2"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"dicomweb-client": "^0.6.0",
"file-saver": "^2.0.2",
"jszip": "^3.2.2"
}
}
@@ -0,0 +1,105 @@
import OHIF from '@ohif/core';
import {
save,
getDicomWebClientFromContext,
getSOPInstanceReferenceFromActiveViewport,
getSOPInstanceReferencesFromViewports,
} from './utils';
import _downloadAndZip from './downloadAndZip';
const {
utils: {
Queue,
},
} = OHIF;
export function getCommands(context) {
const queue = new Queue(1);
const actions = {
/**
* @example Running this command using Commands Manager
* commandsManager.runCommand(
* 'downloadAndZip',
* {
* listOfUIDs: [...],
* options: {
* progress(status) {
* console.info('Progress:', (status.progress * 100).toFixed(2) + '%');
* }
* }
* },
* 'VIEWER'
* );
*/
downloadAndZip({ servers, dicomWebClient, listOfUIDs, options }) {
return save(
_downloadAndZip(
dicomWebClient || getDicomWebClientFromContext(context, servers),
listOfUIDs,
options
),
listOfUIDs
);
},
downloadAndZipSeriesOnViewports({ servers, viewports, progress }) {
const dicomWebClient = getDicomWebClientFromContext(context, servers);
const listOfUIDs = getSOPInstanceReferencesFromViewports(viewports);
return save(
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
listOfUIDs
);
},
downloadAndZipSeriesOnActiveViewport({ servers, viewports, progress }) {
const dicomWebClient = getDicomWebClientFromContext(context, servers);
const listOfUIDs = getSOPInstanceReferenceFromActiveViewport(viewports);
return save(
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
listOfUIDs
);
},
};
const definitions = {
downloadAndZip: {
commandFn: queue.bindSafe(actions.downloadAndZip, error),
storeContexts: ['servers'],
},
downloadAndZipSeriesOnViewports: {
commandFn: queue.bindSafe(actions.downloadAndZipSeriesOnViewports, error),
storeContexts: ['servers', 'viewports'],
options: { progress },
},
downloadAndZipSeriesOnActiveViewport: {
commandFn: queue.bindSafe(
actions.downloadAndZipSeriesOnActiveViewport,
error
),
storeContexts: ['servers', 'viewports'],
options: { progress },
},
};
return {
actions,
definitions,
};
}
/**
* Utils
*/
function progress(status) {
OHIF.log.info(
'Download and Zip Progress:',
(status.progress * 100.0).toFixed(2) + '%'
);
}
function error(e) {
if (e.message === 'Queue limit reached') {
OHIF.log.warn('A download is already in progress, please wait.');
} else {
OHIF.log.error(e);
}
}
@@ -0,0 +1,244 @@
import OHIF from '@ohif/core';
import { api } from 'dicomweb-client';
import dicomParser from 'dicom-parser';
import JSZip from 'jszip';
/**
* Constants
*/
const {
utils: {
isDicomUid,
hierarchicalListUtils,
progressTrackingUtils: progressUtils,
},
} = OHIF;
/**
* Public Methods
*/
/**
* Download and Zip all DICOM P10 instances from specified DICOM Web Client
* based on an hierarchical list of UIDs;
*
* @param {DICOMwebClient} dicomWebClient A DICOMwebClient instance through
* which the referenced instances will be retrieved;
* @param {Array} listOfUIDs The hierarchical list of UIDs from the instances
* that should be retrieved:
* A hierarchical list of UIDs is a regular JS Array where the type of the UID
* (study, series, instance) is determined by its nasting lavel. For example:
* @ The following list instructs the library to download all the instances
* from both studies "A" and "B":
*
* ['studyUIDFromA', 'studyUIDFromB']
*
* @ In the previous example both UIDs are treated as STUDY UIDs because both
* of them are listed in the same (top) level of the list. If, on the other
* hand, only instances from series "I" and "J" from the study "B"
* are to be downloaded, the expected hierarchical list would be:
*
* ['studyUIDFromA', ['studyUIDFromB', ['seriesUIDFromI', 'seriesUIDFromJ']]]
*
* @ Which, when prettified, reads like this:
*
* [
* 'studyUIDFromA',
* ['studyUIDFromB', [
* 'seriesUIDFromI',
* 'seriesUIDFromJ'
* ]]
* ]
*
* @ Furthermore, if only instances "X", "Y" and "Z" from series "J" need to
* be downloaded (instead of all the instances from that series), the list
* could be changed to:
*
* [
* 'studyUIDFromA',
* ['studyUIDFromB', [
* 'seriesUIDFromI',
* ['seriesUIDFromJ', [
* 'instanceUIDFromX',
* 'instanceUIDFromY',
* 'instanceUIDFromZ'
* ]]
* ]]
* ]
*
* Please refer to hierarchicalListUtils.js for more information and utilities;
*
* @param {Object} options A plain object with options;
* @param {function} options.progress A callback to retrieve notifications
* @returns {Promise} A promise that resolves to an URL from which the ZIP file
* can be downloaded;
*/
async function downloadAndZip(dicomWebClient, listOfUIDs, options) {
if (dicomWebClient instanceof api.DICOMwebClient) {
const settings = buildSettings(listOfUIDs, options);
const { compression } = settings.tasks;
// Register user-provided progress handler as a task list observer
progressUtils.addObserver(settings.taskList, settings.options.progress);
const buffers = await downloadAll(dicomWebClient, settings).catch(error => {
// Reject promise from compression task on download failure
compression.deferred.reject(error);
throw error;
});
compression.deferred.resolve(zipAll(buffers, settings));
const url = await compression.deferred.promise;
return url;
}
throw new Error('A valid DICOM Web Client instance is expected');
}
/**
* Utils
*/
async function zipAll(buffers, settings) {
const zip = new JSZip();
OHIF.log.info('Adding DICOM P10 files to archive:', buffers.length);
buffers.forEach((buffer, i) => {
const path = buildPath(buffer) || `${i}.dcm`;
zip.file(path, buffer);
});
// Set compression task progress to 50%
progressUtils.update(settings.tasks.compression.task, 0.5);
const blob = await zip.generateAsync({ type: 'blob' });
return URL.createObjectURL(blob);
}
function buildSettings(listOfUIDs, options) {
const taskList = progressUtils.createList();
const compression = progressUtils.addDeferred(taskList);
const downloads = [];
// Build downloads list
hierarchicalListUtils.forEach(
listOfUIDs,
(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) => {
if (isDicomUid(StudyInstanceUID)) {
downloads.push({
tracking: progressUtils.addDeferred(taskList),
parameters: [StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID],
});
}
}
);
// Print tree of hierarchical references
OHIF.log.info('Downloading DICOM P10 files for references:');
OHIF.log.info(hierarchicalListUtils.print(listOfUIDs));
return {
options: Object(options),
taskList,
tasks: {
downloads,
compression,
},
};
}
function buildPath(buffer) {
let path;
try {
const byteArray = new Uint8Array(buffer);
const dataSet = dicomParser.parseDicom(byteArray, {
// Stop parsing after SeriesInstanceUID is found
untilTag: 'x0020000e',
});
const StudyInstanceUID = dataSet.string('x0020000d');
const SeriesInstanceUID = dataSet.string('x0020000e');
const SOPInstanceUID = dataSet.string('x00080018');
if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) {
path = `${StudyInstanceUID}/${SeriesInstanceUID}/${SOPInstanceUID}.dcm`;
}
} catch (e) {
OHIF.log.error('Error parsing downloaded DICOM P10 file...', e);
}
return path;
}
async function downloadAll(dicomWebClient, settings) {
const { downloads } = settings.tasks;
// Make sure at least one download was initiated
if (downloads.length < 1) {
throw new Error('No valid reference to be downloaded');
}
const promises = downloads.map(item => {
const {
parameters,
tracking: { deferred, task },
} = item;
deferred.resolve(download(task, dicomWebClient, ...parameters));
return deferred.promise;
});
// Wait on created download promises
return Promise.all(promises).then(results => {
const buffers = [];
// The "results" array may directly contain buffers (ArrayBuffer instances)
// or arrays of buffers, depending on the type of downloads initiated on the
// previous step (retrieveStudy, retrieveSeries or retrieveinstance). Ex:
// results = [buf1, [buf2, buf3], buf4, [buf5], ...];
results.forEach(
function select(nesting, result) {
if (result instanceof ArrayBuffer) {
buffers.push(result);
} else if (nesting && Array.isArray(result)) {
// "nesting" argument is important to make sure only two levels
// of arrays are visited. For example, "bufX" should not be visited:
// [buf1, [buf2, buf3, [bufX]], buf4, [buf5], ...];
result.forEach(select.bind(null, false));
}
}.bind(null, true)
);
return buffers;
});
}
async function download(
task,
dicomWebClient,
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID
) {
// Strict DICOM-formatted variable names COULDN'T be used here because the
// DICOM Web client interface expects them in this specific format.
// @TODO: Add support for download progress handler which will use the
// currently not use "task" param
if (!isDicomUid(studyInstanceUID)) {
throw new Error('Download requires at least a "StudyInstanceUID" property');
}
if (!isDicomUid(seriesInstanceUID)) {
// Download entire study
return dicomWebClient.retrieveStudy({
studyInstanceUID,
});
}
if (!isDicomUid(sopInstanceUID)) {
// Download entire series
return dicomWebClient.retrieveSeries({
studyInstanceUID,
seriesInstanceUID,
});
}
// Download specific instance
return dicomWebClient.retrieveInstance({
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID,
});
}
/**
* Exports
*/
export { downloadAndZip as default, downloadAndZip };
@@ -0,0 +1,43 @@
import { getDicomWebClientFromConfig } from './utils';
import { getCommands } from './commandsModule';
/**
* Constants
*/
/**
* Globals
*/
const sharedContext = {
dicomWebClient: null,
};
/**
* Extension
*/
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'dicom-p10-downloader',
/**
* LIFECYCLE HOOKS
*/
preRegistration({ appConfig }) {
const dicomWebClient = getDicomWebClientFromConfig(appConfig);
if (dicomWebClient) {
sharedContext.dicomWebClient = dicomWebClient;
}
},
/**
* MODULE GETTERS
*/
getCommandsModule() {
return getCommands(sharedContext);
},
};
@@ -0,0 +1,110 @@
import OHIF from '@ohif/core';
import { api } from 'dicomweb-client';
import { saveAs } from 'file-saver';
const {
utils: { isDicomUid, resolveObjectPath, hierarchicalListUtils },
DICOMWeb,
} = OHIF;
function validDicomUid(subject) {
if (isDicomUid(subject)) {
return subject;
}
}
function getActiveServerFromServersStore(store) {
const servers = resolveObjectPath(store, 'servers');
if (Array.isArray(servers) && servers.length > 0) {
return servers.find(server => resolveObjectPath(server, 'active') === true);
}
}
function getDicomWebClientFromConfig(config) {
const servers = resolveObjectPath(config, 'servers.dicomWeb');
if (Array.isArray(servers) && servers.length > 0) {
const server = servers[0];
return new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
}
}
function getDicomWebClientFromContext(context, store) {
const activeServer = getActiveServerFromServersStore(store);
if (activeServer) {
return new api.DICOMwebClient({
url: activeServer.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(activeServer),
});
} else if (context.dicomWebClient instanceof api.DICOMwebClient) {
return context.dicomWebClient;
}
}
function getSOPInstanceReference(viewports, index) {
if (index >= 0) {
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = Object(
resolveObjectPath(viewports, `viewportSpecificData.${index}`)
);
return Object.freeze(
hierarchicalListUtils.addToList(
[],
validDicomUid(StudyInstanceUID),
validDicomUid(SeriesInstanceUID),
validDicomUid(SOPInstanceUID)
)
);
}
}
function getSOPInstanceReferenceFromActiveViewport(viewports) {
return getSOPInstanceReference(
viewports,
resolveObjectPath(viewports, 'activeViewportIndex')
);
}
function getSOPInstanceReferencesFromViewports(viewports) {
const list = [];
const viewportSpecificData = resolveObjectPath(
viewports,
'viewportSpecificData'
);
Object.keys(viewportSpecificData).forEach(index => {
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = Object(
viewportSpecificData[index]
);
hierarchicalListUtils.addToList(
list,
validDicomUid(StudyInstanceUID),
validDicomUid(SeriesInstanceUID),
validDicomUid(SOPInstanceUID)
);
});
return list;
}
function save(promise, listOfUIDs) {
return Promise.resolve(promise)
.then(url => {
OHIF.log.info('Files successfully compressed:', url);
const StudyInstanceUID = hierarchicalListUtils.getItem(listOfUIDs, 0);
saveAs(url, `${StudyInstanceUID}.zip`);
return url;
})
.catch(error => {
OHIF.log.error('Failed to create Zip file...', error);
return null;
});
}
export {
save,
validDicomUid,
getDicomWebClientFromConfig,
getDicomWebClientFromContext,
getSOPInstanceReferenceFromActiveViewport,
getSOPInstanceReferencesFromViewports,
};
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.2...@ohif/extension-dicom-pdf@1.0.3) (2020-04-02)
**Note:** Version bump only for package @ohif/extension-dicom-pdf
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.1...@ohif/extension-dicom-pdf@1.0.2) (2020-03-09)
**Note:** Version bump only for package @ohif/extension-dicom-pdf
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.0...@ohif/extension-dicom-pdf@1.0.1) (2019-12-16)
### Bug Fixes
* 🐛 Update ohif pdf extension to optionally use pdfjs ([#1162](https://github.com/OHIF/Viewers/issues/1162)) ([31e542d](https://github.com/OHIF/Viewers/commit/31e542dfd973c3e982ede6d02d79bfe65175e792)), closes [#1049](https://github.com/OHIF/Viewers/issues/1049)
# [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)
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-pdf",
"version": "1.0.0",
"version": "1.0.3",
"description": "OHIF extension for Dicom PDF",
"author": "OHIF",
"license": "MIT",
@@ -37,7 +37,8 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"lodash.isequal": "^4.5.0"
"lodash.isequal": "^4.5.0",
"pdfjs-dist": "^2.2.228"
},
"gitHead": "a5baa9228c0eda0df880136bde4420d78e6f8706"
}
@@ -0,0 +1,27 @@
import OHIF from '@ohif/core';
import { connect } from 'react-redux';
import OHIFDicomPDFViewport from './OHIFDicomPDFViewport';
const { setViewportActive } = OHIF.redux.actions;
const mapStateToProps = (state, ownProps) => {
const { activeViewportIndex } = state.viewports;
return { activeViewportIndex };
};
const mapDispatchToProps = (dispatch, ownProps) => {
const { viewportIndex } = ownProps;
return {
setViewportActive: () => {
dispatch(setViewportActive(viewportIndex));
},
};
};
const ConnectedOHIFDicomPDFViewer = connect(
mapStateToProps,
mapDispatchToProps
)(OHIFDicomPDFViewport);
export default ConnectedOHIFDicomPDFViewer;
@@ -0,0 +1,62 @@
.DicomPDFViewport {
--header-height: 50px;
}
.DicomPDFViewport #toolbar {
display: flex;
align-items: center;
color: #fff;
padding: 0.5em;
border-bottom: var(--ui-border-thickness) solid var(--ui-border-color);
height: var(--header-height);
}
.DicomPDFViewport #canvas {
height: calc(100% - var(--header-height));
overflow-y: scroll;
}
.DicomPDFViewport #text-layer {
height: 100%;
margin-top: var(--header-height);
}
.DicomPDFViewport #pdf-canvas-container {
position: relative;
}
.DicomPDFViewport canvas, #text-layer {
display: block;
margin: 0 auto;
overflow: hidden;
}
.DicomPDFViewport #toolbar button {
color: currentColor;
background-color: transparent;
font: inherit;
border: var(--ui-border-thickness) solid var(--ui-border-color);
border-radius: 3px;
padding: 0.25em 0.5em;
margin-right: 0.5em;
cursor: pointer;
}
.DicomPDFViewport #text-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1;
}
.DicomPDFViewport #text-layer>span {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
+199 -25
View File
@@ -1,6 +1,14 @@
import React, { Component } from 'react';
import React, { Component, createRef } from 'react';
import dicomParser from 'dicom-parser';
import PDFJS from 'pdfjs-dist';
import PropTypes from 'prop-types';
import TypedArrayProp from './TypedArrayProp';
import './DicomPDFViewport.css';
import pdfjsBuild from 'pdfjs-dist/build/pdf';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
pdfjsBuild.GlobalWorkerOptions.workerSrc = pdfjsWorker;
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
@@ -8,22 +16,94 @@ const SOP_CLASS_UIDS = {
};
class DicomPDFViewport extends Component {
state = {
fileURL: null,
error: null,
};
constructor(props) {
super(props);
this.state = {
fileURL: null,
error: null,
currentPageIndex: 1,
pdf: null,
scale: 1,
};
this.canvas = createRef();
this.textLayer = createRef();
}
static propTypes = {
byteArray: TypedArrayProp.uint8,
useNative: PropTypes.bool,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
setViewportActive: PropTypes.func,
viewportIndex: PropTypes.number,
};
renderPDF = (dataSet, byteArray) => {
static defaultProps = {
useNative: false,
};
async componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
const fileURL = this.getPDFFileUrl(dataSet, this.props.byteArray);
this.setState(state => ({ ...state, fileURL }));
if (!this.props.useNative) {
const pdf = await PDFJS.getDocument(fileURL).promise;
this.setState(state => ({ ...state, pdf }), () => this.updatePDFCanvas());
}
}
updatePDFCanvas = async () => {
const { pdf, scale, currentPageIndex } = this.state;
const context = this.canvas.getContext('2d');
const page = await pdf.getPage(currentPageIndex);
let viewport = page.getViewport({ scale });
this.canvas.height = viewport.height;
this.canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
};
await page.render(renderContext);
const textContent = await page.getTextContent();
this.textLayer.innerHTML = '';
this.textLayer.style.height = viewport.height + 'px';
this.textLayer.style.width = viewport.width + 'px';
PDFJS.renderTextLayer({
textContent,
container: this.textLayer,
viewport,
textDivs: [],
});
};
componentDidUpdate(prevProps, prevState) {
const { currentPageIndex, scale } = this.state;
const newValidScale = prevState.scale !== scale && scale > 0;
const newValidPageNumber =
prevState.currentPageIndex !== currentPageIndex && currentPageIndex > 0;
if (newValidScale || newValidPageNumber) {
this.updatePDFCanvas();
}
}
getPDFFileUrl = (dataSet, byteArray) => {
let pdfByteArray = byteArray;
if (dataSet) {
const sopClassUid = dataSet.string('x00080016');
const SOPClassUID = dataSet.string('x00080016');
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
if (SOPClassUID !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
throw new Error('This is not a DICOM-encapsulated PDF');
}
@@ -36,50 +116,144 @@ class DicomPDFViewport extends Component {
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(PDF);
this.setState({
fileURL,
});
return fileURL;
};
onPageChange = async event => {
const { currentPageIndex, pdf } = this.state;
let newPageIndex = currentPageIndex;
const action = event.target.getAttribute('data-pager');
if (action === 'prev') {
if (currentPageIndex === 1) {
return;
}
newPageIndex -= 1;
if (currentPageIndex < 0) {
newPageIndex = 0;
}
}
if (action === 'next') {
if (currentPageIndex === pdf.numPages - 1) {
return;
}
newPageIndex += 1;
if (currentPageIndex > pdf.numPages - 1) {
newPageIndex = pdf.numPages - 1;
}
}
this.setState(state => ({ ...state, currentPageIndex: newPageIndex }));
};
onZoomChange = () => {
let newZoomValue = this.state.scale;
const action = event.target.getAttribute('data-pager');
if (action === '+') {
newZoomValue += 0.25;
}
if (action === '-') {
newZoomValue -= 0.25;
}
this.setState(state => ({ ...state, scale: newZoomValue }));
};
parseByteArray = byteArray => {
const options = {
untilTag: '',
};
const options = { untilTag: '' };
let dataSet;
try {
dataSet = dicomParser.parseDicom(byteArray, options);
} catch (error) {
this.setState({
error,
});
this.setState(state => ({ ...state, error }));
}
return dataSet;
};
componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
setViewportActiveHandler = () => {
const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
this.renderPDF(dataSet, this.props.byteArray);
}
if (viewportIndex !== activeViewportIndex) {
setViewportActive(viewportIndex);
}
};
downloadPDFCanvas = () => {
const { fileURL } = this.state;
const a = document.createElement('a');
a.href = fileURL;
a.download = fileURL.substr(fileURL.lastIndexOf('/') + 1);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
render() {
const { fileURL, pdf, error } = this.state;
return (
<div
className={'DicomPDFViewport'}
onClick={this.setViewportActiveHandler}
onScroll={this.setViewportActiveHandler}
style={{ width: '100%', height: '100%' }}
>
{this.state.fileURL && (
{!this.props.useNative ? (
<>
<div id="toolbar">
<div id="pager">
{pdf && pdf.numPages > 1 && (
<>
<button data-pager="prev" onClick={this.onPageChange}>
{`<`}
</button>
<button data-pager="next" onClick={this.onPageChange}>
{`>`}
</button>
</>
)}
<button data-pager="-" onClick={this.onZoomChange}>
{`-`}
</button>
<button data-pager="+" onClick={this.onZoomChange}>
{`+`}
</button>
<button onClick={this.downloadPDFCanvas}>Download</button>
</div>
</div>
<div id="canvas">
<div id="pdf-canvas-container">
<canvas
id="pdf-canvas"
ref={canvas => (this.canvas = canvas)}
/>
<div
id="text-layer"
ref={textLayer => (this.textLayer = textLayer)}
></div>
</div>
</div>
</>
) : (
<object
data={this.state.fileURL}
aria-label="PDF Viewer"
data={fileURL}
type="application/pdf"
width="100%"
height="100%"
/>
)}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
{error && <h2>{JSON.stringify(error)}</h2>}
</div>
);
}
@@ -8,19 +8,19 @@ const SOP_CLASS_UIDS = {
const OHIFDicomPDFSopClassHandler = {
id: 'OHIFDicomPDFSopClassHandlerPlugin',
type: MODULE_TYPES.SOP_CLASS_HANDLER,
sopClassUids: [SOP_CLASS_UIDS.ENCAPSULATED_PDF],
sopClassUIDs: [SOP_CLASS_UIDS.ENCAPSULATED_PDF],
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
return {
plugin: 'pdf',
modality: 'DOC',
displaySetInstanceUid: utils.guid(),
Modality: 'DOC',
displaySetInstanceUID: utils.guid(),
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
authorizationHeaders: authorizationHeaders,
};
},
@@ -11,6 +11,9 @@ class OHIFDicomPDFViewport extends Component {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
setViewportActive: PropTypes.func,
};
state = {
@@ -30,42 +33,37 @@ class OHIFDicomPDFViewport extends Component {
componentDidMount() {
const { displaySet, studies } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders,
} = displaySet;
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => {
const byteArray = new Uint8Array(data);
this.setState({
byteArray: byteArray,
});
},
data => this.setState({ byteArray: new Uint8Array(data) }),
error => {
this.setState({
error,
});
this.setState({ error });
throw new Error(error);
}
);
}
render() {
const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
const { byteArray, error } = this.state;
const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy };
return (
<OHIFComponentPlugin {...pluginProps}>
{this.state.byteArray && (
<DicomPDFViewport byteArray={this.state.byteArray} />
{byteArray && (
<DicomPDFViewport
byteArray={byteArray}
setViewportActive={setViewportActive}
viewportIndex={viewportIndex}
activeViewportIndex={activeViewportIndex}
/>
)}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
{error && <h2>{JSON.stringify(error)}</h2>}
</OHIFComponentPlugin>
);
}
+15 -4
View File
@@ -1,16 +1,27 @@
import React from 'react';
import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js';
import OHIFDicomPDFViewport from './OHIFDicomPDFViewport.js';
const Component = React.lazy(() => {
return import('./ConnectedOHIFDicomPDFViewer');
});
const ConnectedOHIFDicomPDFViewer = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'pdf',
getViewportModule() {
return OHIFDicomPDFViewport;
return ConnectedOHIFDicomPDFViewer;
},
getSopClassHandlerModule() {
return OHIFDicomPDFSopClassHandler;
}
},
};
@@ -0,0 +1,8 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
};
@@ -0,0 +1,44 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'OHIFExtDicomSeg',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
});
};
+27
View File
@@ -0,0 +1,27 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.1.0...@ohif/extension-dicom-rt@0.1.1) (2020-04-28)
**Note:** Version bump only for package @ohif/extension-dicom-rt
# [0.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.0.2...@ohif/extension-dicom-rt@0.1.0) (2020-04-24)
### Features
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
## 0.0.2 (2020-04-09)
**Note:** Version bump only for package @ohif/extension-dicom-rt
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -0,0 +1 @@
# @ohif/extension-dicom-rt
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@ohif/extension-dicom-rt",
"version": "0.1.1",
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"engines": {
"node": ">=10",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"files": [
"dist",
"README.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
"dev:dicom-html": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"prepublishOnly": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.0.9",
"dcmjs": "^0.12.2",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"react-select": "^3.0.8"
}
}
@@ -0,0 +1,237 @@
import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core';
import loadRTStruct from './loadRTStruct';
import id from './id';
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
DICOM_RT_STRUCT: '1.2.840.10008.5.1.4.1.1.481.3',
};
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
const OHIFDicomRTStructSopClassHandler = {
id: 'OHIFDicomRTStructSopClassHandler',
type: MODULE_TYPES.SOP_CLASS_HANDLER,
sopClassUIDs,
getDisplaySetFromSeries: function (
series,
study,
dicomWebClient,
authorizationHeaders
) {
const instance = series.getFirstInstance();
const metadata = instance.getData().metadata;
const {
SeriesDate,
SeriesTime,
SeriesDescription,
FrameOfReferenceUID,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = metadata;
// TODO -> GET REFERENCED FRAME OF REFERENCE SEQUENCE.
const rtStructDisplaySet = {
Modality: 'RTSTRUCT',
displaySetInstanceUID: utils.guid(),
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
FrameOfReferenceUID,
authorizationHeaders,
metadata,
isDerived: true,
referencedDisplaySetUID: null, // Assigned when loaded.
labelmapIndex: null, // Assigned when loaded.
isLoaded: false,
SeriesDate,
SeriesTime,
SeriesDescription,
};
if (!metadata.ReferencedSeriesSequence) {
const ReferencedFrameOfReferenceSequence =
metadata.ReferencedFrameOfReferenceSequence;
if (ReferencedFrameOfReferenceSequence) {
// TODO -> @dannyrb Do we augment metadata or add a (potentially large? fallback list in filterDerivedDisplaySets )
metadata.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
ReferencedFrameOfReferenceSequence
);
}
}
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
return loadRTStruct(
rtStructDisplaySet,
referencedDisplaySet,
studies
).catch(error => {
rtStructDisplaySet.isLoaded = false;
throw new Error(error);
});
};
return rtStructDisplaySet;
},
};
function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
ReferencedFrameOfReferenceSequence
) {
const ReferencedSeriesSequence = [];
_getSequenceAsArray(ReferencedFrameOfReferenceSequence).forEach(
referencedFrameOfReference => {
const { RTReferencedStudySequence } = referencedFrameOfReference;
_getSequenceAsArray(RTReferencedStudySequence).forEach(
rtReferencedStudy => {
const { RTReferencedSeriesSequence } = rtReferencedStudy;
_getSequenceAsArray(RTReferencedSeriesSequence).forEach(
rtReferencedSeries => {
const ReferencedInstanceSequence = [];
const {
ContourImageSequence,
SeriesInstanceUID,
} = rtReferencedSeries;
_getSequenceAsArray(ContourImageSequence).forEach(
contourImage => {
ReferencedInstanceSequence.push({
ReferencedSOPInstanceUID:
contourImage.ReferencedSOPInstanceUID,
ReferencedSOPClassUID: contourImage.ReferencedSOPClassUID,
});
}
);
const referencedSeries = {
SeriesInstanceUID,
ReferencedInstanceSequence,
};
ReferencedSeriesSequence.push(referencedSeries);
}
);
}
);
}
);
return ReferencedSeriesSequence;
}
function _getSequenceAsArray(sequence) {
return Array.isArray(sequence) ? sequence : [sequence];
}
function _getReferencedFrameOfReferenceSequence(instance) {
const referencedFrameOfReferenceSequenceRaw = instance['30060010'];
const referencedFrameOfReferenceSequence = [];
if (
!referencedFrameOfReferenceSequenceRaw ||
!referencedFrameOfReferenceSequenceRaw.Value
) {
return undefined;
}
referencedFrameOfReferenceSequenceRaw.Value.forEach(
referencedFrameOfReferenceRaw => {
const frameOfReferenceUID = DICOMWeb.getString(
referencedFrameOfReferenceRaw['00200052']
);
const referencedFrameOfReference = { frameOfReferenceUID };
const RTReferencedStudySequenceRaw =
referencedFrameOfReferenceRaw['30060012'];
referencedFrameOfReferenceSequence.push(referencedFrameOfReference);
if (RTReferencedStudySequenceRaw && RTReferencedStudySequenceRaw.Value) {
referencedFrameOfReference.rtReferencedStudySequence = [];
const { rtReferencedStudySequence } = referencedFrameOfReference;
RTReferencedStudySequenceRaw.Value.forEach(rtReferencedStudyRaw => {
const referencedSopClassUID = DICOMWeb.getString(
rtReferencedStudyRaw['00081150']
);
const referencedSOPInstanceUID = DICOMWeb.getString(
rtReferencedStudyRaw['00081155']
);
const rtReferencedStudy = {
referencedSopClassUID,
referencedSOPInstanceUID,
rtReferencedSeriesSequence: [],
};
rtReferencedStudySequence.push(rtReferencedStudy);
const { rtReferencedSeriesSequence } = rtReferencedStudy;
const rtReferencedSeriesSequenceRaw =
rtReferencedStudyRaw['30060014'];
rtReferencedSeriesSequenceRaw.Value.forEach(rtReferencedSeriesRaw => {
const seriesInstanceUID = DICOMWeb.getString(
rtReferencedSeriesRaw['0020000E']
);
const rtReferencedSeries = {
seriesInstanceUID,
contourImageSequence: [],
};
rtReferencedSeriesSequence.push(rtReferencedSeries);
const { contourImageSequence } = rtReferencedSeries;
const contourImageSequenceRaw = rtReferencedSeriesRaw['30060016'];
contourImageSequenceRaw.Value.forEach(contourImageRaw => {
const referencedSOPClassUID = DICOMWeb.getString(
contourImageRaw['00081150']
);
const referencedSOPInstanceUID = DICOMWeb.getString(
contourImageRaw['00081155']
);
const referencedFrameNumber = DICOMWeb.getString(
contourImageRaw['00081160']
);
const referencedSegmentNumber = DICOMWeb.getString(
contourImageRaw['0062000B']
);
const contourImage = {
referencedSOPClassUID,
referencedSOPInstanceUID,
referencedFrameNumber,
referencedSegmentNumber,
};
contourImageSequence.push(contourImage);
});
});
});
}
}
);
return referencedFrameOfReferenceSequence;
}
export default OHIFDicomRTStructSopClassHandler;
@@ -0,0 +1,61 @@
import React from 'react';
const LoadingIndicator = ({ height = "200px", width = "200px", expand = false }) => {
return (
<div style={expand ? { height: "100%", width: "100%" } : {}}>
<svg style={{ margin: 'auto', background: 'none', display: 'block', shapeRendering: 'auto' }} width={width} height={height} viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<g transform="rotate(0 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(30 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(60 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(90 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(120 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(150 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(180 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(210 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(240 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(270 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(300 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(330 50 50)">
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
</rect>
</g>
</svg>
</div>
);
};
export default LoadingIndicator;
@@ -0,0 +1,44 @@
.dcmrt-panel-section {
display: flex;
flex-direction: column;
color: var(--text-primary-color);
font-size: 12px;
}
.dcmrt-panel-section .header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: var(--ui-gray-dark);
border-radius: 5px;
font-weight: bold;
padding: 10px 15px;
}
.dcmrt-panel-section .header .eye-icon,
.dcmrt-panel-section .header .angle-double-down,
.dcmrt-panel-section .header .angle-double-up {
cursor: pointer;
color: var(--active-color);
}
.dcmrt-panel-section .header .eye-icon:hover,
.dcmrt-panel-section .header .angle-double-down:hover,
.dcmrt-panel-section .header .angle-double-up:hover {
color: var(--hover-color);
}
.dcmrt-panel-section .header .eye-icon.expanded {
color: var(--default-color);
}
.icons {
display: flex;
align-items: center;
border-radius: 5px;
}
.icons .angle-double-down, .icons .angle-double-up {
margin-left: 15px;
}
@@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@ohif/ui';
import './PanelSection.css';
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
const PanelSection = ({
title,
children,
visible = false,
expanded = false,
loading = false,
hideVisibleButton = false,
onVisibilityChange = () => { },
onExpandChange = () => { }
}) => {
const [isExpanded, setIsExpanded] = useState(expanded);
const [isVisible, setIsVisible] = useState(visible);
useEffect(() => {
setIsVisible(visible);
}, [visible]);
return (
<div
className="dcmrt-panel-section"
style={{
marginBottom: isExpanded ? 0 : 2,
height: isExpanded ? '100%' : 'unset'
}}
>
<div className="header">
<div>{title}</div>
<div className="icons">
{!hideVisibleButton && (
<Icon
className={`eye-icon ${isVisible && 'expanded'}`}
name={isVisible ? "eye" : "eye-closed"}
width="20px"
height="20px"
onClick={() => {
const newVisibility = !isVisible;
setIsVisible(newVisibility);
onVisibilityChange(newVisibility);
}}
/>
)}
<Icon
className={`angle-double-${isExpanded ? 'down' : 'up'} ${isExpanded && 'expanded'}`}
name={`angle-double-${isExpanded ? 'down' : 'up'}`}
width="20px"
height="20px"
onClick={() => {
const newExpandValue = !isExpanded;
setIsExpanded(newExpandValue);
onExpandChange(newExpandValue);
}}
/>
</div>
</div>
{loading && isExpanded && <LoadingIndicator expand height="70px" width="70px" />}
{children}
</div>
);
};
PanelSection.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
visible: PropTypes.bool,
expanded: PropTypes.bool,
onVisibilityChange: PropTypes.func
};
PanelSection.defaultProps = {
visible: false,
expanded: false,
onVisibilityChange: () => { }
};
export default PanelSection;
@@ -0,0 +1,26 @@
.dcmrt-panel {
display: flex;
flex-direction: column;
background-color: var(--primary-background-color);
height: 100%;
width: 100%;
padding: 20px;
}
.dcmrt-panel .dcmrt-panel-header {
display: flex;
flex-direction: row;
justify-content: space-between;
color: white;
}
.dcmrt-panel .dcmrt-panel-header .cog-icon {
align-self: flex-end;
color: var(--default-color);
cursor: pointer;
margin-bottom: 20px;
}
.dcmrt-panel .dcmrt-panel-header .cog-icon:hover {
color: var(--hover-color);
}
@@ -0,0 +1,258 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
import { utils } from '@ohif/core';
import './RTPanel.css';
import StructureSetItem from '../StructureSetItem/StructureSetItem';
import RTPanelSettings from '../RTSettings/RTSettings';
import PanelSection from '../PanelSection/PanelSection';
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
const { studyMetadataManager } = utils;
const refreshViewport = () => {
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
};
/**
* RTPanel component
*
* @param {Object} props
* @param {Array} props.studies
* @param {Array} props.viewports - viewportSpecificData
* @param {number} props.activeIndex - activeViewportIndex
* @param {number} props.isOpen - isOpen
* @returns component
*/
const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => {
const [selectedContour, setSelectedContour] = useState();
const DEFAULT_SET_INDEX = 0;
const DEFAULT_STATE = {
referencedDisplaysets: [],
sets: [],
selectedSet: null,
};
const [state, setState] = useState(DEFAULT_STATE);
const [showSettings, setShowSettings] = useState(false);
const activeViewport = viewports[activeIndex];
/*
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
*
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
* or other components that could rely on loaded rtstructs that
* the first batch of structs were loaded so that e.g. when the user opens the panel
* before the structs are fully loaded, the panel can subscribe to this custom event
* and update itself with the new structs.
*
* This limitation is due to the fact that the rtmodule is an object (which will be
* updated after the structs are loaded) that React its not aware of its changes
* because the module object its not passed in to the panel component as prop but accessed externally.
*
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
* allows us to easily watch the module or the rtstruct loading process in any other component
* without subscribing to external events.
*/
useEffect(() => {
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
return () => {
document.removeEventListener('extensiondicomrtrtloaded', updateStructureSets);
};
}, []);
const updateStructureSets = () => {
const module = cornerstoneTools.getModule('rtstruct');
const StructureSets = module.state.StructureSets;
if (StructureSets && StructureSets.length) {
const viewportSets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
activeViewport.SeriesInstanceUID
);
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
Modality: 'RTSTRUCT',
});
if (viewportSets.length) {
const defaultSet = viewportSets[DEFAULT_SET_INDEX];
setState({
referencedDisplaysets,
selectedSet: defaultSet,
sets: viewportSets
});
} else {
setState(DEFAULT_STATE);
}
}
};
useEffect(() => {
updateStructureSets();
}, [studies, viewports, activeIndex]);
useEffect(() => {
setShowSettings(showSettings && !isOpen);
}, [isOpen]);
const toContourItem = ({ ROINumber, ROIName, RTROIObservations, colorArray, visible }, loadedSet) => {
let interpretedType = '';
if (RTROIObservations && RTROIObservations.RTROIInterpretedType) {
interpretedType = `(${RTROIObservations.RTROIInterpretedType})`;
}
const isSameContour = selectedContour && selectedContour === ROINumber;
return (
<StructureSetItem
key={ROINumber}
selected={isSameContour}
onClick={() => {
setSelectedContour(isSameContour ? null : ROINumber);
const enabledElements = cornerstone.getEnabledElements();
const element = enabledElements[activeIndex].element;
const toolState = cornerstoneTools.getToolState(element, 'stack');
if (!toolState) {
return;
}
const imageIds = toolState.data[0].imageIds;
const module = cornerstoneTools.getModule('rtstruct');
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
state.selectedSet.SeriesInstanceUID,
ROINumber,
imageIds
);
const frameIndex = imageIds.indexOf(imageId);
const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId);
const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId);
onContourItemClick({
StudyInstanceUID,
SOPInstanceUID,
frameIndex,
activeViewportIndex: activeIndex
});
}}
label={`${ROIName} ${interpretedType}`}
index={ROINumber}
color={colorArray}
visible={visible}
onVisibilityChange={() => {
const module = cornerstoneTools.getModule('rtstruct');
module.setters.toggleROIContour(state.selectedSet.SeriesInstanceUID, ROINumber);
}}
/>
);
};
const configurationChangeHandler = newConfiguration => {
const module = cornerstoneTools.getModule('rtstruct');
module.configuration.lineWidth = newConfiguration.lineWidth;
module.configuration.opacity = newConfiguration.opacity;
refreshViewport();
};
if (showSettings) {
const module = cornerstoneTools.getModule('rtstruct');
return (
<RTPanelSettings
configuration={module.configuration}
onBack={() => setShowSettings(false)}
onChange={configurationChangeHandler}
/>
);
}
return (
<div className="dcmrt-panel">
<div className="dcmrt-panel-header">
<h3>RT Structure Sets</h3>
<Icon
className="cog-icon"
name="cog"
width="25px"
height="25px"
onClick={() => setShowSettings(true)}
/>
</div>
{!state.referencedDisplaysets.length && <LoadingIndicator expand height="70px" width="70px" />}
{state.referencedDisplaysets.map(displaySet => {
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
const module = cornerstoneTools.getModule('rtstruct');
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
const loadedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
return (
<PanelSection
key={SeriesInstanceUID}
title={metadata.StructureSetLabel}
loading={!isLoaded || !loadedSet}
visible={isLoaded && loadedSet.visible}
hideVisibleButton={!isLoaded}
expanded={isLoaded && loadedSet.SeriesInstanceUID === state.selectedSet.SeriesInstanceUID}
onVisibilityChange={() => {
const module = cornerstoneTools.getModule('rtstruct');
module.setters.toggleStructureSet(SeriesInstanceUID);
}}
onExpandChange={async () => {
if (!isLoaded) {
await displaySet.load(viewports[activeIndex], studies);
const module = cornerstoneTools.getModule('rtstruct');
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
const selectedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
setState(state => ({ ...state, selectedSet, sets }));
}
}}
>
<ScrollableArea>
<TableList headless>
{isLoaded && loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))}
</TableList>
</ScrollableArea>
</PanelSection>
);
})}
</div>
);
};
RTPanel.propTypes = {
viewports: PropTypes.shape({
displaySetInstanceUID: PropTypes.string,
frameRate: PropTypes.any,
InstanceNumber: PropTypes.number,
isMultiFrame: PropTypes.bool,
isReconstructable: PropTypes.bool,
Modality: PropTypes.string,
plugin: PropTypes.string,
SeriesDate: PropTypes.string,
SeriesDescription: PropTypes.string,
SeriesInstanceUID: PropTypes.string,
SeriesNumber: PropTypes.any,
SeriesTime: PropTypes.string,
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
StudyInstanceUID: PropTypes.string,
}),
activeIndex: PropTypes.number.isRequired,
studies: PropTypes.array.isRequired,
isOpen: PropTypes.bool.isRequired,
};
RTPanel.defaultProps = {};
export default RTPanel;
@@ -0,0 +1,88 @@
.dcmrt-settings {
display: flex;
flex-direction: column;
padding: 20px;
color: white;
}
.dcmrt-settings .settings-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-bottom: 20px;
}
.dcmrt-settings .settings-title h3 {
color: var(--text-primary-color);
margin: 0;
padding: 0;
}
.dcmrt-settings .settings-section .content {
padding: 10px;
background-color: var(--ui-gray-dark);
border-radius: 5px;
}
.dcmrt-settings .settings-section .header {
border-bottom: 2px solid black;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: var(--ui-gray-dark);
border-radius: 5px;
font-weight: bold;
font-size: 12px;
padding: 10px 15px;
}
.dcmrt-settings .settings-section p {
font-weight: bold;
}
.dcmrt-settings .range {
display: flex;
padding: 10px 10px;
font-size: 12px;
}
.dcmrt-settings .range:not(:last-of-type) {
padding-bottom: 0;
}
.dcmrt-settings .range label {
margin: 0;
display: unset;
min-width: 40px;
max-width: 40px;
}
.dcmrt-settings .range input {
width: 100%;
padding: 0 20px 0 20px;
}
.dcmrt-settings .return-button {
color: #333;
background-color: #fff;
padding: 4px 12px;
line-height: 1.4;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
user-select: none;
border: 1px solid #ccc;
border-radius: 4px;
transition: all 0.15s ease;
}
.dcmrt-settings .return-button:hover,
.dcmrt-settings .return-button:active,
.dcmrt-settings .return-button.active {
background-color: var(--active-color);
border-color: var(--ui-border-color-active);
color: #fff;
}
@@ -0,0 +1,71 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Range } from '@ohif/ui';
import './RTSettings.css';
const RTSettings = ({ configuration, onBack, onChange }) => {
const toFloat = value => parseFloat(value / 100).toFixed(2);
const save = (field, value) => {
onChange({ ...configuration, [field]: value });
};
const SettingsSection = ({ title, children }) => {
return (
<div className="settings-section">
<div className="header">{title}</div>
<div className="content">
{children}
</div>
</div>
);
};
return (
<div className="dcmrt-settings">
<div className="settings-title">
<h3>RT Structure Set Settings</h3>
<button className="return-button" onClick={onBack}>
Back
</button>
</div>
<SettingsSection title="Segment Outline">
<div className="range">
<label htmlFor="range">Opacity</label>
<Range
showPercentage
step={1}
min={0}
max={100}
value={configuration.opacity * 100}
onChange={event => save('opacity', toFloat(event.target.value))}
/>
</div>
<div className="range">
<label htmlFor="range">Width</label>
<Range
showValue
step={1}
min={1}
max={5}
value={configuration.lineWidth}
onChange={event => save('lineWidth', parseInt(event.target.value))}
/>
</div>
</SettingsSection>
</div>
);
};
RTSettings.propTypes = {
configuration: PropTypes.shape({
lineWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
}).isRequired,
onBack: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
export default RTSettings;
@@ -0,0 +1,88 @@
.dcmrt-structure-set-item .item-color-section {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.dcmrt-structure-set-item .item-color {
margin-top: 8px;
border-radius: 100%;
width: 20px;
height: 20px;
}
.dcmrt-structure-set-item .item-info {
display: inline-block;
margin-top: 9px;
padding-left: 9px;
width: 90px;
border-left: 1px solid var(--text-secondary-color);
color: var(--text-primary-color);
width: 100%;
}
.dcmrt-structure-set-item.selected .item-actions {
height: 35px;
visibility: visible;
}
.dcmrt-structure-set-item .item-actions {
margin-left: -1px;
background-color: var(--ui-gray-darker);
height: 0;
overflow: hidden;
transition: all 0.3s ease;
visibility: hidden;
padding-left: 14px;
}
.dcmrt-structure-set-item .item-actions .btnAction {
background-color: transparent;
border: none;
color: var(--default-color);
cursor: pointer;
line-height: 35px;
height: 35px;
transition: all 0.3s ease;
}
.dcmrt-structure-set-item .item-actions .btnAction:hover, .dcmrt-structure-set-item .item-actions .btnAction:active {
color: var(--text-primary-color);
}
.dcmrt-structure-set-item .item-actions .btnAction i {
margin-right: 4px;
}
.dcmrt-structure-set-item.selected .item-actions {
height: 35px;
visibility: visible;
}
.dcmrt-structure-set-item .item-label {
display: flex;
justify-content: space-between;
padding-right: 10px;
}
.dcmrt-structure-set-item .item-label span {
overflow-wrap: normal;
white-space: nowrap;
overflow: hidden;
max-width: calc(100% - 40px); /* calc(100% - 50px); 20px = eye icon */
text-overflow: ellipsis;
}
.dcmrt-structure-set-item .item-label .eye-icon {
cursor: pointer;
color: var(--active-color);
}
.dcmrt-structure-set-item .item-label .eye-icon:hover {
color: var(--hover-color);
}
.dcmrt-structure-set-item .item-label .eye-icon.--visible {
color: var(--default-color);
}
@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { TableListItem, Icon } from '@ohif/ui';
import './StructureSetItem.css';
const ColoredCircle = ({ color }) => {
return (
<div
className="item-color"
style={{ backgroundColor: `rgba(${color.join(',')})` }}
></div>
);
};
ColoredCircle.propTypes = {
color: PropTypes.array.isRequired,
};
const StructureSetItem = ({
index,
label,
onClick,
itemClass,
color,
visible = true,
onVisibilityChange,
selected = false,
}) => {
const [isVisible, setIsVisible] = useState(visible);
return (
<div className={`dcmrt-structure-set-item ${selected && 'selected'}`}>
<TableListItem
key={index}
itemKey={index}
itemIndex={index}
itemClass={itemClass}
itemMeta={<ColoredCircle color={color} />}
itemMetaClass="item-color-section"
onItemClick={onClick}
>
<div>
<div className="item-label" style={{ marginBottom: 4 }}>
<span>{label}</span>
<Icon
className={`eye-icon ${isVisible && '--visible'}`}
name={isVisible ? 'eye' : 'eye-closed'}
width="20px"
height="20px"
onClick={event => {
event.stopPropagation();
const newVisibility = !isVisible;
setIsVisible(newVisibility);
onVisibilityChange(newVisibility);
}}
/>
</div>
{false && <div className="item-info">{'...'}</div>}
{false && (
<div className="item-actions">
<button
className="btnAction"
onClick={() => console.log('Relabelling...')}
>
<span style={{ marginRight: '4px' }}>
<Icon name="edit" width="14px" height="14px" />
</span>
Relabel
</button>
<button
className="btnAction"
onClick={() => console.log('Editing description...')}
>
<span style={{ marginRight: '4px' }}>
<Icon name="edit" width="14px" height="14px" />
</span>
Description
</button>
</div>
)}
</div>
</TableListItem>
</div>
);
};
StructureSetItem.propTypes = {
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
itemClass: PropTypes.string,
color: PropTypes.array.isRequired,
};
StructureSetItem.defaultProps = {
itemClass: '',
onClick: () => { },
};
export default StructureSetItem;
+3
View File
@@ -0,0 +1,3 @@
const id = 'rt';
export default id;
+77
View File
@@ -0,0 +1,77 @@
import React from 'react';
import init from './init.js';
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
import id from './id.js';
import RTPanel from './components/RTPanel/RTPanel';
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id,
/**
*
*
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
getPanelModule({ commandsManager }) {
const ExtendedRTPanel = props => {
const contourItemClickHandler = contourData => {
commandsManager.runCommand('jumpToImage', contourData);
};
return (
<RTPanel {...props} onContourItemClick={contourItemClickHandler} />
);
};
return {
menuOptions: [
{
icon: 'list',
label: 'RTSTRUCT',
target: 'rt-panel',
isDisabled: studies => {
if (!studies) {
return true;
}
for (let i = 0; i < studies.length; i++) {
const study = studies[i];
if (study && study.series) {
for (let j = 0; j < study.series.length; j++) {
const series = study.series[j];
if (
/* Could be expanded to contain RTPLAN and RTDOSE information in the future */
['RTSTRUCT'].includes(series.Modality)
) {
return false;
}
}
}
}
return true;
},
},
],
components: [
{
id: 'rt-panel',
component: ExtendedRTPanel,
},
],
defaultContext: ['VIEWER'],
};
},
getSopClassHandlerModule({ servicesManager }) {
return sopClassHandlerModule;
},
};
+25
View File
@@ -0,0 +1,25 @@
import cornerstoneTools from 'cornerstone-tools';
import RTStructDisplayTool from './tools/RTStructDisplayTool';
import rtStructModule from './tools/modules/rtStructModule';
import TOOL_NAMES from './utils/toolNames';
const defaultConfig = {
TOOL_NAMES: {
RT_STRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
},
};
/**
*
* @param {object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ servicesManager, configuration = {} }) {
const conifg = Object.assign({}, defaultConfig, configuration);
TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL = conifg.TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL;
cornerstoneTools.register('module', 'rtstruct', rtStructModule);
cornerstoneTools.addTool(RTStructDisplayTool);
}
+309
View File
@@ -0,0 +1,309 @@
import OHIF from '@ohif/core';
import dcmjs from 'dcmjs';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
import TOOL_NAMES from './utils/toolNames';
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
const globalImageIdSpecificToolStateManager =
cornerstoneTools.globalImageIdSpecificToolStateManager;
const { DicomLoaderService } = OHIF.utils;
export default async function loadRTStruct(
rtStructDisplaySet,
referencedDisplaySet,
studies
) {
const rtStructModule = cornerstoneTools.getModule('rtstruct');
// Set here is loading is asynchronous.
// If this function throws its set back to false.
rtStructDisplaySet.isLoaded = true;
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
rtStructDisplaySet,
studies
);
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
const rtStructDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
rtStructDataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
dicomData.meta
);
// global cornerstone tools state to attach measurements to.
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
const {
StructureSetROISequence,
ROIContourSequence,
RTROIObservationsSequence,
StructureSetLabel,
} = rtStructDataset;
// Define our structure set entry and add it to the rtstruct module state.
const structureSet = {
StructureSetLabel,
SeriesInstanceUID: rtStructDataset.SeriesInstanceUID,
ROIContours: [],
referencedSeriesSequence:
rtStructDisplaySet.metadata.ReferencedSeriesSequence,
visible: true,
};
rtStructModule.setters.structureSet(structureSet);
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
studies,
StudyInstanceUID,
SeriesInstanceUID
);
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
for (let i = 0; i < ROIContourSequence.length; i++) {
const ROIContour = ROIContourSequence[i];
const { ReferencedROINumber, ContourSequence } = ROIContour;
if (!ContourSequence) {
continue;
}
_setROIContourMetadata(
structureSet,
StructureSetROISequence,
RTROIObservationsSequence,
ROIContour
);
for (let c = 0; c < ContourSequence.length; c++) {
const {
ContourImageSequence,
ContourData,
NumberOfContourPoints,
ContourGeometricType,
} = ContourSequence[c];
if (ContourGeometricType !== 'CLOSED_PLANAR') {
// TODO: Do we want to visualise types other than closed planar?
// We could easily do open planar.
continue;
}
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
toolState,
imageId,
rtStructDisplayToolName
);
const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
const points = [];
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
points.push({
x: ContourData[p],
y: ContourData[p + 1],
z: ContourData[p + 2],
});
}
transformPointsToImagePlane(points, imagePlane);
const measurementData = {
handles: {
points,
},
structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
ROINumber: ReferencedROINumber,
};
imageIdSpecificToolData.push(measurementData);
}
}
_setToolEnabledIfNotEnabled(rtStructDisplayToolName);
/*
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
*
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
* or other components that could rely on loaded rtstructs that
* the first batch of structs were loaded so that e.g. when the user opens the panel
* before the structs are fully loaded, the panel can subscribe to this custom event
* and update itself with the new structs.
*
* This limitation is due to the fact that the rtmodule is an object (which will be
* updated after the structs are loaded) that React its not aware of its changes
* because the module object its not passed in to the panel component as prop but accessed externally.
*
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
* allows us to easily watch the module or the rtstruct loading process in any other component
* without subscribing to external events.
*/
const event = new CustomEvent('extensiondicomrtrtloaded');
document.dispatchEvent(event);
}
function _setROIContourMetadata(
structureSet,
StructureSetROISequence,
RTROIObservationsSequence,
ROIContour
) {
const StructureSetROI = StructureSetROISequence.find(
structureSetROI =>
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
);
const ROIContourData = {
ROINumber: StructureSetROI.ROINumber,
ROIName: StructureSetROI.ROIName,
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
ROIDescription: StructureSetROI.ROIDescription,
visible: true,
};
_setROIContourDataColor(ROIContour, ROIContourData);
if (RTROIObservationsSequence) {
// If present, add additional RTROIObservations metadata.
_setROIContourRTROIObservations(
ROIContourData,
RTROIObservationsSequence,
ROIContour.ReferencedROINumber
);
}
structureSet.ROIContours.push(ROIContourData);
}
function _setROIContourDataColor(ROIContour, ROIContourData) {
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
// If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
}
if (ROIDisplayColor) {
ROIContourData.colorArray = [...ROIDisplayColor];
} else {
//Choose a color from the cornerstoneTools colorLUT
// We sample from the default color LUT here (i.e. 0), as we have nothing else to go on.
const { getters } = cornerstoneTools.getModule('segmentation');
const color = getters.colorForSegmentIndexColorLUT(
0,
ROIContourData.ROINumber
);
ROIContourData.colorArray = [...color];
}
}
function _setROIContourRTROIObservations(
ROIContourData,
RTROIObservationsSequence,
ROINumber
) {
const RTROIObservations = RTROIObservationsSequence.find(
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
);
if (RTROIObservations) {
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
const {
ObservationNumber,
ROIObservationDescription,
RTROIInterpretedType,
ROIInterpreter,
} = RTROIObservations;
ROIContourData.RTROIObservations = {
ObservationNumber,
ROIObservationDescription,
RTROIInterpretedType,
ROIInterpreter,
};
}
}
function _setToolEnabledIfNotEnabled(toolName) {
cornerstone.getEnabledElements().forEach(enabledElement => {
const { element } = enabledElement;
const tool = cornerstoneTools.getToolForElement(element, toolName);
if (tool.mode !== 'enabled') {
// If not already active or passive, set passive so contours render.
cornerstoneTools.setToolEnabled(toolName);
}
cornerstone.updateImage(element);
});
}
function _getOrCreateImageIdSpecificToolData(toolState, imageId, toolName) {
if (toolState.hasOwnProperty(imageId) === false) {
toolState[imageId] = {};
}
const imageIdToolState = toolState[imageId];
// If we don't have tool state for this type of tool, add an empty object
if (imageIdToolState.hasOwnProperty(toolName) === false) {
imageIdToolState[toolName] = {
data: [],
};
}
return imageIdToolState[toolName].data;
}
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
imageIdSopInstanceUidPairsEntry =>
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry.imageId;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(
studies,
StudyInstanceUID,
SeriesInstanceUID
) {
const study = studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
);
const displaySets = study.displaySets.filter(set => {
return set.SeriesInstanceUID === SeriesInstanceUID;
});
if (displaySets.length > 1) {
console.warn(
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
);
// TODO -> We could make check the instance list and see if any match?
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
// ^ Will that even happen?
}
const referencedDisplaySet = displaySets[0];
return referencedDisplaySet.images.map(image => {
return {
imageId: image.getImageId(),
sopInstanceUID: image.getSOPInstanceUID(),
};
});
}
@@ -0,0 +1,93 @@
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
import TOOL_NAMES from '../utils/toolNames';
// Cornerstone 3rd party dev kit imports
const draw = importInternal('drawing/draw');
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
const getNewContext = importInternal('drawing/getNewContext');
const BaseTool = importInternal('base/BaseTool');
/**
* @class RTStructDisplayTool - Renders RTSTRUCT data in a read only manner (i.e. as an overlay).
* @extends cornerstoneTools.BaseTool
*/
export default class RTStructDisplayTool extends BaseTool {
constructor(props = {}) {
const defaultProps = {
mixins: ['enabledOrDisabledBinaryTool'],
name: TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL,
};
const initialProps = Object.assign(defaultProps, props);
super(initialProps);
this._rtStructModule = cornerstoneTools.getModule('rtstruct');
}
renderToolData(evt) {
const eventData = evt.detail;
const rtstructModule = this._rtStructModule;
const toolState = getToolState(evt.currentTarget, this.name);
if (!toolState) {
return;
}
const { lineWidth, opacity } = rtstructModule.configuration;
// We have tool data for this element - iterate over each one and draw it
const context = getNewContext(eventData.canvasContext.canvas);
for (let i = 0; i < toolState.data.length; i++) {
const data = toolState.data[i];
const structureSet = rtstructModule.getters.structureSet(
data.structureSetSeriesInstanceUid
);
// Don't render if entire StructureSet is hidden.
if (structureSet.visible === false) {
continue;
}
const ROIContourData = rtstructModule.getters.ROIContour(
data.structureSetSeriesInstanceUid,
data.ROINumber
);
// Don't render if ROIContour is hidden.
if (ROIContourData.visible === false) {
continue;
}
const points = data.handles.points;
if (!points.length) {
continue;
}
const colorArray = ROIContourData.colorArray;
const color = `rgba(${colorArray[0]},${colorArray[1]},${
colorArray[2]
},${opacity})`;
lineWidth;
draw(context, context => {
drawJoinedLines(
context,
eventData.element,
points[points.length - 1],
points,
{
color,
lineWidth,
}
);
});
}
}
}
@@ -0,0 +1,67 @@
import cornerstoneTools from 'cornerstone-tools';
import TOOL_NAMES from '../../../utils/toolNames';
const globalImageIdSpecificToolStateManager =
cornerstoneTools.globalImageIdSpecificToolStateManager;
/**
* getImageIdOfCenterFrameOfROIContour - Returns the imageId nearest to the center of the
* volumes extent in the stack's Z direction which contains one of the ROIContour's Contours.
* @param {string} structureSetSeriesInstanceUid The SeriesInstanceUID of the structure set.
* @param {number} ROINumber The ROINumber of the region.
* @param {string} imageIdsInStack The stack of imageIds.
*
* @returns The imageId
*/
export default function getImageIdOfCenterFrameOfROIContour(
structureSetSeriesInstanceUid,
ROINumber,
imageIdsInStack
) {
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
const imageIdIndicies = [];
for (let i = 0; i < imageIdsInStack.length; i++) {
const imageId = imageIdsInStack[i];
const imageIdSpecificToolState = toolState[imageId];
if (
!imageIdSpecificToolState ||
!imageIdSpecificToolState[rtStructDisplayToolName] ||
!imageIdSpecificToolState[rtStructDisplayToolName].data
) {
continue;
}
const toolData = imageIdSpecificToolState[rtStructDisplayToolName].data;
if (
_toolDataContainsROIContour(
toolData,
structureSetSeriesInstanceUid,
ROINumber
)
) {
imageIdIndicies.push(i);
}
}
const centerImageIdIndex =
imageIdIndicies[Math.floor(imageIdIndicies.length / 2)];
return imageIdsInStack[centerImageIdIndex];
}
function _toolDataContainsROIContour(
toolData,
structureSetSeriesInstanceUid,
ROINumber
) {
return !!toolData.some(
toolDataI =>
toolDataI.structureSetSeriesInstanceUid ===
structureSetSeriesInstanceUid && toolDataI.ROINumber === ROINumber
);
}
@@ -0,0 +1,17 @@
/**
* structureSetReferencesSeriesInstanceUid - Returns true if the structure set
* references the given SeriesInstanceUID.
* @param {*} StructureSet
* @param {*} SeriesInstanceUID
*/
export default function structureSetReferencesSeriesInstanceUid(
StructureSet,
SeriesInstanceUID
) {
const { referencedSeriesSequence } = StructureSet;
return referencedSeriesSequence.some(
referencedSeries =>
referencedSeries.SeriesInstanceUID ===
SeriesInstanceUID
);
}
Loaded 100 of 498 files, more files were not shown because too many files have changed in this diff. Show more