Compare commits

...
Author SHA1 Message Date
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
323 changed files with 13130 additions and 12592 deletions

No files matched your search

+4 -1
View File
@@ -28,5 +28,8 @@
],
"prettier.disableLanguages": ["html"],
"prettier.endOfLine": "lf",
"workbench.colorCustomizations": {}
"workbench.colorCustomizations": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
+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'],
},
@@ -5,7 +5,7 @@ An extension can register a SOP Class Handler Module by defining a
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,
};
},
+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)
...
+8 -6
View File
@@ -21,7 +21,6 @@ export default {
/**
* MODULE GETTERS
*/
getViewportModule() {
return '... react component ...';
},
@@ -64,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',
@@ -78,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,
};
},
@@ -98,6 +97,9 @@ const panelModule = {
icon: 'th-list',
label: 'Segments',
target: 'segment-panel',
isDisabled: studies => {
return false;
},
},
],
components: [
+74
View File
@@ -3,6 +3,80 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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
+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"
// ]
};
+7 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.2.2",
"version": "2.5.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.12.0",
"cornerstone-tools": "4.12.5",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.8.2",
"dcmjs": "^0.10.1",
"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();
+24 -17
View File
@@ -74,10 +74,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) {
@@ -245,7 +241,20 @@ const commandsModule = ({ servicesManager }) => {
},
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);
}
},
};
const definitions = {
@@ -334,30 +343,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;
+122 -30
View File
@@ -3,27 +3,8 @@ import { SimpleDialog } from '@ohif/ui';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import merge from 'lodash.merge';
import queryString from 'query-string';
import initCornerstoneTools from './initCornerstoneTools.js';
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 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,
@@ -80,7 +58,6 @@ export default function init({ servicesManager, configuration }) {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
@@ -89,7 +66,6 @@ export default function init({ servicesManager, configuration }) {
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
segmentation: [csTools.BrushTool],
other: [
csTools.PanTool,
csTools.ZoomTool,
@@ -107,6 +83,9 @@ export default function init({ servicesManager, configuration }) {
tools.push(...toolsGroupedByType[toolsGroup])
);
/* Measurement Service */
_connectToolsToMeasurementService(MeasurementService);
/* Add extension tools configuration here. */
const internalToolsConfig = {
ArrowAnnotate: {
@@ -182,3 +161,116 @@ export default function init({ servicesManager, configuration }) {
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);
}
);
});
}
);
};
+1 -10
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',
@@ -245,7 +236,7 @@ const definitions = [
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);
});
});
});
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.1.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.10.1",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
@@ -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,
};
},
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.7",
"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
@@ -34,18 +34,18 @@ class DicomMicroscopyViewport extends Component {
.then(instances => {
const promises = [];
for (let i = 0; i < instances.length; i++) {
const sopInstanceUID = instances[i]['00080018']['Value'][0];
const sopInstanceUid = instances[i]['00080018']['Value'][0];
const retrieveInstanceOptions = {
studyInstanceUID: displaySet.studyInstanceUid,
seriesInstanceUID: displaySet.seriesInstanceUid,
sopInstanceUID,
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 +56,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 });
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-pdf",
"version": "1.0.1",
"version": "1.0.2",
"description": "OHIF extension for Dicom PDF",
"author": "OHIF",
"license": "MIT",
+2 -2
View File
@@ -101,9 +101,9 @@ class DicomPDFViewport extends Component {
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');
}
@@ -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,
};
},
@@ -33,6 +33,7 @@ class OHIFDicomPDFViewport extends Component {
componentDidMount() {
const { displaySet, studies } = this.props.viewportData;
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => this.setState({ byteArray: new Uint8Array(data) }),
error => {
@@ -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,33 @@
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',
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,
}),
],
});
};
@@ -0,0 +1,11 @@
# 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 (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)
+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.
+65
View File
@@ -0,0 +1,65 @@
# @ohif/dicom-segmentation
![npm (scoped)](https://img.shields.io/npm/v/@ohif/dicom-segmentation.svg?style=flat-square)
This extension adds support to view and navigate the segments available in the DICOM SEG,
allowing control over visibility of a given segment and providing metadata about the segment(s).
<!-- TODO: Simple image or GIF? -->
#### Index
Extension Id: `com.ohif.dicom-segmentation`
## Tool Configuration
Tools can be configured through extension configuration using the tools key:
```js
...
cornerstoneExtensionConfig: {
tools: {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
},
},
},
},
...
```
## 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
- [cornerstonejs/react-cornerstone-viewport][react-viewport]
- [cornerstonejs/cornerstoneTools][cornerstone-tools]
- [cornerstonejs/cornerstone][cornerstone]
<!--
Links
-->
<!-- prettier-ignore-start -->
[docs-commands]: https://www.com
[docs-hotkeys]: https://www.com
[docs-userprefs]: htt
[react-viewport]: https://github.com/cornerstonejs/react-cornerstone-viewport
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
[cornerstone]: https://github.com/cornerstonejs/cornerstone
<!-- prettier-ignore-end -->
@@ -0,0 +1,43 @@
{
"name": "@ohif/extension-dicom-segmentation",
"version": "0.1.0",
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
"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.12.5",
"dcmjs": "^0.6.1",
"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,71 @@
import { MODULE_TYPES, utils } from '@ohif/core';
import loadSegmentation from './loadSegmentation';
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
DICOM_SEG: '1.2.840.10008.5.1.4.1.1.66.4',
};
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 OHIFDicomSegSopClassHandler = {
id: 'OHIFDicomSegSopClassHandler',
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;
const segDisplaySet = {
Modality: 'SEG',
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,
};
segDisplaySet.load = function(referencedDisplaySet, studies) {
return loadSegmentation(
segDisplaySet,
referencedDisplaySet,
studies
).catch(error => {
segDisplaySet.isLoaded = false;
throw new Error(error);
});
};
return segDisplaySet;
},
};
export default OHIFDicomSegSopClassHandler;
@@ -0,0 +1,41 @@
.dcmseg-brush-color-selector .selector-buttons 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;
}
.dcmseg-brush-color-selector .selector-buttons button:hover,
.dcmseg-brush-color-selector .selector-buttons button:active,
.dcmseg-brush-color-selector .selector-buttons button.active {
background-color: var(--active-color);
border-color: var(--ui-border-color-active);
color: #fff;
}
.dcmseg-brush-color-selector .selector-buttons {
margin-left: 10px;
display: flex;
flex-direction: row;
align-items: center;
}
.dcmseg-brush-color-selector .selector-buttons button:last-of-type {
margin-left: 5px;
}
.dcmseg-brush-color-selector .selector-active-segment {
border-radius: 100%;
width: 32px;
height: 32px;
text-align: center;
line-height: 32px;
}
@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import './BrushColorSelector.css';
const BrushColorSelector = ({ defaultColor, index, onNext, onPrev }) => (
<div className="dcmseg-brush-color-selector">
<div
className="selector-active-segment"
style={{ backgroundColor: defaultColor }}
>
{index}
</div>
<div className="selector-buttons">
<button onClick={onPrev}>
Previous
</button>
<button onClick={onNext}>
Next
</button>
</div>
</div>
);
BrushColorSelector.propTypes = {
defaultColor: PropTypes.string.isRequired,
index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
onNext: PropTypes.func.isRequired,
onPrev: PropTypes.func.isRequired,
};
export default BrushColorSelector;
@@ -0,0 +1,4 @@
.dcmseg-brush-radius label {
display: block;
margin-bottom: 8px;
}
@@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Range } from '@ohif/ui';
import './BrushRadius.css';
const BrushRadius = ({ value, onChange, min, max, step }) => (
<div className="dcmseg-brush-radius">
<label htmlFor="dcmseg-brush-radius">Brush Radius</label>
<Range
value={value}
min={min}
max={max}
step={step}
onChange={onChange}
id="dcmseg-brush-radius"
/>
</div>
);
BrushRadius.propTypes = {
value: PropTypes.number.isRequired,
onChange: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
};
BrushRadius.defaultProps = {
onChange: () => { },
min: 1,
max: 50,
step: 1,
};
export default BrushRadius;
@@ -0,0 +1,64 @@
.dcmseg-segment-item .segment-color-section {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.dcmseg-segment-item .segment-color {
margin-top: 8px;
border-radius: 100%;
width: 20px;
height: 20px;
}
.dcmseg-segment-item .segment-label {
overflow: hidden;
max-width: calc(100% - 50px);
text-overflow: ellipsis;
overflow-wrap: normal;
white-space: nowrap;
}
.dcmseg-segment-item .segment-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%;
}
.dcmseg-segment-item.selected .segment-actions {
height: 35px;
visibility: visible;
}
.dcmseg-segment-item .segment-actions {
margin-left: -1px;
background-color: var(--ui-gray-darker);
height: 0;
overflow: hidden;
transition: all 0.3s ease;
visibility: hidden;
padding-left: 14px;
}
.dcmseg-segment-item .segment-actions .btnAction {
background-color: transparent;
border: none;
color: var(--default-color);
cursor: pointer;
line-height: 35px;
height: 35px;
transition: all 0.3s ease;
}
.dcmseg-segment-item .segment-actions .btnAction:hover, .dcmseg-segment-item .segment-actions .btnAction:active {
color: var(--text-primary-color);
}
.dcmseg-segment-item .segment-actions .btnAction i {
margin-right: 4px;
}
@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TableListItem, Icon } from '@ohif/ui';
import './SegmentItem.css';
const ColoredCircle = ({ color }) => {
return (
<div
className="segment-color"
style={{ backgroundColor: `rgba(${color.join(',')})` }}
></div>
);
};
ColoredCircle.propTypes = {
color: PropTypes.array.isRequired,
};
const SegmentItem = ({ index, label, onClick, itemClass, color }) => (
<div className="dcmseg-segment-item">
<TableListItem
key={index}
itemKey={index}
itemIndex={index}
itemClass={itemClass}
itemMeta={<ColoredCircle color={color} />}
itemMetaClass="segment-color-section"
onItemClick={onClick}
>
<div>
<div className="segment-label" style={{ marginBottom: 4 }}>
{label}
</div>
{false && <div className="segment-info">{'...'}</div>}
{false && (
<div className="segment-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>
);
SegmentItem.propTypes = {
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
itemClass: PropTypes.string,
color: PropTypes.array.isRequired,
};
SegmentItem.defaultProps = {
itemClass: '',
onClick: () => { }
};
export default SegmentItem;
@@ -0,0 +1,25 @@
.dcmseg-segmentation-item {
display: flex;
justify-content: start;
margin: 0;
}
.dcmseg-segmentation-item .segmentation-meta {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.dcmseg-segmentation-item .segmentation-meta-title {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
color: white;
max-width: calc(100% - 30px);
}
.dcmseg-segmentation-item .segmentation-meta-description {
font-size: 12px;
color: var(--text-secondary-color);
}
@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import './SegmentationItem.css';
const SegmentationItem = ({ onClick, title, description }) => {
return (
<li className="dcmseg-segmentation-item" onClick={onClick}>
<div className="segmentation-meta">
<div className="segmentation-meta-title">{title}</div>
<div className="segmentation-meta-description">{description}</div>
</div>
</li>
);
};
SegmentationItem.propTypes = {
onClick: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
};
SegmentationItem.defaultProps = {
description: ''
};
export default SegmentationItem;
@@ -0,0 +1,61 @@
.dcmseg-segmentation-panel {
color: white;
min-height: 200px;
background-color: var(--primary-background-color);
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.dcmseg-segmentation-panel h3 {
padding-left: 20px;
margin-top: 0;
}
.dcmseg-segmentation-panel .segmentations {
padding: 0 20px 20px 20px;
}
.dcmseg-segmentation-panel .cog-icon {
margin: 20px 20px 0 0;
align-self: flex-end;
color: var(--default-color);
cursor: pointer;
}
.dcmseg-segmentation-panel .cog-icon:hover {
color: var(--hover-color);
}
.dcmseg-segmentation-panel .segmentation-icon {
margin-right: 8px;
margin-top: 12px;
min-width: 14px;
}
.dcmseg-segmentation-panel .selector-form {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 20px;
height: 150px;
}
.dcmseg-segmentation-panel .selector-form>div {
width: 100%;
}
.dcmseg-segmentation-panel .selector-form>div:first-of-type {
display: flex;
flex-direction: row;
justify-content: center;
}
.dcmseg-segmentation-panel .tableList .tableListHeader .numberOfItems {
float: unset;
max-width: unset;
flex: unset;
margin-right: 16px;
}
@@ -0,0 +1,475 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import moment from 'moment';
import { utils, log } from '@ohif/core';
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
import {
BrushColorSelector,
BrushRadius,
SegmentationItem,
SegmentItem,
SegmentationSelect,
} from '../index';
import './SegmentationPanel.css';
import SegmentationSettings from '../SegmentationSettings/SegmentationSettings';
const { studyMetadataManager } = utils;
const refreshViewport = () => {
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
};
/**
* SegmentationPanel component
*
* @param {Object} props
* @param {Array} props.studies
* @param {Array} props.viewports - viewportSpecificData
* @param {number} props.activeIndex - activeViewportIndex
* @returns component
*/
const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
/*
* TODO: wrap get/set interactions with the cornerstoneTools
* store with context to make these kind of things less blurry.
*/
const segmentationModule = cornerstoneTools.getModule('segmentation');
const { configuration } = segmentationModule;
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
const [brushRadius, setBrushRadius] = useState(DEFAULT_BRUSH_RADIUS);
/* TODO: We shouldn't hardcode this color, in the future the SEG may set the colorLUT to whatever it wants. */
const [brushColor, setBrushColor] = useState('rgba(221, 85, 85, 1)');
const [selectedSegment, setSelectedSegment] = useState();
const [showSegSettings, setShowSegSettings] = useState(false);
const [selectedSegmentation, setSelectedSegmentation] = useState();
const viewport = viewports[activeIndex];
const {
StudyInstanceUID,
SeriesInstanceUID,
displaySetInstanceUID,
} = viewport;
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
const firstImageId = studyMetadata.getFirstImageId(displaySetInstanceUID);
/* CornerstoneTools */
const [brushStackState, setBrushStackState] = useState(
segmentationModule.state.series[firstImageId]
);
useEffect(() => {
setShowSegSettings(showSegSettings && !isOpen);
}, [isOpen]);
useEffect(() => {
setBrushStackState(segmentationModule.state.series[firstImageId]);
}, [studies, viewports, activeIndex, firstImageId]);
useEffect(() => {
if (brushStackState) {
setSelectedSegmentation(brushStackState.activeLabelmapIndex);
}
const labelmapModifiedHandler = event => {
log.warn('labelmap modified', event);
setBrushStackState(segmentationModule.state.series[firstImageId]);
};
/*
* These are specific to each element;
* Need to iterate cornerstone-tools tracked enabled elements?
* Then only care about the one tied to active viewport?
*/
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
enabledElement.addEventListener(
'cornerstonetoolslabelmapmodified',
labelmapModifiedHandler
)
);
return () => {
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
enabledElement.removeEventListener(
'cornerstonetoolslabelmapmodified',
labelmapModifiedHandler
)
);
};
});
if (!brushStackState) {
return null;
}
const labelmap3D =
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
/*
* 2. UseEffect to update state? or to a least trigger a re-render
* 4. Toggle visibility of labelmap?
* 5. Toggle visibility of seg?
*
* If the port is cornerstone, just need to call a re-render.
* If the port is vtkjs, its a bit more tricky as we now need to create a new
*/
const getLabelmapList = () => {
/* Get list of SEG labelmaps specific to active viewport (reference series) */
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
StudyInstanceUID,
SeriesInstanceUID
);
return referencedSegDisplaysets.map((displaySet, index) => {
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
/* Map to display representation */
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
const isActiveLabelmap =
labelmapIndex === brushStackState.activeLabelmapIndex;
const displayDate = date.format('ddd, MMM Do YYYY');
const displayTime = date.format('h:mm:ss a');
const displayDescription = displaySet.SeriesDescription;
return {
value: labelmapIndex,
title: displayDescription,
description: displayDate,
onClick: async () => {
const activatedLabelmapIndex = await _setActiveLabelmap(
viewport,
studies,
displaySet,
firstImageId,
brushStackState.activeLabelmapIndex
);
setSelectedSegmentation(activatedLabelmapIndex);
},
};
});
};
const labelmapList = getLabelmapList();
const segmentList = [];
if (labelmap3D) {
/*
* Newly created segments have no `meta`
* So we instead build a list of all segment indexes in use
* Then find any associated metadata
*/
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
.reduce((acc, labelmap2D) => {
if (labelmap2D) {
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
for (let i = 0; i < segmentIndexes.length; i++) {
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
acc.push(segmentIndexes[i]);
}
}
}
return acc;
}, [])
.sort((a, b) => a - b);
const colorLutTable =
segmentationModule.state.colorLutTables[labelmap3D.colorLUTIndex];
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
const segmentIndex = uniqueSegmentIndexes[i];
const color = colorLutTable[segmentIndex];
let segmentLabel = '(unlabeled)';
let segmentNumber = segmentIndex;
/* Meta */
if (hasLabelmapMeta) {
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
if (segmentMeta) {
segmentNumber = segmentMeta.SegmentNumber;
segmentLabel = segmentMeta.SegmentLabel;
}
}
const sameSegment = selectedSegment === segmentNumber;
const setCurrentSelectedSegment = () => {
_setActiveSegment(
firstImageId,
segmentNumber,
labelmap3D.activeSegmentIndex
);
setSelectedSegment(sameSegment ? null : segmentNumber);
};
segmentList.push(
<SegmentItem
key={segmentNumber}
itemClass={`segment-item ${sameSegment && 'selected'}`}
onClick={setCurrentSelectedSegment}
label={segmentLabel}
index={segmentNumber}
color={color}
/>
);
}
/*
* Let's iterate over segmentIndexes ^ above
* If meta has a match, use it to show info
* If now, add "no-meta" class
* Show default name
*/
}
const updateBrushSize = evt => {
const updatedRadius = Number(evt.target.value);
if (updatedRadius !== brushRadius) {
setBrushRadius(updatedRadius);
segmentationModule.setters.radius(updatedRadius);
}
};
const decrementSegment = event => {
event.preventDefault();
if (labelmap3D.activeSegmentIndex > 1) {
labelmap3D.activeSegmentIndex--;
}
setSelectedSegment(labelmap3D.activeSegmentIndex);
updateActiveSegmentColor();
};
const incrementSegment = event => {
event.preventDefault();
labelmap3D.activeSegmentIndex++;
setSelectedSegment(labelmap3D.activeSegmentIndex);
updateActiveSegmentColor();
};
const updateActiveSegmentColor = () => {
const color = getActiveSegmentColor();
setBrushColor(color);
};
const getActiveSegmentColor = () => {
if (!brushStackState) {
return 'rgba(255, 255, 255, 1)';
}
const colorLutTable =
segmentationModule.state.colorLutTables[labelmap3D.colorLUTIndex];
const color = colorLutTable[labelmap3D.activeSegmentIndex];
return `rgba(${color.join(',')})`;
};
const updateConfiguration = newConfiguration => {
/* Supported configuration */
configuration.renderFill = newConfiguration.renderFill;
configuration.renderOutline = newConfiguration.renderOutline;
configuration.shouldRenderInactiveLabelmaps =
newConfiguration.shouldRenderInactiveLabelmaps;
configuration.fillAlpha = newConfiguration.fillAlpha;
configuration.outlineAlpha = newConfiguration.outlineAlpha;
configuration.outlineWidth = newConfiguration.outlineWidth;
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
refreshViewport();
};
if (showSegSettings) {
return (
<SegmentationSettings
configuration={configuration}
onBack={() => setShowSegSettings(false)}
onChange={updateConfiguration}
/>
);
} else {
return (
<div className="dcmseg-segmentation-panel">
<Icon
className="cog-icon"
name="cog"
width="25px"
height="25px"
onClick={() => setShowSegSettings(true)}
/>
{false && (
<form className="selector-form">
<BrushColorSelector
defaultColor={brushColor}
index={labelmap3D.activeSegmentIndex}
onNext={incrementSegment}
onPrev={decrementSegment}
/>
<BrushRadius
value={brushRadius}
onChange={updateBrushSize}
min={configuration.minRadius}
max={configuration.maxRadius}
/>
</form>
)}
<h3>Segmentations</h3>
<div className="segmentations">
<SegmentationSelect
value={
labelmapList.find(i => i.value === selectedSegmentation) || null
}
formatOptionLabel={SegmentationItem}
options={labelmapList}
/>
</div>
<ScrollableArea>
<TableList
customHeader={<SegmentsHeader count={segmentList.length} />}
>
{segmentList}
</TableList>
</ScrollableArea>
</div>
);
}
};
SegmentationPanel.propTypes = {
/*
* An object, with int index keys?
* Maps to: state.viewports.viewportSpecificData, in `viewer`
* Passed in MODULE_TYPES.PANEL when specifying component in viewer
*/
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,
};
SegmentationPanel.defaultProps = {};
/**
* Returns SEG Displaysets that reference the target series, sorted by dateTime
*
* @param {string} StudyInstanceUID
* @param {string} SeriesInstanceUID
* @returns Array
*/
const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
/* Referenced DisplaySets */
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: SeriesInstanceUID,
Modality: 'SEG',
});
/* Sort */
referencedDisplaysets.sort((a, b) => {
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
return aNumber - bNumber;
});
return referencedDisplaysets;
};
/**
*
*
* @param {*} viewportSpecificData
* @param {*} studies
* @param {*} displaySet
* @param {*} firstImageId
* @param {*} activeLabelmapIndex
* @returns
*/
const _setActiveLabelmap = async (
viewportSpecificData,
studies,
displaySet,
firstImageId,
activeLabelmapIndex
) => {
if (displaySet.labelmapIndex === activeLabelmapIndex) {
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
return;
}
if (!displaySet.isLoaded) {
// What props does this expect `viewportSpecificData` to have?
// TODO: Should this return the `labelmapIndex`?
await displaySet.load(viewportSpecificData, studies);
}
const { state } = cornerstoneTools.getModule('segmentation');
const brushStackState = state.series[firstImageId];
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
refreshViewport();
return displaySet.labelmapIndex;
};
/**
*
* @param {*} firstImageId
* @param {*} activeSegmentIndex
* @returns
*/
const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex) => {
if (segmentIndex === activeSegmentIndex) {
log.info(`${activeSegmentIndex} is already the active segment`);
return;
}
const { state } = cornerstoneTools.getModule('segmentation');
const brushStackState = state.series[firstImageId];
const labelmap3D =
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
labelmap3D.activeSegmentIndex = segmentIndex;
refreshViewport();
return segmentIndex;
};
const SegmentsHeader = ({ count }) => {
return (
<React.Fragment>
<div className="tableListHeaderTitle">Segments</div>
<div className="numberOfItems">{count}</div>
</React.Fragment>
);
};
export default SegmentationPanel;
@@ -0,0 +1,55 @@
import React from 'react';
import Select from 'react-select';
const SegmentationSelect = ({ value, formatOptionLabel, options }) => (
<Select
value={value}
formatOptionLabel={formatOptionLabel}
styles={segmentationSelectStyles}
options={options}
/>
);
const computedstyle = getComputedStyle(document.body);
const uiGrayDarker = computedstyle.getPropertyValue('--ui-gray-darker');
const activeColor = computedstyle.getPropertyValue('--active-color');
const defaultColor = computedstyle.getPropertyValue('--default-color');
const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
const segmentationSelectStyles = {
control: (base, state) => ({
...base,
cursor: 'pointer',
background: uiGrayDarker,
borderRadius: state.isFocused ? '5px 5px 5px 5px' : 5,
borderColor: state.isFocused ? activeColor : defaultColor,
boxShadow: state.isFocused ? null : null,
minHeight: '50px',
'&:hover': {
borderColor: activeColor,
},
}),
menu: base => ({
...base,
borderRadius: 5,
background: uiGrayDarker,
}),
option: (base, state) => ({
...base,
cursor: 'pointer',
'&:first-of-type': {
borderTopLeftRadius: 5,
borderTopRightRadius: 5,
},
'&:last-of-type': {
borderBottomLeftRadius: 5,
borderBottomRightRadius: 5,
},
background: state.isSelected ? uiGrayDark : uiGrayDarker,
'&:hover': {
background: uiGrayDark,
},
}),
};
export default SegmentationSelect;
@@ -0,0 +1,95 @@
.dcmseg-segmentation-settings {
display: flex;
flex-direction: column;
padding: 20px;
}
.dcmseg-segmentation-settings .range label {
display: block;
margin-bottom: 8px;
}
.dcmseg-segmentation-settings .settings-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-bottom: 20px;
}
.dcmseg-segmentation-settings .settings-title h3 {
color: var(--text-primary-color);
margin: 0;
padding: 0;
}
.dcmseg-segmentation-settings .settings-group {
display: flex;
flex-direction: column;
color: var(--text-primary-color);
font-size: 12px;
}
.dcmseg-segmentation-settings .settings-group .range {
display: flex;
padding: 20px 15px;
}
.dcmseg-segmentation-settings .settings-group .range:not(:last-of-type) {
padding-bottom: 0;
}
.dcmseg-segmentation-settings .settings-group .range label {
margin: 0;
display: unset;
min-width: 40px;
max-width: 40px;
}
.dcmseg-segmentation-settings .settings-group .range input {
width: 100%;
padding: 0 20px 0 20px;
}
.dcmseg-segmentation-settings .custom-check label {
font-weight: bold;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid black;
padding: 10px 15px;
}
.dcmseg-segmentation-settings .custom-check input {
width: 15px;
height: 15px;
}
.dcmseg-segmentation-settings .settings-group {
background-color: var(--ui-gray-dark);
border-radius: 5px;
}
.dcmseg-segmentation-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;
}
.dcmseg-segmentation-settings .return-button:hover,
.dcmseg-segmentation-settings .return-button:active,
.dcmseg-segmentation-settings .return-button.active {
background-color: var(--active-color);
border-color: var(--ui-border-color-active);
color: #fff;
}
@@ -0,0 +1,178 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Range } from '@ohif/ui';
import './SegmentationSettings.css';
const SegmentationSettings = ({ configuration, onBack, onChange }) => {
const [state, setState] = useState({
renderFill: configuration.renderFill,
renderOutline: configuration.renderOutline,
shouldRenderInactiveLabelmaps: configuration.shouldRenderInactiveLabelmaps,
fillAlpha: configuration.fillAlpha,
outlineAlpha: configuration.outlineAlpha,
outlineWidth: configuration.outlineWidth,
fillAlphaInactive: configuration.fillAlphaInactive,
outlineAlphaInactive: configuration.outlineAlphaInactive
});
useEffect(() => {
onChange(state);
}, [state]);
const check = field => {
setState(state => ({ ...state, [field]: !state[field] }));
};
const save = (field, value) => {
setState(state => ({ ...state, [field]: value }));
};
const toFloat = value => parseFloat(value / 100).toFixed(2);
return (
<div className="dcmseg-segmentation-settings">
<div className="settings-title">
<h3>Segmentations Settings</h3>
<button className="return-button" onClick={onBack}>
Back
</button>
</div>
<div
className="settings-group"
style={{ marginBottom: state.renderFill ? 15 : 0 }}
>
<CustomCheck
label="Segment Fill"
checked={state.renderFill}
onChange={() => check('renderFill')}
/>
{state.renderFill && (
<CustomRange
label="Opacity"
step={1}
min={0}
max={100}
value={state.fillAlpha * 100}
onChange={event => save('fillAlpha', toFloat(event.target.value))}
showPercentage
/>
)}
</div>
<div
className="settings-group"
style={{ marginBottom: state.renderOutline ? 15 : 0 }}
>
<CustomCheck
label="Segment Outline"
checked={state.renderOutline}
onChange={() => check('renderOutline')}
/>
{state.renderOutline && (
<>
<CustomRange
value={state.outlineAlpha * 100}
label="Opacity"
showPercentage
step={1}
min={0}
max={100}
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
/>
<CustomRange
value={state.outlineWidth}
label="Width"
showValue
step={1}
min={0}
max={5}
onChange={event => save('outlineWidth', parseInt(event.target.value))}
/>
</>
)}
</div>
{(state.renderFill || state.renderOutline) && (
<div
className="settings-group"
style={{ marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0 }}
>
<CustomCheck
label="Render inactive segmentations"
checked={state.shouldRenderInactiveLabelmaps}
onChange={() => check('shouldRenderInactiveLabelmaps')}
/>
{state.shouldRenderInactiveLabelmaps && (
<>
{state.renderFill && (
<CustomRange
label="Fill Opacity"
showPercentage
step={1}
min={0}
max={100}
value={state.fillAlphaInactive * 100}
onChange={event => save('fillAlphaInactive', toFloat(event.target.value))}
/>
)}
{state.renderOutline && (
<CustomRange
label="Outline Opacity"
showPercentage
step={1}
min={0}
max={100}
value={state.outlineAlphaInactive * 100}
onChange={event => save('outlineAlphaInactive', toFloat(event.target.value))}
/>
)}
</>
)}
</div>
)}
</div>
);
};
const CustomCheck = ({ label, checked, onChange }) => {
return (
<div className="custom-check">
<label>
<span>{label}</span>
<input type="checkbox" checked={checked} onChange={onChange} />
</label>
</div>
);
};
const CustomRange = props => {
const { label, onChange } = props;
return (
<div className="range">
<label htmlFor="range">{label}</label>
<Range
{...props}
onChange={event => {
event.persist();
onChange(event);
}}
/>
</div>
);
};
SegmentationSettings.propTypes = {
configuration: PropTypes.shape({
renderFill: PropTypes.bool.isRequired,
renderOutline: PropTypes.bool.isRequired,
shouldRenderInactiveLabelmaps: PropTypes.bool.isRequired,
fillAlpha: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, /* TODO: why fillAlpha is string? */
outlineAlpha: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, /* TODO: why fillAlpha is string? */
outlineWidth: PropTypes.number.isRequired,
fillAlphaInactive: PropTypes.number.isRequired,
outlineAlphaInactive: PropTypes.number.isRequired,
}).isRequired,
onBack: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
export default SegmentationSettings;
@@ -0,0 +1,7 @@
export { default as BrushColorSelector } from './BrushColorSelector/BrushColorSelector';
export { default as BrushRadius } from './BrushRadius/BrushRadius';
export { default as SegmentationItem } from './SegmentationItem/SegmentationItem';
export { default as SegmentationSettings } from './SegmentationSettings/SegmentationSettings';
export { default as SegmentationPanel } from './SegmentationPanel/SegmentationPanel';
export { default as SegmentItem } from './SegmentItem/SegmentItem';
export { default as SegmentationSelect } from './SegmentationSelect';
@@ -0,0 +1,30 @@
import init from './init.js';
import toolbarModule from './toolbarModule.js';
import panelModule from './panelModule.js';
import sopClassHandlerModule from './OHIFDicomSegSopClassHandler.js';
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'com.ohif.dicom-segmentation',
/**
*
*
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
getToolbarModule({ servicesManager }) {
return toolbarModule;
},
getPanelModule({ servicesManager }) {
return panelModule;
},
getSopClassHandlerModule({ servicesManager }) {
return sopClassHandlerModule;
},
};
+20
View File
@@ -0,0 +1,20 @@
import csTools from 'cornerstone-tools';
/**
*
* @param {object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ servicesManager, configuration = {} }) {
const { BrushTool, SphericalBrushTool, CorrectionScissorsTool } = csTools;
const tools = [BrushTool, SphericalBrushTool, CorrectionScissorsTool];
tools.forEach(tool => csTools.addTool(tool));
csTools.addTool(BrushTool, {
name: 'BrushEraser',
configuration: {
alwaysEraseOnClick: true,
},
});
}
@@ -0,0 +1,115 @@
import OHIF from '@ohif/core';
import * as dcmjs from 'dcmjs';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
const { DicomLoaderService } = OHIF.utils;
export default async function loadSegmentation(
segDisplaySet,
referencedDisplaySet,
studies
) {
const { StudyInstanceUID } = referencedDisplaySet;
// Set here is loading is asynchronous.
// If this function throws its set back to false.
segDisplaySet.isLoaded = true;
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
segDisplaySet,
studies
);
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
const imageIds = _getImageIdsForDisplaySet(
studies,
StudyInstanceUID,
referencedDisplaySet.SeriesInstanceUID
);
const results = _parseSeg(segArrayBuffer, imageIds);
if (!results) {
throw new Error('Fractional segmentations are not yet supported');
}
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
const { setters } = cornerstoneTools.getModule('segmentation');
// TODO: Could define a color LUT based on colors in the SEG.
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
setters.labelmap3DByFirstImageId(
imageIds[0],
labelmapBuffer,
labelmapIndex,
segMetadata,
imageIds.length,
segmentsOnFrame
);
segDisplaySet.labelmapIndex = labelmapIndex;
}
function _getNextLabelmapIndex(firstImageId) {
const { state } = cornerstoneTools.getModule('segmentation');
const brushStackState = state.series[firstImageId];
let labelmapIndex = 0;
if (brushStackState) {
const { labelmaps3D } = brushStackState;
labelmapIndex = labelmaps3D.length;
for (let i = 0; i < labelmaps3D.length; i++) {
if (!labelmaps3D[i]) {
labelmapIndex = i;
break;
}
}
}
return labelmapIndex;
}
function _parseSeg(arrayBuffer, imageIds) {
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
imageIds,
arrayBuffer,
cornerstone.metaData
);
}
function _getImageIdsForDisplaySet(
studies,
StudyInstanceUID,
SeriesInstanceUID
) {
const study = studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
);
const displaySets = study.displaySets.filter(displaySet => {
return displaySet.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 => image.getImageId());
}
@@ -0,0 +1,39 @@
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
export default {
menuOptions: [
{
icon: 'list',
label: 'Segmentations',
target: 'segmentation-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 (series.Modality === 'SEG') {
return false;
}
}
}
}
return true;
},
},
],
components: [
{
id: 'segmentation-panel',
component: SegmentationPanel,
},
],
defaultContext: ['VIEWER'],
};
@@ -0,0 +1,60 @@
/* TODO: Creation tools (future release).
const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command',
SET_TOOL_ACTIVE: 'setToolActive',
BUILT_IN: 'builtIn',
};
*/
const definitions = []; /* TODO: Creation tools (future release). [
{
id: 'SegDropdown',
label: 'Segmentation',
icon: 'ellipse-circle',
buttons: [
{
id: 'Brush',
label: 'Brush',
icon: 'brush',
//
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'Brush' },
},
{
id: 'SphericalBrush',
label: 'Spherical',
icon: 'sphere',
//
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'SphericalBrush' },
},
{
id: 'CorrectionScissors',
label: 'Correction Scissors',
icon: 'scissors',
//
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'CorrectionScissors' },
},
{
id: 'BrushEraser',
label: 'Eraser',
icon: 'trash',
//
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setToolActive',
commandOptions: { toolName: 'BrushEraser' },
},
],
},
]; */
export default {
definitions,
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
};
@@ -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: 'OHIFExtLesionTracker',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
});
};
+22
View File
@@ -0,0 +1,22 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-lesion-tracker@0.1.0...@ohif/extension-lesion-tracker@0.2.0) (2020-02-10)
### Features
* Lesion tracker right panel ([#1428](https://github.com/OHIF/Viewers/issues/1428)) ([98a649b](https://github.com/OHIF/Viewers/commit/98a649b455ffc712938fc5035cdef40695e58440))
# 0.1.0 (2020-02-06)
### Features
* lesion-tracker extension ([#1420](https://github.com/OHIF/Viewers/issues/1420)) ([73e4409](https://github.com/OHIF/Viewers/commit/73e440968ce4699d081a9c9f2d21dd68095b3056))
+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.
+38
View File
@@ -0,0 +1,38 @@
# @ohif/extension-lesion-tracker
This project is an OHIF extension that can be used with the Core OHIF Platform,
either at runtime, or as an ES6 dependency, to create a medical image viewing
application similar to that of the [legacy Lesion
Tracker][legacy-lesion-tracker] viewer.
## About
...
## Scope
...
### Configuration
...
### Extensions
...
## Build & Deploy
...
## Funding/Support
...
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[legacy-lesion-tracker]: http://lesiontracker.ohif.org/studylist
<!-- prettier-ignore-end -->
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@ohif/extension-lesion-tracker",
"version": "0.2.0",
"description": "OHIF extension for Lesion Tracker",
"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",
"dev:lesion-tracker": "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",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6"
}
}
@@ -0,0 +1,60 @@
.MeasurementComparisonTable {
height: 100%;
display: flex;
flex-direction: column;
}
.MeasurementComparisonTable .displayTexts {
display: flex;
}
.MeasurementComparisonTable .displayTexts .measurementDisplayText {
flex: 1;
}
.MeasurementComparisonTable .measurementTableHeader {
display: flex;
position: relative;
padding-left: 38px;
}
.MeasurementComparisonTable .measurementTableHeader .warning-status {
left: 0;
top: 3px;
cursor: pointer;
width: 40px;
height: 100%;
display: flex;
align-items: center;
}
.MeasurementComparisonTable .measurementTableHeader .warning-status .warning-border {
padding: 2px 3px;
width: 30px;
height: 30px;
}
.MeasurementComparisonTable .measurementTableHeader .warning-status svg {
width: 20px;
height: 18px;
}
.MeasurementComparisonTable .measurementTableHeader .measurementTableHeaderItem {
position: relative;
flex: 1;
}
.MeasurementComparisonTable .generate-report {
background-color: #151a1f;
margin-top: 2px;
padding: 10px 0;
display: flex;
justify-content: center;
align-items: center;
}
.MeasurementComparisonTable .generate-report button {
font-size: 12px;
color: black;
background-color: var(--active-color);
}
@@ -0,0 +1,151 @@
import React from 'react';
import { MeasurementTable } from '@ohif/ui';
import './MeasurementComparisonTable.css';
const overallWarnings = {
warningList: [
'All measurements should have a location',
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
],
};
const measurements = [
{
measurementId: '125',
measurementNumber: '125',
itemNumber: 1,
label: '(No description)',
data: [{ displayText: '12.5 x 4.6' }],
},
{
measurementId: '124',
measurementNumber: '124',
itemNumber: 2,
label: '(No description)',
data: [{ displayText: '32.5 x 1.6' }],
},
{
measurementId: '123',
measurementNumber: '123',
itemNumber: 3,
hasWarnings: true,
warningList: [
'All measurements should have a location',
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
],
label: '(No description)',
data: [{ displayText: '5.5 x 9.2' }],
},
];
const additionalFindings = [
{
measurementId: '122',
measurementNumber: '122',
itemNumber: 1,
hasWarnings: true,
warningList: [
'All measurements should have a location',
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
],
label: '(No description)',
data: [{ displayText: '23.5 x 9.2' }],
},
{
measurementId: '121',
measurementNumber: '121',
itemNumber: 2,
hasWarnings: true,
warningList: [
'All measurements should have a location',
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
],
label: '(No description)',
data: [{ displayText: '11.2 x 9.2' }],
},
{
measurementId: '120',
measurementNumber: '120',
itemNumber: 3,
label: '(No description)',
data: [{ displayText: '2.9 x 9.2' }],
},
];
const currentCollections = [
{
selectorAction: () => { },
maxMeasurements: 3,
groupName: 'Measurements',
measurements: measurements,
},
{
selectorAction: () => { },
groupName: 'Additional Findings',
measurements: additionalFindings,
},
];
const comparisonColletions = [
{
selectorAction: () => { },
maxMeasurements: 3,
groupName: 'Measurements',
measurements: measurements,
},
{
selectorAction: () => { },
groupName: 'Additional Findings',
measurements: additionalFindings,
},
];
const comparisonCollections = currentCollections.map((group, index) => {
return {
...group,
measurements: group.measurements.map((measurement, measurementIndex) => {
const comparisonCollection = comparisonColletions[index].measurements;
if (measurementIndex < comparisonCollection.length) {
return {
...measurement,
data: [
...measurement.data,
...comparisonCollection[measurementIndex].data,
],
};
}
}),
};
});
const comparisonTimepoints = [
{
key: 'Current',
date: '10-Apr-18',
},
{
key: 'Comparison',
date: '15-Jun-18',
}
];
const MeasurementComparisonTable = () => {
return (
<div className="MeasurementComparisonTable">
<MeasurementTable
timepoints={comparisonTimepoints}
overallWarnings={overallWarnings}
measurementCollection={comparisonCollections}
onRelabelClick={() => { }}
onEditDescriptionClick={() => { }}
/>
<div className="generate-report">
<button className="btn btn-primary">Generate Report</button>
</div>
</div>
);
};
export default MeasurementComparisonTable;
+52
View File
@@ -0,0 +1,52 @@
import MeasurementComparisonTable from './components/MeasurementComparisonTable';
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'lesion-tracker',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getPanelModule({ servicesManager, commandsManager }) {
return {
menuOptions: [
{
icon: 'th-list',
label: 'Measurements',
target: 'lesion-tracker-panel',
},
],
components: [
{
id: 'lesion-tracker-panel',
component: MeasurementComparisonTable,
},
],
defaultContext: ['VIEWER']
};
},
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns Object
*/
getToolbarModule() {
return null;
},
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns Object
*/
getCommandsModule() {
return null;
},
};
+194 -319
View File
@@ -3,6 +3,134 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.2...@ohif/extension-vtk@1.3.3) (2020-03-24)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.1...@ohif/extension-vtk@1.3.2) (2020-03-23)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.0...@ohif/extension-vtk@1.3.1) (2020-03-17)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.24...@ohif/extension-vtk@1.3.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)
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.24](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.23...@ohif/extension-vtk@1.2.24) (2020-03-09)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.22...@ohif/extension-vtk@1.2.23) (2020-03-09)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.21...@ohif/extension-vtk@1.2.22) (2020-03-06)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.20...@ohif/extension-vtk@1.2.21) (2020-02-29)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.19...@ohif/extension-vtk@1.2.20) (2020-02-21)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.18...@ohif/extension-vtk@1.2.19) (2020-02-20)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.17...@ohif/extension-vtk@1.2.18) (2020-02-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.16...@ohif/extension-vtk@1.2.17) (2020-02-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.15...@ohif/extension-vtk@1.2.16) (2020-02-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.14...@ohif/extension-vtk@1.2.15) (2020-02-07)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.13...@ohif/extension-vtk@1.2.14) (2020-02-06)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.12...@ohif/extension-vtk@1.2.13) (2020-01-30)
@@ -340,621 +468,368 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.4...@ohif/extension-vtk@0.53.5) (2019-11-13)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.3...@ohif/extension-vtk@0.53.4) (2019-11-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.2...@ohif/extension-vtk@0.53.3) (2019-11-11)
### Bug Fixes
* mpr2d vtkjs viewport does not render if range is set to `NaN` values ([#1157](https://github.com/OHIF/Viewers/issues/1157)) ([84a0972](https://github.com/OHIF/Viewers/commit/84a097212babc0f98198b9ced1def9743973a5a8))
- mpr2d vtkjs viewport does not render if range is set to `NaN` values
([#1157](https://github.com/OHIF/Viewers/issues/1157))
([84a0972](https://github.com/OHIF/Viewers/commit/84a097212babc0f98198b9ced1def9743973a5a8))
## [0.53.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.1...@ohif/extension-vtk@0.53.2) (2019-11-08)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.0...@ohif/extension-vtk@0.53.1) (2019-11-08)
**Note:** Version bump only for package @ohif/extension-vtk
# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.32...@ohif/extension-vtk@0.53.0) (2019-11-06)
### Features
* modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1146](https://github.com/OHIF/Viewers/issues/1146) [#1142](https://github.com/OHIF/Viewers/issues/1142) [#1143](https://github.com/OHIF/Viewers/issues/1143) [#1110](https://github.com/OHIF/Viewers/issues/1110) [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1119](https://github.com/OHIF/Viewers/issues/1119)
- modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151))
([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)),
closes [#1086](https://github.com/OHIF/Viewers/issues/1086)
[#1116](https://github.com/OHIF/Viewers/issues/1116)
[#1116](https://github.com/OHIF/Viewers/issues/1116)
[#1146](https://github.com/OHIF/Viewers/issues/1146)
[#1142](https://github.com/OHIF/Viewers/issues/1142)
[#1143](https://github.com/OHIF/Viewers/issues/1143)
[#1110](https://github.com/OHIF/Viewers/issues/1110)
[#1086](https://github.com/OHIF/Viewers/issues/1086)
[#1116](https://github.com/OHIF/Viewers/issues/1116)
[#1119](https://github.com/OHIF/Viewers/issues/1119)
## [0.52.32](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.31...@ohif/extension-vtk@0.52.32) (2019-11-05)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.31](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.30...@ohif/extension-vtk@0.52.31) (2019-11-05)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.30](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.29...@ohif/extension-vtk@0.52.30) (2019-11-04)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.29](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.28...@ohif/extension-vtk@0.52.29) (2019-11-04)
### Bug Fixes
* 🐛 Upgrade react-vtkjs-viewport to fix volume orientation ([#1143](https://github.com/OHIF/Viewers/issues/1143)) ([1d86b1a](https://github.com/OHIF/Viewers/commit/1d86b1a102fc422053cff103de8c12ca6f437354))
- 🐛 Upgrade react-vtkjs-viewport to fix volume orientation
([#1143](https://github.com/OHIF/Viewers/issues/1143))
([1d86b1a](https://github.com/OHIF/Viewers/commit/1d86b1a102fc422053cff103de8c12ca6f437354))
## [0.52.28](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.27...@ohif/extension-vtk@0.52.28) (2019-11-04)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.27](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.26...@ohif/extension-vtk@0.52.27) (2019-10-31)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.26](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.25...@ohif/extension-vtk@0.52.26) (2019-10-30)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.25](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.24...@ohif/extension-vtk@0.52.25) (2019-10-29)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.24](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.23...@ohif/extension-vtk@0.52.24) (2019-10-29)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.22...@ohif/extension-vtk@0.52.23) (2019-10-28)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.21...@ohif/extension-vtk@0.52.22) (2019-10-28)
### Bug Fixes
* MIP styling ([#1109](https://github.com/OHIF/Viewers/issues/1109)) ([0d21cc6](https://github.com/OHIF/Viewers/commit/0d21cc6ad0c47706b9e62e05fe2a0f1d86339760))
- MIP styling ([#1109](https://github.com/OHIF/Viewers/issues/1109))
([0d21cc6](https://github.com/OHIF/Viewers/commit/0d21cc6ad0c47706b9e62e05fe2a0f1d86339760))
## [0.52.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.20...@ohif/extension-vtk@0.52.21) (2019-10-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.19...@ohif/extension-vtk@0.52.20) (2019-10-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.18...@ohif/extension-vtk@0.52.19) (2019-10-25)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.17...@ohif/extension-vtk@0.52.18) (2019-10-25)
### Bug Fixes
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
- 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092))
([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
## [0.52.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.16...@ohif/extension-vtk@0.52.17) (2019-10-25)
### Bug Fixes
* 🐛 Fix crosshairs location when using MIP ([#1080](https://github.com/OHIF/Viewers/issues/1080)) ([b451ce4](https://github.com/OHIF/Viewers/commit/b451ce440796f13b0891739d2130e8ee71993d3d))
- 🐛 Fix crosshairs location when using MIP
([#1080](https://github.com/OHIF/Viewers/issues/1080))
([b451ce4](https://github.com/OHIF/Viewers/commit/b451ce440796f13b0891739d2130e8ee71993d3d))
## [0.52.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.15...@ohif/extension-vtk@0.52.16) (2019-10-23)
### Bug Fixes
* 🐛 Switch to orhtographic view for 2D MPR ([#1074](https://github.com/OHIF/Viewers/issues/1074)) ([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08))
- 🐛 Switch to orhtographic view for 2D MPR
([#1074](https://github.com/OHIF/Viewers/issues/1074))
([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08))
## [0.52.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.14...@ohif/extension-vtk@0.52.15) (2019-10-23)
### Performance Improvements
* ⚡️ Throttle UI refresh of synced WL value in vtk port ([#1070](https://github.com/OHIF/Viewers/issues/1070)) ([9d08e81](https://github.com/OHIF/Viewers/commit/9d08e81aa5def63a5e34f464aae099f7c29f0ac5))
- ⚡️ Throttle UI refresh of synced WL value in vtk port
([#1070](https://github.com/OHIF/Viewers/issues/1070))
([9d08e81](https://github.com/OHIF/Viewers/commit/9d08e81aa5def63a5e34f464aae099f7c29f0ac5))
## [0.52.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.13...@ohif/extension-vtk@0.52.14) (2019-10-22)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.12...@ohif/extension-vtk@0.52.13) (2019-10-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.11...@ohif/extension-vtk@0.52.12) (2019-10-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.10...@ohif/extension-vtk@0.52.11) (2019-10-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.9...@ohif/extension-vtk@0.52.10) (2019-10-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.8...@ohif/extension-vtk@0.52.9) (2019-10-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.7...@ohif/extension-vtk@0.52.8) (2019-10-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.6...@ohif/extension-vtk@0.52.7) (2019-10-11)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.5...@ohif/extension-vtk@0.52.6) (2019-10-11)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.4...@ohif/extension-vtk@0.52.5) (2019-10-10)
### Performance Improvements
* ⚡️ Update react-vtkjs-viewport for performance improvement ([6ee5094](https://github.com/OHIF/Viewers/commit/6ee5094))
- ⚡️ Update react-vtkjs-viewport for performance improvement
([6ee5094](https://github.com/OHIF/Viewers/commit/6ee5094))
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.3...@ohif/extension-vtk@0.52.4) (2019-10-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.2...@ohif/extension-vtk@0.52.3) (2019-10-09)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.1...@ohif/extension-vtk@0.52.2) (2019-10-09)
### Performance Improvements
* 🎸 Update vtk viewport for faster image reconstruction ([#1016](https://github.com/OHIF/Viewers/issues/1016)) ([8513ad9](https://github.com/OHIF/Viewers/commit/8513ad9))
- 🎸 Update vtk viewport for faster image reconstruction
([#1016](https://github.com/OHIF/Viewers/issues/1016))
([8513ad9](https://github.com/OHIF/Viewers/commit/8513ad9))
## [0.52.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.0...@ohif/extension-vtk@0.52.1) (2019-10-04)
**Note:** Version bump only for package @ohif/extension-vtk
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.3...@ohif/extension-vtk@0.52.0) (2019-10-03)
### Features
* 🎸 Synced Window Leveling ([559b637](https://github.com/OHIF/Viewers/commit/559b637)), closes [#558](https://github.com/OHIF/Viewers/issues/558)
- 🎸 Synced Window Leveling
([559b637](https://github.com/OHIF/Viewers/commit/559b637)), closes
[#558](https://github.com/OHIF/Viewers/issues/558)
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.2...@ohif/extension-vtk@0.51.3) (2019-10-03)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.1...@ohif/extension-vtk@0.51.2) (2019-10-02)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.51.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.0...@ohif/extension-vtk@0.51.1) (2019-10-01)
### Bug Fixes
* **rotate:** Use new MPR rotate in react-vtkjs-viewport 0.1.2 ([#964](https://github.com/OHIF/Viewers/issues/964)) ([8503cbc](https://github.com/OHIF/Viewers/commit/8503cbc))
- **rotate:** Use new MPR rotate in react-vtkjs-viewport 0.1.2
([#964](https://github.com/OHIF/Viewers/issues/964))
([8503cbc](https://github.com/OHIF/Viewers/commit/8503cbc))
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.23...@ohif/extension-vtk@0.51.0) (2019-10-01)
### Features
* 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
- 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode
toggle ([#947](https://github.com/OHIF/Viewers/issues/947))
([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
## [0.50.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.22...@ohif/extension-vtk@0.50.23) (2019-10-01)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.21...@ohif/extension-vtk@0.50.22) (2019-09-27)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.20...@ohif/extension-vtk@0.50.21) (2019-09-27)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.19...@ohif/extension-vtk@0.50.20) (2019-09-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.18...@ohif/extension-vtk@0.50.19) (2019-09-26)
### Bug Fixes
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
- Add some code splitting for PWA build
([#937](https://github.com/OHIF/Viewers/issues/937))
([8938035](https://github.com/OHIF/Viewers/commit/8938035))
## [0.50.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.17...@ohif/extension-vtk@0.50.18) (2019-09-23)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.16...@ohif/extension-vtk@0.50.17) (2019-09-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.15...@ohif/extension-vtk@0.50.16) (2019-09-19)
### Bug Fixes
* Set transfer function range to data range when loading volumes in VTK ([#930](https://github.com/OHIF/Viewers/issues/930)) ([2341b32](https://github.com/OHIF/Viewers/commit/2341b32))
- Set transfer function range to data range when loading volumes in VTK
([#930](https://github.com/OHIF/Viewers/issues/930))
([2341b32](https://github.com/OHIF/Viewers/commit/2341b32))
## [0.50.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.14...@ohif/extension-vtk@0.50.15) (2019-09-19)
### Bug Fixes
* Bump VTK viewport dependency ([#929](https://github.com/OHIF/Viewers/issues/929)) ([66a0aad](https://github.com/OHIF/Viewers/commit/66a0aad))
* Stop adding empty labelmap to VTK Viewports for now. Wait until paint tools are implemented ([#926](https://github.com/OHIF/Viewers/issues/926)) ([7f642a0](https://github.com/OHIF/Viewers/commit/7f642a0))
- Bump VTK viewport dependency
([#929](https://github.com/OHIF/Viewers/issues/929))
([66a0aad](https://github.com/OHIF/Viewers/commit/66a0aad))
- Stop adding empty labelmap to VTK Viewports for now. Wait until paint tools
are implemented ([#926](https://github.com/OHIF/Viewers/issues/926))
([7f642a0](https://github.com/OHIF/Viewers/commit/7f642a0))
## [0.50.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.13...@ohif/extension-vtk@0.50.14) (2019-09-17)
### Bug Fixes
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
- bump cornerstone-tools version in peerDeps
([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
## [0.50.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.12...@ohif/extension-vtk@0.50.13) (2019-09-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.11...@ohif/extension-vtk@0.50.12) (2019-09-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.10...@ohif/extension-vtk@0.50.11) (2019-09-10)
### Bug Fixes
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
- simplify runtime-extension usage
([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.9...@ohif/extension-vtk@0.50.10) (2019-09-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.8...@ohif/extension-vtk@0.50.9) (2019-09-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.7...@ohif/extension-vtk@0.50.8) (2019-09-09)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.6...@ohif/extension-vtk@0.50.7) (2019-09-06)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.5...@ohif/extension-vtk@0.50.6) (2019-09-04)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.4...@ohif/extension-vtk@0.50.5) (2019-09-04)
### Bug Fixes
* measurementsAPI issue caused by production build ([#842](https://github.com/OHIF/Viewers/issues/842)) ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
- measurementsAPI issue caused by production build
([#842](https://github.com/OHIF/Viewers/issues/842))
([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.3...@ohif/extension-vtk@0.50.4) (2019-08-26)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.2...@ohif/extension-vtk@0.50.3) (2019-08-22)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.1...@ohif/extension-vtk@0.50.2) (2019-08-15)
### Bug Fixes
* Update vtk and viewport dependencies ([98c40bc](https://github.com/OHIF/Viewers/commit/98c40bc))
- Update vtk and viewport dependencies
([98c40bc](https://github.com/OHIF/Viewers/commit/98c40bc))
## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.0-alpha.10...@ohif/extension-vtk@0.50.1) (2019-08-14)
**Note:** Version bump only for package @ohif/extension-vtk
# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.1.4-alpha.9...@ohif/extension-vtk@0.50.0-alpha.10) (2019-08-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.1.4-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.1.4-alpha.8...@ohif/extension-vtk@0.1.4-alpha.9) (2019-08-14)
**Note:** Version bump only for package @ohif/extension-vtk
## 0.1.4-alpha.8 (2019-08-14)
**Note:** Version bump only for package @ohif/extension-vtk
# Change Log
All notable changes to this project will be documented in this file. See
+6 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.2.13",
"version": "1.3.3",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -33,8 +33,9 @@
"@ohif/i18n": "^0.50.0",
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.12.5",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.8.2",
"dcmjs": "^0.10.1",
"dicom-parser": "^1.8.3",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
@@ -52,9 +53,9 @@
"react-vtkjs-viewport": "^0.8.3"
},
"devDependencies": {
"@ohif/core": "^2.3.7",
"@ohif/ui": "^1.1.9",
"cornerstone-tools": "^4.12.0",
"@ohif/core": "^2.6.2",
"@ohif/ui": "^1.4.1",
"cornerstone-tools": "4.12.5",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1",
+159 -117
View File
@@ -1,46 +1,34 @@
import React, { Component } from 'react';
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
import ConnectedVTKViewport from './ConnectedVTKViewport';
import LoadingIndicator from './LoadingIndicator.js';
import OHIF from '@ohif/core';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import handleSegmentationStorage from './handleSegmentationStorage.js';
import cornerstoneTools from 'cornerstone-tools';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
const segmentationModule = cornerstoneTools.getModule('segmentation');
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;
// TODO: Figure out where we plan to put this long term
const volumeCache = {};
const labelmapCache = {};
/**
* Create a labelmap image with the same dimensions as our background volume.
*
* @param backgroundImageData vtkImageData
*/
/* TODO: Not currently used until we have drawing tools in vtkjs.
function createLabelMapImageData(backgroundImageData) {
// TODO => Need to do something like this if we start drawing a new segmentation
// On a vtkjs viewport.
const labelMapData = vtkImageData.newInstance(
backgroundImageData.get('spacing', 'origin', 'direction')
);
@@ -55,7 +43,7 @@ function createLabelMapImageData(backgroundImageData) {
labelMapData.getPointData().setScalars(dataArray);
return labelMapData;
}
} */
class OHIFVTKViewport extends Component {
state = {
@@ -68,10 +56,10 @@ class OHIFVTKViewport extends Component {
viewportData: PropTypes.shape({
studies: PropTypes.array,
displaySet: PropTypes.shape({
studyInstanceUid: PropTypes.string,
displaySetInstanceUid: PropTypes.string,
sopClassUids: PropTypes.arrayOf(PropTypes.string),
sopInstanceUid: PropTypes.string,
StudyInstanceUID: PropTypes.string,
displaySetInstanceUID: PropTypes.string,
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
SOPInstanceUID: PropTypes.string,
frameIndex: PropTypes.number,
}),
}),
@@ -97,18 +85,18 @@ class OHIFVTKViewport extends Component {
static getCornerstoneStack(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
SOPInstanceUID,
frameIndex
) {
// Create shortcut to displaySet
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
study => study.StudyInstanceUID === StudyInstanceUID
);
const displaySet = study.displaySets.find(set => {
return set.displaySetInstanceUid === displaySetInstanceUid;
return set.displaySetInstanceUID === displaySetInstanceUID;
});
// Get stack from Stack Manager
@@ -119,17 +107,14 @@ class OHIFVTKViewport extends Component {
if (frameIndex !== undefined) {
stack.currentImageIdIndex = frameIndex;
} else if (sopInstanceUid) {
} else 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) {
@@ -144,48 +129,73 @@ class OHIFVTKViewport extends Component {
getViewportData = (
studies,
studyInstanceUid,
displaySetInstanceUid,
sopClassUid,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
SOPClassUID,
SOPInstanceUID,
frameIndex
) => {
const stack = OHIFVTKViewport.getCornerstoneStack(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopClassUid,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
SOPClassUID,
SOPInstanceUID,
frameIndex
);
let imageDataObject;
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
let labelmapDataObject;
let labelmapColorLUT;
switch (sopClassUid) {
case SOP_CLASSES.SEGMENTATION_STORAGE:
throw new Error('Not yet implemented');
/*
const data = handleSegmentationStorage(
stack.imageIds,
displaySetInstanceUid
const firstImageId = stack.imageIds[0];
const { state } = segmentationModule;
const brushStackState = state.series[firstImageId];
if (brushStackState) {
const { activeLabelmapIndex } = brushStackState;
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`;
if (labelmapCache[vtkLabelmapID]) {
labelmapDataObject = labelmapCache[vtkLabelmapID];
} else {
// TODO -> We need an imageId based getter in cornerstoneTools
const labelmapBuffer = labelmap3D.buffer;
// Create VTK Image Data with buffer as input
labelmapDataObject = vtkImageData.newInstance();
const dataArray = vtkDataArray.newInstance({
numberOfComponents: 1, // labelmap with single component
values: new Uint16Array(labelmapBuffer),
});
labelmapDataObject.getPointData().setScalars(dataArray);
labelmapDataObject.setDimensions(...imageDataObject.dimensions);
labelmapDataObject.setSpacing(
...imageDataObject.vtkImageData.getSpacing()
);
labelmapDataObject.setOrigin(
...imageDataObject.vtkImageData.getOrigin()
);
labelmapDataObject.setDirection(
...imageDataObject.vtkImageData.getDirection()
);
imageDataObject = data.referenceDataObject;
labelmapDataObject = data.labelmapDataObject;
// Cache the labelmap volume.
labelmapCache[vtkLabelmapID] = labelmapDataObject;
}
return loadImageData(imageDataObject).then(() => {
return {
data: imageDataObject.vtkImageData,
labelmap: labelmapDataObject,
};
});
*/
default:
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
return imageDataObject;
labelmapColorLUT = state.colorLutTables[labelmap3D.colorLUTIndex];
}
return {
imageDataObject,
labelmapDataObject,
labelmapColorLUT,
};
};
/**
@@ -194,25 +204,31 @@ class OHIFVTKViewport extends Component {
* @param {object} imageDataObject
* @param {object} imageDataObject.vtkImageData
* @param {object} imageDataObject.imageMetaData0
* @param {number} [imageDataObject.imageMetaData0.windowWidth] - The volume's initial windowWidth
* @param {number} [imageDataObject.imageMetaData0.windowCenter] - The volume's initial windowCenter
* @param {string} imageDataObject.imageMetaData0.modality - CT, MR, PT, etc
* @param {string} displaySetInstanceUid
* @param {number} [imageDataObject.imageMetaData0.WindowWidth] - The volume's initial WindowWidth
* @param {number} [imageDataObject.imageMetaData0.WindowCenter] - The volume's initial WindowCenter
* @param {string} imageDataObject.imageMetaData0.Modality - CT, MR, PT, etc
* @param {string} displaySetInstanceUID
* @returns vtkVolumeActor
* @memberof OHIFVTKViewport
*/
getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
if (volumeCache[displaySetInstanceUid]) {
return volumeCache[displaySetInstanceUid];
getOrCreateVolume(imageDataObject, displaySetInstanceUID) {
if (volumeCache[displaySetInstanceUID]) {
return volumeCache[displaySetInstanceUID];
}
const { vtkImageData, imageMetaData0 } = imageDataObject;
const { windowWidth, windowCenter, modality } = imageMetaData0;
// TODO -> Should update react-vtkjs-viewport and react-cornerstone-viewports
// internals to use naturalized DICOM JSON names.
const {
windowWidth: WindowWidth,
windowCenter: WindowCenter,
modality: Modality,
} = imageMetaData0;
const { lower, upper } = _getRangeFromWindowLevels(
windowWidth,
windowCenter,
modality
WindowWidth,
WindowCenter,
Modality
);
const volumeActor = vtkVolume.newInstance();
const volumeMapper = vtkVolumeMapper.newInstance();
@@ -236,7 +252,7 @@ class OHIFVTKViewport extends Component {
// TODO: maybe we should auto adjust samples to 1000.
volumeMapper.setMaximumSamplesPerRay(4000);
volumeCache[displaySetInstanceUid] = volumeActor;
volumeCache[displaySetInstanceUID] = volumeActor;
return volumeActor;
}
@@ -244,53 +260,61 @@ class OHIFVTKViewport extends Component {
setStateFromProps() {
const { studies, displaySet } = this.props.viewportData;
const {
studyInstanceUid,
displaySetInstanceUid,
sopClassUids,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
sopClassUIDs,
SOPInstanceUID,
frameIndex,
} = displaySet;
if (sopClassUids.length > 1) {
if (sopClassUIDs.length > 1) {
console.warn(
'More than one SOPClassUid in the same series is not yet supported.'
'More than one SOPClassUID in the same series is not yet supported.'
);
}
const sopClassUid = sopClassUids[0];
const imageDataObject = this.getViewportData(
const study = studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
);
const dataDetails = {
studyDate: study.studyDate,
studyTime: study.studyTime,
studyDescription: study.studyDescription,
patientName: study.patientName,
patientId: study.patientId,
seriesNumber: String(displaySet.seriesNumber),
seriesDescription: displaySet.seriesDescription,
};
const {
imageDataObject,
labelmapDataObject,
labelmapColorLUT,
} = this.getViewportData(
studies,
studyInstanceUid,
displaySetInstanceUid,
sopClassUid,
sopInstanceUid,
StudyInstanceUID,
displaySetInstanceUID,
SOPInstanceUID,
frameIndex
);
this.imageDataObject = imageDataObject;
// TODO: Temporarily disabling this since it is not yet
// being used and hurts performance significantly.
/*if (!labelmap) {
/* TODO: Not currently used until we have drawing tools in vtkjs.
if (!labelmap) {
labelmap = createLabelMapImageData(data);
}*/
} */
const volumeActor = this.getOrCreateVolume(
imageDataObject,
displaySetInstanceUid
displaySetInstanceUID
);
this.setState({
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
paintFilterLabelMapImageData: null, // TODO
percentComplete: 0,
});
this.setState(
{
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
paintFilterLabelMapImageData: null, // TODO
percentComplete: 0,
dataDetails,
},
() => {
this.loadProgressively(imageDataObject);
@@ -302,6 +326,9 @@ class OHIFVTKViewport extends Component {
setTimeout(() => {
this.setState({
volumes: [volumeActor],
paintFilterLabelMapImageData: labelmapDataObject,
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
labelmapColorLUT,
});
}, 200);
}
@@ -313,13 +340,13 @@ class OHIFVTKViewport 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();
@@ -331,7 +358,7 @@ class OHIFVTKViewport extends Component {
const { isLoading, insertPixelDataPromises } = imageDataObject;
const numberOfFrames = insertPixelDataPromises.length;
const NumberOfFrames = insertPixelDataPromises.length;
if (!isLoading) {
this.setState({ isLoaded: true });
@@ -341,7 +368,7 @@ class OHIFVTKViewport extends Component {
insertPixelDataPromises.forEach(promise => {
promise.then(numberProcessed => {
const percentComplete = Math.floor(
(numberProcessed * 100) / numberOfFrames
(numberProcessed * 100) / NumberOfFrames
);
if (percentComplete !== this.state.percentComplete) {
@@ -353,12 +380,15 @@ class OHIFVTKViewport extends Component {
});
Promise.all(insertPixelDataPromises).then(() => {
this.setState({ isLoaded: true });
this.setState({
isLoaded: true,
});
});
}
render() {
let childrenWithProps = null;
const { configuration } = segmentationModule;
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
@@ -375,6 +405,10 @@ class OHIFVTKViewport extends Component {
const style = { width: '100%', height: '100%', position: 'relative' };
const visible = configuration.renderFill || configuration.renderOutline;
const opacity = configuration.fillAlpha;
const outlineThickness = configuration.outlineThickness;
return (
<>
<div style={style}>
@@ -391,6 +425,14 @@ class OHIFVTKViewport extends Component {
this.state.paintFilterBackgroundImageData
}
viewportIndex={this.props.viewportIndex}
dataDetails={this.state.dataDetails}
labelmapRenderingOptions={{
colorLUT: this.state.labelmapColorLUT,
globalOpacity: opacity,
visible,
outlineThickness,
renderOutline: true,
}}
onScroll={this.props.onScroll}
/>
)}
@@ -409,21 +451,21 @@ class OHIFVTKViewport extends Component {
* @private
* @param {number} [width] - the width of our window
* @param {number} [center] - the center of our window
* @param {string} [modality] - 'PT', 'CT', etc.
* @param {string} [Modality] - 'PT', 'CT', etc.
* @returns { lower, upper } - range
*/
function _getRangeFromWindowLevels(width, center, modality = undefined) {
function _getRangeFromWindowLevels(width, center, Modality = undefined) {
// For PET just set the range to 0-5 SUV
if (Modality === 'PT') {
return { lower: 0, upper: 5 };
}
const levelsAreNotNumbers = isNaN(center) || isNaN(width);
if (levelsAreNotNumbers) {
return { lower: 0, upper: 512 };
}
// For PET just set the range to 0-5 SUV
if (modality === 'PT') {
return { lower: 0, upper: 5 };
}
return {
lower: center - width / 2.0,
upper: center + width / 2.0,
+2 -2
View File
@@ -19,8 +19,8 @@ const VTKViewport = props => {
useEffect(() => {
const handleScrollEvent = evt => {
const vtkViewportApiReference = props.onScroll(props.viewportIndex) || {};
const viewportUid = vtkViewportApiReference.uid;
const viewportWasScrolled = viewportUid === evt.detail.uid;
const viewportUID = vtkViewportApiReference.uid;
const viewportWasScrolled = viewportUID === evt.detail.uid;
if (viewportWasScrolled) {
setViewportActiveHandler();
+2 -5
View File
@@ -70,12 +70,9 @@ const commandsModule = ({ commandsManager }) => {
if (cornerstoneElement) {
const imageId = cornerstoneElement.image.imageId;
const { modality } = cornerstone.metaData.get(
'generalSeriesModule',
imageId
);
const Modality = cornerstone.metaData.get('Modality', imageId);
if (modality !== 'PT') {
if (Modality !== 'PT') {
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
return {
@@ -1,150 +0,0 @@
import * as dcmjs from "dcmjs";
import OHIF from "@ohif/core";
import { api } from "dicomweb-client";
const { StackManager } = 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 getCornerstoneStack(studies, studyInstanceUid, displaySetInstanceUid) {
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
);
// Create shortcut to displaySet
const displaySet = getDisplaySet(
studies,
studyInstanceUid,
displaySetInstanceUid
);
// Get stack from Stack Manager
const stack = StackManager.findOrCreateStack(study, displaySet);
// Clone the stack here so we don't mutate it later
const stackClone = Object.assign({}, stack);
stackClone.currentImageIdIndex = 0;
return stackClone;
}
function retrieveDicomData(
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID,
wadoRoot
) {
const config = {
url: wadoRoot,
headers: DICOMWeb.getAuthorizationHeader()
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID
};
return dicomWeb.retrieveInstance(options);
}
async function handleSegmentationStorage(
studies,
studyInstanceUid,
displaySetInstanceUid
) {
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
);
const displaySet = getDisplaySet(
studies,
studyInstanceUid,
displaySetInstanceUid
);
// TODO: This is terrible but we need to use WADO-RS or we can't retrieve the SEG
// from google cloud
const wadoRoot = displaySet.images[0].getData().wadoRoot;
const StudyInstanceUID = displaySet.images[0].getStudyInstanceUID();
const SeriesInstanceUID = displaySet.images[0].getSeriesInstanceUID();
const SOPInstanceUID = displaySet.images[0].getSOPInstanceUID();
const arrayBuffer = await retrieveDicomData(
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
wadoRoot
);
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
const segments = dcmjs.adapters.VTKjs.Segmentation.generateSegments(dataset);
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());
if (!results) {
throw new Error("Fractional segmentations are not supported");
}
const cachedStack = StackManager.findOrCreateStack(
study,
referenceDisplaySet
);
const stack = Object.assign({}, cachedStack);
stack.currentImageIdIndex = 0;
return {
referenceDataObject,
labelmapDataObject
};
return {
studyInstanceUid,
displaySetInstanceUid,
stack
};
}
export default handleSegmentationStorage;
@@ -13,21 +13,21 @@ const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportI
return false;
};
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
const { displaySetInstanceUID, StudyInstanceUID } = viewportSpecificData[
activeViewportIndex
];
const studies = studyMetadataManager.all();
const study = studies.find(
study => study.studyInstanceUID === studyInstanceUid
study => study.studyInstanceUID === StudyInstanceUID
);
if (!study) {
return false;
}
const displaySet = study._displaySets.find(set => set.displaySetInstanceUid === displaySetInstanceUid);
const displaySet = study._displaySets.find(set => set.displaySetInstanceUID === displaySetInstanceUID);
if (!displaySet) {
return false;
+2 -1
View File
@@ -10,7 +10,8 @@ module.exports = {
// https://jestjs.io/docs/en/configuration#projects-array-string-projectconfig
projects: [
// "<rootDir>/extensions/*/jest.config.js",
"<rootDir>/platform/*/jest.config.js"
"<rootDir>/platform/*/jest.config.js",
"<rootDir>/extensions/*/jest.config.js"
],
coverageDirectory: "<rootDir>/coverage/"
};
+2
View File
@@ -13,11 +13,13 @@
"scripts": {
"cm": "npx git-cz",
"build": "lerna run build:viewer --stream",
"build:lt": "lerna run build:viewer:lesion-tracker --stream",
"build:ci": "lerna run build:viewer:ci --stream",
"build:demo": "lerna run build:viewer:demo --stream",
"build:package": "lerna run build:viewer:package --stream",
"build:package-all": "lerna run build:package --parallel --stream",
"dev": "lerna run dev:viewer --stream",
"dev:lt": "lerna run dev:viewer:lesion-tracker --stream",
"dev:project": ".scripts/dev.sh",
"dev:orthanc": "lerna run dev:orthanc --stream",
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
+195 -178
View File
@@ -3,6 +3,123 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.1...@ohif/core@2.6.2) (2020-03-24)
### Bug Fixes
* OverlayPlane module usage for ArrayBuffer, BulkDataURI, and InlineBinary cases, as well as PaletteColor LUTs for ArrayBuffer (i.e. local drag/drop) case ([#1546](https://github.com/OHIF/Viewers/issues/1546)) ([404d52f](https://github.com/OHIF/Viewers/commit/404d52fe5c0442dd13e4d407bb0687d72fa5f32c))
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.0...@ohif/core@2.6.1) (2020-03-23)
### Bug Fixes
* avoid-wasteful-renders ([#1544](https://github.com/OHIF/Viewers/issues/1544)) ([e41d339](https://github.com/OHIF/Viewers/commit/e41d339f5faef6b93700bc860f37f29f32ad5ed6))
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.3...@ohif/core@2.6.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)
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.5.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.2...@ohif/core@2.5.3) (2020-03-09)
**Note:** Version bump only for package @ohif/core
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.1...@ohif/core@2.5.2) (2020-03-06)
**Note:** Version bump only for package @ohif/core
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.0...@ohif/core@2.5.1) (2020-02-21)
### Bug Fixes
* 💡 Update log to warn ([#1454](https://github.com/OHIF/Viewers/issues/1454)) ([509a7b7](https://github.com/OHIF/Viewers/commit/509a7b7e57834a0653add98cc320b2a5dbefd51d)), closes [#1451](https://github.com/OHIF/Viewers/issues/1451)
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.1...@ohif/core@2.5.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.4.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.0...@ohif/core@2.4.1) (2020-02-12)
### Bug Fixes
* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd))
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.9...@ohif/core@2.4.0) (2020-02-10)
### Features
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
## [2.3.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.8...@ohif/core@2.3.9) (2020-02-07)
**Note:** Version bump only for package @ohif/core
## [2.3.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.7...@ohif/core@2.3.8) (2020-02-06)
### Bug Fixes
* Remove trash data from redux storage after updates ([#1358](https://github.com/OHIF/Viewers/issues/1358)) ([7b2d44f](https://github.com/OHIF/Viewers/commit/7b2d44f2c18241ea521b8d3652aee32e36eaddb8))
## [2.3.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.6...@ohif/core@2.3.7) (2020-01-30)
@@ -225,339 +342,239 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.8.0...@ohif/core@1.9.0) (2019-11-13)
### Features
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
- expose UiNotifications service
([#1172](https://github.com/OHIF/Viewers/issues/1172))
([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.1...@ohif/core@1.8.0) (2019-11-12)
### Features
* 🎸 Update hotkeys and user preferences modal ([#1135](https://github.com/OHIF/Viewers/issues/1135)) ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), closes [#923](https://github.com/OHIF/Viewers/issues/923)
- 🎸 Update hotkeys and user preferences modal
([#1135](https://github.com/OHIF/Viewers/issues/1135))
([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)),
closes [#923](https://github.com/OHIF/Viewers/issues/923)
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.0...@ohif/core@1.7.1) (2019-11-08)
### Bug Fixes
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
- Add a fallback metadata provider which pulls metadata from WADO-…
([#1158](https://github.com/OHIF/Viewers/issues/1158))
([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.2...@ohif/core@1.7.0) (2019-11-05)
### Features
* 🎸 Filter by url query param for seriesInstnaceUID ([#1117](https://github.com/OHIF/Viewers/issues/1117)) ([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
- 🎸 Filter by url query param for seriesInstnaceUID
([#1117](https://github.com/OHIF/Viewers/issues/1117))
([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)),
closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
## [1.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.1...@ohif/core@1.6.2) (2019-11-05)
### Bug Fixes
* [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study List before all series have finishe… ([#1090](https://github.com/OHIF/Viewers/issues/1090)) ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125))
- [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study
List before all series have finishe…
([#1090](https://github.com/OHIF/Viewers/issues/1090))
([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125))
## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.0...@ohif/core@1.6.1) (2019-10-31)
### Bug Fixes
* application crash if patientName is an object ([#1138](https://github.com/OHIF/Viewers/issues/1138)) ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa))
- application crash if patientName is an object
([#1138](https://github.com/OHIF/Viewers/issues/1138))
([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa))
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.2...@ohif/core@1.6.0) (2019-10-26)
### Features
* Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
- Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840))
([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.1...@ohif/core@1.5.2) (2019-10-25)
### Bug Fixes
* set SR in ActiveViewport by clicking thumb ([#1091](https://github.com/OHIF/Viewers/issues/1091)) ([986b7ae](https://github.com/OHIF/Viewers/commit/986b7ae2bf4f7d27f326e62f93285ce20eaf0a79))
- set SR in ActiveViewport by clicking thumb
([#1091](https://github.com/OHIF/Viewers/issues/1091))
([986b7ae](https://github.com/OHIF/Viewers/commit/986b7ae2bf4f7d27f326e62f93285ce20eaf0a79))
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.0...@ohif/core@1.5.1) (2019-10-25)
### Bug Fixes
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
- 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092))
([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.4.0...@ohif/core@1.5.0) (2019-10-25)
### Features
* 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List ([#1069](https://github.com/OHIF/Viewers/issues/1069)) ([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db))
- 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List
([#1069](https://github.com/OHIF/Viewers/issues/1069))
([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db))
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.2...@ohif/core@1.4.0) (2019-10-15)
### Features
* 🎸 Only allow reconstruction of datasets that make sense ([#1010](https://github.com/OHIF/Viewers/issues/1010)) ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes [#561](https://github.com/OHIF/Viewers/issues/561)
- 🎸 Only allow reconstruction of datasets that make sense
([#1010](https://github.com/OHIF/Viewers/issues/1010))
([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes
[#561](https://github.com/OHIF/Viewers/issues/561)
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.1...@ohif/core@1.3.2) (2019-10-14)
### Bug Fixes
* Return display sets in StudyMetadata._createDisplaySetsForSeries ([#1042](https://github.com/OHIF/Viewers/issues/1042)) ([fc01532](https://github.com/OHIF/Viewers/commit/fc01532))
- Return display sets in StudyMetadata.\_createDisplaySetsForSeries
([#1042](https://github.com/OHIF/Viewers/issues/1042))
([fc01532](https://github.com/OHIF/Viewers/commit/fc01532))
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.0...@ohif/core@1.3.1) (2019-10-11)
**Note:** Version bump only for package @ohif/core
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.2.0...@ohif/core@1.3.0) (2019-10-11)
### Features
* 🎸 Improve usability of Google Cloud adapter, including direct routes to studies ([#989](https://github.com/OHIF/Viewers/issues/989)) ([2bc361c](https://github.com/OHIF/Viewers/commit/2bc361c))
- 🎸 Improve usability of Google Cloud adapter, including direct routes to
studies ([#989](https://github.com/OHIF/Viewers/issues/989))
([2bc361c](https://github.com/OHIF/Viewers/commit/2bc361c))
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.1.0...@ohif/core@1.2.0) (2019-10-09)
### Bug Fixes
* OHIF-1002 Study lazy load should be true by default ([#1004](https://github.com/OHIF/Viewers/issues/1004)) ([66d8bc6](https://github.com/OHIF/Viewers/commit/66d8bc6))
- OHIF-1002 Study lazy load should be true by default
([#1004](https://github.com/OHIF/Viewers/issues/1004))
([66d8bc6](https://github.com/OHIF/Viewers/commit/66d8bc6))
### Features
* Allow a server requestOptions.auth to be a function that returns the Authorization header. ([#928](https://github.com/OHIF/Viewers/issues/928)) ([0064a4b](https://github.com/OHIF/Viewers/commit/0064a4b))
- Allow a server requestOptions.auth to be a function that returns the
Authorization header. ([#928](https://github.com/OHIF/Viewers/issues/928))
([0064a4b](https://github.com/OHIF/Viewers/commit/0064a4b))
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.2...@ohif/core@1.1.0) (2019-10-03)
### Features
* Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
- Use QIDO + WADO to load series metadata individually rather than the entire
study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953))
([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.1...@ohif/core@1.0.2) (2019-10-02)
### Bug Fixes
* Temporarily sort SEG files to the end of the display set list as a workaround for several metadata issues ([#987](https://github.com/OHIF/Viewers/issues/987)) ([b3b4c10](https://github.com/OHIF/Viewers/commit/b3b4c10))
- Temporarily sort SEG files to the end of the display set list as a workaround
for several metadata issues
([#987](https://github.com/OHIF/Viewers/issues/987))
([b3b4c10](https://github.com/OHIF/Viewers/commit/b3b4c10))
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.0...@ohif/core@1.0.1) (2019-09-27)
### Bug Fixes
* Check for Value in 00081155 sequence (Few patient protocol images doesn't have this value) and removed a duplicate declaration ([#921](https://github.com/OHIF/Viewers/issues/921)) ([d0ec9cf](https://github.com/OHIF/Viewers/commit/d0ec9cf))
- Check for Value in 00081155 sequence (Few patient protocol images doesn't have
this value) and removed a duplicate declaration
([#921](https://github.com/OHIF/Viewers/issues/921))
([d0ec9cf](https://github.com/OHIF/Viewers/commit/d0ec9cf))
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.10...@ohif/core@1.0.0) (2019-09-27)
### Bug Fixes
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
- 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support
in local file and WADO-RS-only use cases
([#862](https://github.com/OHIF/Viewers/issues/862))
([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes
[#838](https://github.com/OHIF/Viewers/issues/838)
### BREAKING CHANGES
* DICOM Seg
- DICOM Seg
## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.9...@ohif/core@0.50.10) (2019-09-23)
### Bug Fixes
* Avoid using variable name "module" ([#942](https://github.com/OHIF/Viewers/issues/942)) ([72427fe](https://github.com/OHIF/Viewers/commit/72427fe)), closes [#940](https://github.com/OHIF/Viewers/issues/940)
- Avoid using variable name "module"
([#942](https://github.com/OHIF/Viewers/issues/942))
([72427fe](https://github.com/OHIF/Viewers/commit/72427fe)), closes
[#940](https://github.com/OHIF/Viewers/issues/940)
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.8...@ohif/core@0.50.9) (2019-09-17)
### Bug Fixes
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
- bump cornerstone-tools version in peerDeps
([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.7...@ohif/core@0.50.8) (2019-09-10)
**Note:** Version bump only for package @ohif/core
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.6...@ohif/core@0.50.7) (2019-09-10)
### Bug Fixes
* remove requestOptions when key is not needed ([32bc47d](https://github.com/OHIF/Viewers/commit/32bc47d))
- remove requestOptions when key is not needed
([32bc47d](https://github.com/OHIF/Viewers/commit/32bc47d))
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.5...@ohif/core@0.50.6) (2019-09-09)
**Note:** Version bump only for package @ohif/core
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.4...@ohif/core@0.50.5) (2019-09-04)
**Note:** Version bump only for package @ohif/core
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.3...@ohif/core@0.50.4) (2019-09-04)
### Bug Fixes
* measurementsAPI issue caused by production build ([#842](https://github.com/OHIF/Viewers/issues/842)) ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
- measurementsAPI issue caused by production build
([#842](https://github.com/OHIF/Viewers/issues/842))
([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.2...@ohif/core@0.50.3) (2019-08-26)
### Bug Fixes
* **Studies:** Qidosupportsincludefield should be true by default ([#801](https://github.com/OHIF/Viewers/issues/801)) ([a88d865](https://github.com/OHIF/Viewers/commit/a88d865))
- **Studies:** Qidosupportsincludefield should be true by default
([#801](https://github.com/OHIF/Viewers/issues/801))
([a88d865](https://github.com/OHIF/Viewers/commit/a88d865))
## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.1...@ohif/core@0.50.2) (2019-08-22)
**Note:** Version bump only for package @ohif/core
## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.0-alpha.10...@ohif/core@0.50.1) (2019-08-14)
**Note:** Version bump only for package @ohif/core
# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.9...@ohif/core@0.50.0-alpha.10) (2019-08-14)
**Note:** Version bump only for package @ohif/core
## [0.11.1-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.8...@ohif/core@0.11.1-alpha.9) (2019-08-14)
**Note:** Version bump only for package @ohif/core
## 0.11.1-alpha.8 (2019-08-14)
**Note:** Version bump only for package @ohif/core
# Change Log
All notable changes to this project will be documented in this file. See
+2 -2
View File
@@ -66,8 +66,8 @@ import { studies } from '@ohif/core';
const studiesMetadata = await studies.retrieveStudiesMetadata(
server, // Object
studyInstanceUids, // Array
seriesInstanceUids // Array (optional)
studyInstanceUIDs, // Array
seriesInstanceUIDs // Array (optional)
);
```
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.3.7",
"version": "2.6.2",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -32,7 +32,7 @@
},
"peerDependencies": {
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.12.0",
"cornerstone-tools": "4.12.5",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3"
},
@@ -41,6 +41,7 @@
"ajv": "^6.10.0",
"dcmjs": "^0.8.3",
"dicomweb-client": "^0.5.2",
"immer": "6.0.2",
"isomorphic-base64": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
"lodash.merge": "^4.6.1",
+6 -5
View File
@@ -31,6 +31,7 @@ const retrieveMeasurements = server => {
const serverUrl = server.wadoRoot;
const studies = utils.studyMetadataManager.all();
const latestSeries = findMostRecentStructuredReport(studies);
if (!latestSeries) return Promise.resolve({});
@@ -57,17 +58,17 @@ const storeMeasurements = async (measurementData, filter, server) => {
const serverUrl = server.wadoRoot;
const firstMeasurementKey = Object.keys(measurementData)[0];
const firstMeasurement = measurementData[firstMeasurementKey][0];
const studyInstanceUid =
firstMeasurement && firstMeasurement.studyInstanceUid;
const StudyInstanceUID =
firstMeasurement && firstMeasurement.StudyInstanceUID;
try {
await stowSRFromMeasurements(measurementData, serverUrl);
if (studyInstanceUid) {
studies.deleteStudyMetadataPromise(studyInstanceUid);
if (StudyInstanceUID) {
studies.deleteStudyMetadataPromise(StudyInstanceUID);
}
return {
message: 'Measurements were saved with success',
message: 'Measurements saved successfully',
};
} catch (error) {
log.error(
@@ -1,6 +1,6 @@
import * as dcmjs from 'dcmjs';
import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBySopInstanceUid';
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
/**
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
@@ -13,6 +13,7 @@ import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBy
*/
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
// Get the dicom data as an Object
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
@@ -30,17 +31,18 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
measurementData[toolName] = [];
measurements.forEach(measurement => {
const instanceMetadata = findInstanceMetadataBySopInstanceUid(
const instanceMetadata = findInstanceMetadataBySopInstanceUID(
displaySets,
measurement.sopInstanceUid
);
const { _study: study, _series: series } = instanceMetadata;
const { studyInstanceUid, patientId } = study;
const { seriesInstanceUid } = series;
const { StudyInstanceUID, PatientID } = study;
const { SeriesInstanceUID } = series;
const { sopInstanceUid, frameIndex } = measurement;
const imagePath = getImagePath(
studyInstanceUid,
seriesInstanceUid,
StudyInstanceUID,
SeriesInstanceUID,
sopInstanceUid,
frameIndex
);
@@ -56,9 +58,10 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
const toolData = Object.assign({}, measurement, {
imageId,
imagePath,
seriesInstanceUid,
studyInstanceUid,
patientId,
SOPInstanceUID: sopInstanceUid,
SeriesInstanceUID,
StudyInstanceUID,
PatientID,
measurementNumber: ++measurementNumber,
timepointId: currentTimepointId,
toolType: toolName,
@@ -75,19 +78,19 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
/**
* Function to create imagePath with all imageData related
*
* @param {string} studyInstanceUid
* @param {string} seriesInstanceUid
* @param {string} sopInstanceUid
* @param {string} StudyInstanceUID
* @param {string} SeriesInstanceUID
* @param {string} SOPInstanceUID
* @param {string} frameIndex
* @returns
*/
const getImagePath = (
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
frameIndex
) => {
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join(
return [StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, frameIndex].join(
'_'
);
};
@@ -2,17 +2,18 @@
* Should Find the requested instance metadata into the displaySets and return
*
* @param {Array} displaySets - List of displaySets
* @param {string} sopInstanceUid - sopInstanceUID to look for
* @param {string} SOPInstanceUID - sopInstanceUID to look for
* @returns {Object} - instance metadata found
*/
const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
let instanceFound;
displaySets.find(displaySet => {
if (!displaySet.images) return false;
instanceFound = displaySet.images.find(
instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid
instanceMetadata =>
instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
);
return !!instanceFound;
@@ -21,4 +22,4 @@ const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
return instanceFound;
};
export default findInstanceMetadataBySopInstanceUid;
export default findInstanceMetadataBySopInstanceUID;
@@ -37,9 +37,9 @@ const isStructuredReportSeries = series => {
];
const firstInstance = series.getFirstInstance();
const sopClassUid = firstInstance._instance.sopClassUid;
const SOPClassUID = firstInstance.getData().metadata.SOPClassUID;
return supportedSopClassUIDs.includes(sopClassUid);
return supportedSopClassUIDs.includes(SOPClassUID);
};
/**
@@ -51,9 +51,9 @@ const isStructuredReportSeries = series => {
*/
const compareSeriesDate = (series1, series2) => {
return (
series1._data.seriesDate > series2._data.seriesDate ||
(series1._data.seriesDate === series2._data.seriesDate &&
series1._data.seriesTime > series2._data.seriesTime)
series1._data.SeriesDate > series2._data.SeriesDate ||
(series1._data.SeriesDate === series2._data.SeriesDate &&
series1._data.SeriesTime > series2._data.SeriesTime)
);
};
+8 -8
View File
@@ -1,23 +1,23 @@
export default function getModalities(modality, modalitiesInStudy) {
if (!modality && !modalitiesInStudy) {
export default function getModalities(Modality, ModalitiesInStudy) {
if (!Modality && !ModalitiesInStudy) {
return {};
}
const modalities = modality || {
const modalities = Modality || {
vr: 'CS',
Value: [],
};
if (modalitiesInStudy) {
if (modalities.vr && modalities.vr === modalitiesInStudy.vr) {
for (let i = 0; i < modalitiesInStudy.Value.length; i++) {
const value = modalitiesInStudy.Value[i];
if (ModalitiesInStudy) {
if (modalities.vr && modalities.vr === ModalitiesInStudy.vr) {
for (let i = 0; i < ModalitiesInStudy.Value.length; i++) {
const value = ModalitiesInStudy.Value[i];
if (modalities.Value.indexOf(value) === -1) {
modalities.Value.push(value);
}
}
} else {
return modalitiesInStudy;
return ModalitiesInStudy;
}
}
@@ -1,74 +1,74 @@
import getModalities from './getModalities';
describe('getModalities', () => {
test('should return an empty object when modality and modalitiesInStudy are not present', () => {
const modality = null;
const modalitiesInStudy = null;
test('should return an empty object when Modality and ModalitiesInStudy are not present', () => {
const Modality = null;
const ModalitiesInStudy = null;
expect(getModalities(modality, modalitiesInStudy)).toEqual({});
expect(getModalities(Modality, ModalitiesInStudy)).toEqual({});
});
test('should return an empty object when modality and modalitiesInStudy are not present', () => {
const modality = null;
const modalitiesInStudy = null;
test('should return an empty object when Modality and ModalitiesInStudy are not present', () => {
const Modality = null;
const ModalitiesInStudy = null;
expect(getModalities(modality, modalitiesInStudy)).toEqual({});
expect(getModalities(Modality, ModalitiesInStudy)).toEqual({});
});
test('should return modalities in Study when modality is not defined', () => {
const modality = null;
test('should return modalities in Study when Modality is not defined', () => {
const Modality = null;
const modalitiesInStudy = {
const ModalitiesInStudy = {
Value: ['MOCKED_VALUE'],
vr: 'MOCKED_VALUE',
};
expect(getModalities(modality, modalitiesInStudy)).toEqual(
modalitiesInStudy
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(
ModalitiesInStudy
);
});
test('should return only the modalitues that exists in modalitiesInStudy', () => {
const modality = {
test('should return only the modalitues that exists in ModalitiesInStudy', () => {
const Modality = {
Value: ['DESIRED_VALUE'],
vr: 'DESIRED_VR',
};
const modalitiesInStudy = {
const ModalitiesInStudy = {
Value: ['DESIRED_VALUE', 'NOT_DESIRED_VALUE'],
vr: 'DESIRED_VR',
};
expect(getModalities(modality, modalitiesInStudy)).toEqual(modality);
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(Modality);
});
test('should return the seek modality when the desired modality does not exist in modalitiesInStudy', () => {
const modality = {
test('should return the seek Modality when the desired Modality does not exist in ModalitiesInStudy', () => {
const Modality = {
Value: ['DESIRED_VALUE'],
vr: 'DESIRED_VR',
};
const modalitiesInStudy = {
const ModalitiesInStudy = {
Value: ['NOT_DESIRED_VALUE'],
vr: 'DESIRED_VR',
};
expect(getModalities(modality, modalitiesInStudy)).toEqual(modality);
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(Modality);
});
test('should return the seek modality when the desired modality does not exist in modalitiesInStudy VR', () => {
const modality = {
test('should return the seek Modality when the desired Modality does not exist in ModalitiesInStudy VR', () => {
const Modality = {
Value: ['DESIRED_VALUE'],
vr: 'DESIRED_VR',
};
const modalitiesInStudy = {
const ModalitiesInStudy = {
Value: ['NOT_DESIRED_VALUE'],
vr: 'ANOTHER_VR',
};
expect(getModalities(modality, modalitiesInStudy)).toEqual(
modalitiesInStudy
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(
ModalitiesInStudy
);
});
});
+1
View File
@@ -1,4 +1,5 @@
export default {
warn: jest.fn(),
error: jest.fn(),
info: jest.fn(),
};
+50 -19
View File
@@ -1,4 +1,4 @@
import hotkeys from './hotkeys';
import hotkeys from './../utils/hotkeys';
import log from './../log.js';
/**
@@ -11,7 +11,7 @@ import log from './../log.js';
*/
export class HotkeysManager {
constructor(commandsManager) {
constructor(commandsManager, servicesManager) {
this.hotkeyDefinitions = {};
this.hotkeyDefaults = [];
this.isEnabled = true;
@@ -22,9 +22,19 @@ export class HotkeysManager {
);
}
this._servicesManager = servicesManager;
this._commandsManager = commandsManager;
}
/**
* Exposes Mousetrap.js's `.record` method, added by the record plugin.
*
* @param {*} event
*/
record(event) {
return hotkeys.record(event);
}
/**
* Disables all hotkeys. Hotkeys added while disabled will not listen for
* input.
@@ -45,38 +55,57 @@ export class HotkeysManager {
/**
* Registers a list of hotkeydefinitions.
*
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
setHotkeys(hotkeyDefinitions) {
const definitions = Array.isArray(hotkeyDefinitions)
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
setHotkeys(hotkeyDefinitions = []) {
try {
const definitions = this._getValidDefinitions(hotkeyDefinitions);
definitions.forEach(definition => this.registerHotkeys(definition));
definitions.forEach(definition => this.registerHotkeys(definition));
} catch (error) {
const { UINotificationService } = this._servicesManager.services;
UINotificationService.show({
title: 'Hotkeys Manager',
message: 'Erro while setting hotkeys',
type: 'error',
});
}
}
/**
* Set default hotkey bindings. These
* values are used in `this.restoreDefaultBindings`.
*
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
setDefaultHotKeys(hotkeyDefinitions) {
setDefaultHotKeys(hotkeyDefinitions = []) {
const definitions = this._getValidDefinitions(hotkeyDefinitions);
this.hotkeyDefaults = definitions;
}
/**
* Take hotkey definitions that can be an array or object and make sure that it
* returns an array of hotkeys
*
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
_getValidDefinitions(hotkeyDefinitions) {
const definitions = Array.isArray(hotkeyDefinitions)
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
this.hotkeyDefaults = definitions;
return definitions;
}
/**
* It parses given object containing hotkeyDefinition to array like.
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
*
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions
* @returns {HotkeyDefinition[]}
*/
_parseToArrayLike(hotkeyDefinitionsObj) {
_parseToArrayLike(hotkeyDefinitionsObj = {}) {
const copy = { ...hotkeyDefinitionsObj };
return Object.entries(copy).map(entryValue =>
this._parseToHotKeyObj(entryValue[0], entryValue[1])
@@ -127,11 +156,13 @@ export class HotkeysManager {
if (previouslyRegisteredDefinition) {
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`);
}
// Set definition & bind
this.hotkeyDefinitions[commandName] = { keys, label };
this._bindHotkeys(commandName, keys);
log.info(`Binding ${commandName} to ${keys}`);
}
/**
@@ -167,12 +198,11 @@ export class HotkeysManager {
}
const isKeyArray = keys instanceof Array;
if (isKeyArray) {
keys.forEach(key => this._bindHotkeys(commandName, key));
return;
}
const combinedKeys = isKeyArray ? keys.join('+') : keys;
hotkeys.bind(keys, evt => {
hotkeys.bind(combinedKeys, evt => {
evt.preventDefault();
evt.stopPropagation();
this._commandsManager.runCommand(commandName, { evt });
});
}
@@ -193,7 +223,8 @@ export class HotkeysManager {
const isKeyArray = keys instanceof Array;
if (isKeyArray) {
keys.forEach(key => this._unbindHotkeys(commandName, key));
const combinedKeys = keys.join('+');
this._unbindHotkeys(commandName, combinedKeys);
return;
}
@@ -1,10 +1,10 @@
import CommandsManager from './CommandsManager.js';
import HotkeysManager from './HotkeysManager.js';
import hotkeys from './hotkeys';
import hotkeys from './../utils/hotkeys';
import log from './../log.js';
jest.mock('./CommandsManager.js');
jest.mock('./hotkeys');
jest.mock('./../utils/hotkeys');
jest.mock('./../log.js');
describe('HotkeysManager', () => {
@@ -60,7 +60,10 @@ describe('HotkeysManager', () => {
});
describe('enable()', () => {
beforeEach(() => hotkeys.unpause.mockClear());
beforeEach(() => {
hotkeys.unpause = jest.fn();
hotkeys.unpause.mockClear();
});
it('sets isEnabled property to true', () => {
hotkeysManager.disable();
@@ -139,20 +142,18 @@ describe('HotkeysManager', () => {
expectedHotkeyDefinition
);
});
it('calls hotkeys.bind for all keys in array', () => {
const definition = { commandName: 'dance', keys: ['h', 'e', 'l', 'o'] };
it('calls hotkeys.bind for the group of keys', () => {
const definition = { commandName: 'dance', keys: ['shift', 'e'] };
hotkeysManager.registerHotkeys(definition);
expect(hotkeys.bind.mock.calls.length).toBe(definition.keys.length);
definition.keys.forEach((key, i) =>
expect(hotkeys.bind.mock.calls[i][0]).toBe(key)
);
expect(hotkeys.bind.mock.calls.length).toBe(1);
expect(hotkeys.bind.mock.calls[0][0]).toBe('shift+e');
});
it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => {
const firstDefinition = {
commandName: 'dance',
keys: ['h', 'e', 'l', 'o'],
keys: ['alt', 'e'],
};
const secondDefinition = { commandName: 'dance', keys: 'a' };
@@ -161,12 +162,8 @@ describe('HotkeysManager', () => {
// Second call
hotkeysManager.registerHotkeys(secondDefinition);
expect(hotkeys.unbind.mock.calls.length).toBe(
firstDefinition.keys.length
);
firstDefinition.keys.forEach((key, i) =>
expect(hotkeys.unbind.mock.calls[i][0]).toBe(key)
);
expect(hotkeys.unbind.mock.calls.length).toBe(1);
expect(hotkeys.unbind.mock.calls[0][0]).toBe('alt+e');
});
});
+55
View File
@@ -1,5 +1,6 @@
import guid from '../utils/guid.js';
import OHIFError from './OHIFError';
import { Vector3 } from 'cornerstone-math';
const OBJECT = 'object';
@@ -63,6 +64,60 @@ class ImageSet {
sortBy(sortingCallback) {
return this.images.sort(sortingCallback);
}
sortByImagePositionPatient() {
const images = this.images;
const referenceImagePositionPatient = _getImagePositionPatient(images[0]);
const refIppVec = new Vector3(
referenceImagePositionPatient[0],
referenceImagePositionPatient[1],
referenceImagePositionPatient[2]
);
const ImageOrientationPatient = _getImageOrientationPatient(images[0]);
const scanAxisNormal = new Vector3(
ImageOrientationPatient[0],
ImageOrientationPatient[1],
ImageOrientationPatient[2]
).cross(
new Vector3(
ImageOrientationPatient[3],
ImageOrientationPatient[4],
ImageOrientationPatient[5]
)
);
const distanceImagePairs = images.map(function(image) {
const ippVec = new Vector3(..._getImagePositionPatient(image));
const positionVector = refIppVec.clone().sub(ippVec);
const distance = positionVector.dot(scanAxisNormal);
return {
distance,
image,
};
});
distanceImagePairs.sort(function(a, b) {
return b.distance - a.distance;
});
const sortedImages = distanceImagePairs.map(a => a.image);
images.sort(function(a, b) {
return sortedImages.indexOf(a) - sortedImages.indexOf(b);
});
}
}
function _getImagePositionPatient(image) {
return image.getData().metadata.ImagePositionPatient;
}
function _getImageOrientationPatient(image) {
return image.getData().metadata.ImageOrientationPatient;
}
export default ImageSet;
+448 -350
View File
@@ -1,412 +1,510 @@
import { parsingUtils } from '../lib/parsingUtils';
const FUNCTION = 'function';
import * as dcmjs from 'dcmjs';
import queryString from 'query-string';
import dicomParser from 'dicom-parser';
import { getFallbackTagFromInstance } from '../utils/metadataProvider/metadataProviderFallbackTags';
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData';
class MetadataProvider {
constructor() {
// Define the main "metadataLookup" private property as an immutable property.
Object.defineProperty(this, 'metadataLookup', {
Object.defineProperty(this, 'studies', {
configurable: false,
enumerable: false,
writable: false,
value: new Map(),
});
// Local reference to provider function bound to current instance.
Object.defineProperty(this, '_provider', {
Object.defineProperty(this, 'imageIdToUIDs', {
configurable: false,
enumerable: false,
writable: true,
value: null,
writable: false,
value: new Map(),
});
}
/**
* Cornerstone Metadata provider to store image meta data
* Data from instances, series, and studies are associated with
* imageIds to facilitate usage of this information by Cornerstone's Tools
*
* e.g. the imagePlane metadata object contains instance information about
* row/column pixel spacing, patient position, and patient orientation. It
* is used in CornerstoneTools to position reference lines and orientation markers.
*
* @param {String} imageId The Cornerstone ImageId
* @param {Object} data An object containing instance, series, and study metadata
*/
addMetadata(imageId, data) {
const instanceMetadata = data.instance;
const seriesMetadata = data.series;
const studyMetadata = data.study;
const numImages = data.numImages;
const metadata = {};
async addInstance(dicomJSONDatasetOrP10ArrayBuffer, options = {}) {
let dicomJSONDataset;
metadata.frameNumber = data.frameNumber;
// If Arraybuffer, parse to DICOMJSON before naturalizing.
if (dicomJSONDatasetOrP10ArrayBuffer instanceof ArrayBuffer) {
const dicomData = DicomMessage.readFile(dicomJSONDatasetOrP10ArrayBuffer);
metadata.study = {
accessionNumber: studyMetadata.accessionNumber,
patientId: studyMetadata.patientId,
studyInstanceUid: studyMetadata.studyInstanceUid,
studyDate: studyMetadata.studyDate,
studyTime: studyMetadata.studyTime,
studyDescription: studyMetadata.studyDescription,
institutionName: studyMetadata.institutionName,
patientHistory: studyMetadata.patientHistory,
};
metadata.series = {
seriesDescription: seriesMetadata.seriesDescription,
seriesNumber: seriesMetadata.seriesNumber,
seriesDate: seriesMetadata.seriesDate,
seriesTime: seriesMetadata.seriesTime,
modality: seriesMetadata.modality,
seriesInstanceUid: seriesMetadata.seriesInstanceUid,
numImages: numImages,
};
metadata.instance = instanceMetadata;
metadata.patient = {
name: studyMetadata.patientName,
id: studyMetadata.patientId,
birthDate: studyMetadata.patientBirthDate,
sex: studyMetadata.patientSex,
age: studyMetadata.patientAge,
};
// If there is sufficient information, populate
// the imagePlane object for easier use in the Viewer
metadata.imagePlane = this.getImagePlane(instanceMetadata);
// Add the metadata to the imageId lookup object
this.metadataLookup.set(imageId, metadata);
}
/**
* Return the metadata for the given imageId
* @param {String} imageId The Cornerstone ImageId
* @returns image metadata
*/
getMetadata(imageId) {
return this.metadataLookup.get(imageId);
}
/**
* Adds a set of metadata to the Cornerstone metadata provider given a specific
* imageId, type, and dataset
*
* @param imageId
* @param type (e.g. series, instance, tagDisplay)
* @param data
*/
addSpecificMetadata(imageId, type, data) {
const metadata = {};
metadata[type] = data;
const oldMetadata = this.metadataLookup.get(imageId);
this.metadataLookup.set(imageId, Object.assign(oldMetadata, metadata));
}
getFromImage(image, type, tag, attrName, defaultValue) {
let value;
if (image.data) {
value = this.getFromDataSet(image.data, type, tag);
dicomJSONDataset = dicomData.dict;
} else {
value = image.instance[attrName];
dicomJSONDataset = dicomJSONDatasetOrP10ArrayBuffer;
}
return value === null ? defaultValue : value;
}
// Check if dataset is already naturalized.
getFromDataSet(dataSet, type, tag) {
if (!dataSet) {
return;
}
let naturalizedDataset;
const fn = dataSet[type];
if (!fn) {
return;
}
return fn.call(dataSet, tag);
}
getFrameIncrementPointer(image) {
const dataSet = image.data;
let frameInstancePointer = '';
if (parsingUtils.isValidDataSet(dataSet)) {
const frameInstancePointerNames = {
x00181063: 'frameTime',
x00181065: 'frameTimeVector',
};
// (0028,0009) = Frame Increment Pointer
const frameInstancePointerTag = parsingUtils.attributeTag(
dataSet,
'x00280009'
if (dicomJSONDataset['SeriesInstanceUID'] === undefined) {
naturalizedDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomJSONDataset
);
frameInstancePointer = frameInstancePointerNames[frameInstancePointerTag];
} else {
frameInstancePointer = image.instance.frameIncrementPointer;
naturalizedDataset = dicomJSONDataset;
}
return frameInstancePointer || '';
const {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
} = naturalizedDataset;
const study = this._getAndCacheStudy(StudyInstanceUID);
const series = this._getAndCacheSeriesFromStudy(study, SeriesInstanceUID);
const instance = this._getAndCacheInstanceFromStudy(series, SOPInstanceUID);
Object.assign(instance, naturalizedDataset);
await this._checkBulkDataAndInlineBinaries(instance, options.server);
return instance;
}
getFrameTimeVector(image) {
const dataSet = image.data;
addImageIdToUIDs(imageId, uids) {
// This method is a fallback for when you don't have WADO-URI or WADO-RS.
// You can add instances fetched by any method by calling addInstance, and hook an imageId to point at it here.
// An example would be dicom hosted at some random site.
if (parsingUtils.isValidDataSet(dataSet)) {
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
return parsingUtils.floatArray(dataSet, 'x00181065');
this.imageIdToUIDs.set(imageId, uids);
}
_getAndCacheStudy(StudyInstanceUID) {
const studies = this.studies;
let study = studies.get(StudyInstanceUID);
if (!study) {
study = { series: new Map() };
studies.set(StudyInstanceUID, study);
}
return image.instance.frameTimeVector;
return study;
}
_getAndCacheSeriesFromStudy(study, SeriesInstanceUID) {
let series = study.series.get(SeriesInstanceUID);
getFrameTime(image) {
const dataSet = image.data;
if (parsingUtils.isValidDataSet(dataSet)) {
// Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility).
// Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups)
// but that case is currently not supported.
return dataSet.floatString('x00181063', -1);
if (!series) {
series = { instances: new Map() };
study.series.set(SeriesInstanceUID, series);
}
return image.instance.frameTime;
return series;
}
/**
* Updates the related metadata for missing fields given a specified image
*
* @param image
*/
updateMetadata(image) {
const imageMetadata = this.metadataLookup.get(image.imageId);
if (!imageMetadata) {
_getAndCacheInstanceFromStudy(series, SOPInstanceUID) {
let instance = series.instances.get(SOPInstanceUID);
if (!instance) {
instance = {};
series.instances.set(SOPInstanceUID, instance);
}
return instance;
}
async _checkBulkDataAndInlineBinaries(instance, server) {
await fetchOverlayData(instance, server);
if (instance.PhotometricInterpretation === 'PALETTE COLOR') {
await fetchPaletteColorLookupTableData(instance, server);
}
}
_getInstance(imageId) {
const uids = this._getUIDsFromImageID(imageId);
if (!uids) {
return;
}
imageMetadata.patient.age =
imageMetadata.patient.age ||
this.getFromDataSet(image.data, 'string', 'x00101010');
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = uids;
imageMetadata.instance.rows = imageMetadata.instance.rows || image.rows;
imageMetadata.instance.columns =
imageMetadata.instance.columns || image.columns;
imageMetadata.instance.sopClassUid =
imageMetadata.instance.sopClassUid ||
this.getFromDataSet(image.data, 'string', 'x00080016');
imageMetadata.instance.sopInstanceUid =
imageMetadata.instance.sopInstanceUid ||
this.getFromDataSet(image.data, 'string', 'x00080018');
imageMetadata.instance.pixelSpacing =
imageMetadata.instance.pixelSpacing ||
this.getFromDataSet(image.data, 'string', 'x00280030');
imageMetadata.instance.frameOfReferenceUID =
imageMetadata.instance.frameOfReferenceUID ||
this.getFromDataSet(image.data, 'string', 'x00200052');
imageMetadata.instance.imageOrientationPatient =
imageMetadata.instance.imageOrientationPatient ||
this.getFromDataSet(image.data, 'string', 'x00200037');
imageMetadata.instance.imagePositionPatient =
imageMetadata.instance.imagePositionPatient ||
this.getFromDataSet(image.data, 'string', 'x00200032');
imageMetadata.instance.sliceThickness =
imageMetadata.instance.sliceThickness ||
this.getFromDataSet(image.data, 'string', 'x00180050');
imageMetadata.instance.sliceLocation =
imageMetadata.instance.sliceLocation ||
this.getFromDataSet(image.data, 'string', 'x00201041');
imageMetadata.instance.tablePosition =
imageMetadata.instance.tablePosition ||
this.getFromDataSet(image.data, 'string', 'x00189327');
imageMetadata.instance.spacingBetweenSlices =
imageMetadata.instance.spacingBetweenSlices ||
this.getFromDataSet(image.data, 'string', 'x00180088');
imageMetadata.instance.lossyImageCompression =
imageMetadata.instance.lossyImageCompression ||
this.getFromDataSet(image.data, 'string', 'x00282110');
imageMetadata.instance.lossyImageCompressionRatio =
imageMetadata.instance.lossyImageCompressionRatio ||
this.getFromDataSet(image.data, 'string', 'x00282112');
imageMetadata.instance.frameIncrementPointer =
imageMetadata.instance.frameIncrementPointer ||
this.getFromDataSet(image.data, 'string', 'x00280009');
imageMetadata.instance.frameTime =
imageMetadata.instance.frameTime ||
this.getFromDataSet(image.data, 'string', 'x00181063');
imageMetadata.instance.frameTimeVector =
imageMetadata.instance.frameTimeVector ||
this.getFromDataSet(image.data, 'string', 'x00181065');
if (
(image.data || image.instance) &&
!imageMetadata.instance.multiframeMetadata
) {
imageMetadata.instance.multiframeMetadata = this.getMultiframeModuleMetadata(
image
);
}
imageMetadata.imagePlane =
imageMetadata.imagePlane || this.getImagePlane(imageMetadata.instance);
}
/**
* Constructs and returns the imagePlane given the metadata instance
*
* @param metadataInstance The metadata instance (InstanceMetadata class) containing information to construct imagePlane
* @returns imagePlane The constructed imagePlane to be used in viewer easily
*/
getImagePlane(instance) {
if (
!instance.rows ||
!instance.columns ||
!instance.pixelSpacing ||
!instance.frameOfReferenceUID ||
!instance.imageOrientationPatient ||
!instance.imagePositionPatient
) {
return;
}
const imageOrientation = instance.imageOrientationPatient.split('\\');
const imagePosition = instance.imagePositionPatient.split('\\');
let columnPixelSpacing = 1.0;
let rowPixelSpacing = 1.0;
if (instance.pixelSpacing) {
const split = instance.pixelSpacing.split('\\');
rowPixelSpacing = parseFloat(split[0]);
columnPixelSpacing = parseFloat(split[1]);
}
return {
frameOfReferenceUID: instance.frameOfReferenceUID,
rows: instance.rows,
columns: instance.columns,
rowCosines: [
parseFloat(imageOrientation[0]),
parseFloat(imageOrientation[1]),
parseFloat(imageOrientation[2]),
],
columnCosines: [
parseFloat(imageOrientation[3]),
parseFloat(imageOrientation[4]),
parseFloat(imageOrientation[5]),
],
imagePositionPatient: [
parseFloat(imagePosition[0]),
parseFloat(imagePosition[1]),
parseFloat(imagePosition[2]),
],
rowPixelSpacing,
columnPixelSpacing,
};
}
/**
* This function extracts miltiframe information from a dicomParser.DataSet object.
*
* @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found.
* @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc).
*/
getMultiframeModuleMetadata(image) {
const imageInfo = {
isMultiframeImage: false,
frameIncrementPointer: null,
numberOfFrames: 0,
frameTime: 0,
frameTimeVector: null,
averageFrameRate: 0, // backwards compatibility only... it might be useless in the future
};
let frameTime;
const numberOfFrames = this.getFromImage(
image,
'intString',
'x00280008',
'numberOfFrames',
-1
return this._getInstanceData(
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID
);
}
if (numberOfFrames > 0) {
// set multi-frame image indicator
imageInfo.isMultiframeImage = true;
imageInfo.numberOfFrames = numberOfFrames;
get(query, imageId, options = { fallback: false }) {
const instance = this._getInstance(imageId);
// (0028,0009) = Frame Increment Pointer
const frameIncrementPointer = this.getFrameIncrementPointer(image);
if (query === INSTANCE) {
return instance;
}
if (frameIncrementPointer === 'frameTimeVector') {
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
const frameTimeVector = this.getFrameTimeVector(image);
return this.getTagFromInstance(query, instance, options);
}
if (frameTimeVector instanceof Array && frameTimeVector.length > 0) {
imageInfo.frameIncrementPointer = frameIncrementPointer;
imageInfo.frameTimeVector = frameTimeVector;
frameTime =
frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length;
imageInfo.averageFrameRate = 1000 / frameTime;
}
} else if (
frameIncrementPointer === 'frameTime' ||
frameIncrementPointer === ''
) {
frameTime = this.getFrameTime(image);
getTag(query, imageId, options) {
return this.get(query, imageId, options);
}
if (frameTime > 0) {
imageInfo.frameIncrementPointer = frameIncrementPointer;
imageInfo.frameTime = frameTime;
imageInfo.averageFrameRate = 1000 / frameTime;
}
getInstance(imageId) {
return this.get(INSTANCE, imageId);
}
getTagFromInstance(
naturalizedTagOrWADOImageLoaderTag,
instance,
options = { fallback: false }
) {
if (!instance) {
return;
}
// If its a naturalized dcmjs tag present on the instance, return.
if (instance[naturalizedTagOrWADOImageLoaderTag]) {
return instance[naturalizedTagOrWADOImageLoaderTag];
}
if (options.fallback) {
// Perhaps the tag has fallbacks?
const fallbackTag = getFallbackTagFromInstance(
naturalizedTagOrWADOImageLoaderTag,
instance
);
if (fallbackTag) {
return fallbackTag;
}
}
return imageInfo;
// Maybe its a legacy CornerstoneWADOImageLoader tag then:
return this._getCornerstoneWADOImageLoaderTag(
naturalizedTagOrWADOImageLoaderTag,
instance
);
}
/**
* Get a bound reference to the provider function.
*/
getProvider() {
let provider = this._provider;
if (typeof this._provider !== FUNCTION) {
provider = this.provider.bind(this);
this._provider = provider;
_getCornerstoneWADOImageLoaderTag(wadoImageLoaderTag, instance) {
let metadata;
switch (wadoImageLoaderTag) {
case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE:
const { SeriesDate, SeriesTime } = instance;
let seriesDate;
let seriesTime;
if (SeriesDate) {
seriesDate = dicomParser.parseDA(SeriesDate);
}
if (SeriesTime) {
seriesTime = dicomParser.parseTM(SeriesTime);
}
metadata = {
modality: instance.Modality,
seriesInstanceUID: instance.SeriesInstanceUID,
seriesNumber: instance.SeriesNumber,
studyInstanceUID: instance.StudyInstanceUID,
seriesDate,
seriesTime,
};
break;
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
metadata = {
patientAge: instance.PatientAge,
patientSize: instance.PatientSize,
patientWeight: instance.PatientWeight,
};
break;
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
const { ImageOrientationPatient } = instance;
// Fallback for DX images.
const PixelSpacing = getFallbackTagFromInstance(
'PixelSpacing',
instance
);
let rowPixelSpacing;
let columnPixelSpacing;
let rowCosines;
let columnCosines;
if (PixelSpacing) {
rowPixelSpacing = PixelSpacing[0];
columnPixelSpacing = PixelSpacing[1];
}
if (ImageOrientationPatient) {
rowCosines = ImageOrientationPatient.slice(0, 3);
columnCosines = ImageOrientationPatient.slice(3, 6);
}
metadata = {
frameOfReferenceUID: instance.FrameOfReferenceUID,
rows: instance.Rows,
columns: instance.Columns,
imageOrientationPatient: ImageOrientationPatient,
rowCosines,
columnCosines,
imagePositionPatient: instance.ImagePositionPatient,
sliceThickness: instance.SliceThickness,
sliceLocation: instance.SliceLocation,
pixelSpacing: PixelSpacing,
rowPixelSpacing,
columnPixelSpacing,
};
break;
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
metadata = {
samplesPerPixel: instance.SamplesPerPixel,
photometricInterpretation: instance.PhotometricInterpretation,
rows: instance.Rows,
columns: instance.Columns,
bitsAllocated: instance.BitsAllocated,
bitsStored: instance.BitsStored,
highBit: instance.HighBit,
pixelRepresentation: instance.PixelRepresentation,
planarConfiguration: instance.PlanarConfiguration,
pixelAspectRatio: instance.PixelAspectRatio,
smallestPixelValue: instance.SmallestPixelValue,
largestPixelValue: instance.LargestPixelValue,
redPaletteColorLookupTableDescriptor:
instance.RedPaletteColorLookupTableDescriptor,
greenPaletteColorLookupTableDescriptor:
instance.GreenPaletteColorLookupTableDescriptor,
bluePaletteColorLookupTableDescriptor:
instance.BluePaletteColorLookupTableDescriptor,
redPaletteColorLookupTableData:
instance.RedPaletteColorLookupTableData,
greenPaletteColorLookupTableData:
instance.GreenPaletteColorLookupTableData,
bluePaletteColorLookupTableData:
instance.BluePaletteColorLookupTableData,
};
break;
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
const { WindowCenter, WindowWidth } = instance;
const windowCenter = Array.isArray(WindowCenter)
? WindowCenter
: [WindowCenter];
const windowWidth = Array.isArray(WindowWidth)
? WindowWidth
: [WindowWidth];
metadata = {
windowCenter,
windowWidth,
};
break;
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
metadata = {
rescaleIntercept: instance.RescaleIntercept,
rescaleSlope: instance.RescaleSlope,
rescaleType: instance.RescaleType,
};
break;
case WADO_IMAGE_LOADER_TAGS.SOP_COMMON_MODULE:
metadata = {
sopClassUID: instance.SOPClassUID,
sopInstanceUID: instance.SOPInstanceUID,
};
break;
case WADO_IMAGE_LOADER_TAGS.PET_ISOTOPE_MODULE:
const { RadiopharmaceuticalInformationSequence } = instance;
if (RadiopharmaceuticalInformationSequence) {
const RadiopharmaceuticalInformation = Array.isArray(
RadiopharmaceuticalInformationSequence
)
? RadiopharmaceuticalInformationSequence[0]
: RadiopharmaceuticalInformationSequence;
const {
RadiopharmaceuticalStartTime,
RadionuclideTotalDose,
RadionuclideHalfLife,
} = RadiopharmaceuticalInformation;
const radiopharmaceuticalInfo = {
radiopharmaceuticalStartTime: dicomParser.parseTM(
RadiopharmaceuticalStartTime
),
radionuclideTotalDose: RadionuclideTotalDose,
radionuclideHalfLife: RadionuclideHalfLife,
};
metadata = {
radiopharmaceuticalInfo,
};
}
break;
case WADO_IMAGE_LOADER_TAGS.OVERLAY_PLANE_MODULE:
const overlays = [];
for (
let overlayGroup = 0x00;
overlayGroup <= 0x1e;
overlayGroup += 0x02
) {
let groupStr = `60${overlayGroup.toString(16)}`;
if (groupStr.length === 3) {
groupStr = `600${overlayGroup.toString(16)}`;
}
const OverlayDataTag = `${groupStr}3000`;
const OverlayData = instance[OverlayDataTag];
if (!OverlayData) {
continue;
}
const OverlayRowsTag = `${groupStr}0010`;
const OverlayColumnsTag = `${groupStr}0011`;
const OverlayType = `${groupStr}0040`;
const OverlayOriginTag = `${groupStr}0050`;
const OverlayDescriptionTag = `${groupStr}0022`;
const OverlayLabelTag = `${groupStr}1500`;
const ROIAreaTag = `${groupStr}1301`;
const ROIMeanTag = `${groupStr}1302`;
const ROIStandardDeviationTag = `${groupStr}1303`;
const OverlayOrigin = instance[OverlayOriginTag];
const overlay = {
rows: instance[OverlayRowsTag],
columns: instance[OverlayColumnsTag],
type: instance[OverlayType],
x: OverlayOrigin[0],
y: OverlayOrigin[1],
pixelData: OverlayData,
description: instance[OverlayDescriptionTag],
label: instance[OverlayLabelTag],
roiArea: instance[ROIAreaTag],
roiMean: instance[ROIMeanTag],
roiStandardDeviation: instance[ROIStandardDeviationTag],
};
overlays.push(overlay);
}
metadata = {
overlays
};
break;
case WADO_IMAGE_LOADER_TAGS.PATIENT_MODULE:
const { PatientName } = instance;
let patientName;
if (PatientName) {
patientName = PatientName.Alphabetic;
}
metadata = {
patientName,
patientId: instance.PatientID,
};
break;
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
metadata = {
instanceNumber: instance.InstanceNumber,
lossyImageCompression: instance.LossyImageCompression,
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
lossyImageCompressionMethod: instance.LossyImageCompressionMethod,
};
break;
case WADO_IMAGE_LOADER_TAGS.GENERAL_STUDY_MODULE:
metadata = {
studyDescription: instance.StudyDescription,
studyDate: instance.StudyDate,
studyTime: instance.StudyTime,
accessionNumber: instance.AccessionNumber,
};
break;
case WADO_IMAGE_LOADER_TAGS.CINE_MODULE:
metadata = {
frameTime: instance.FrameTime,
};
break;
}
return provider;
return metadata;
}
/**
* Looks up metadata for Cornerstone Tools given a specified type and imageId
* A type may be, e.g. 'study', or 'patient', or 'imagePlaneModule'. These types
* are keys in the stored metadata objects.
*
* @param type
* @param imageId
* @returns {Object} Relevant metadata of the specified type
*/
provider(type, imageId) {
const imageMetadata = this.metadataLookup.get(imageId);
if (!imageMetadata) {
_getInstanceData(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
const study = this.studies.get(StudyInstanceUID);
if (!study) {
return;
}
if (imageMetadata.hasOwnProperty(type)) {
return imageMetadata[type];
const series = study.series.get(SeriesInstanceUID);
if (!series) {
return;
}
const instance = series.instances.get(SOPInstanceUID);
return instance;
}
_getUIDsFromImageID(imageId) {
if (imageId.includes('wadors:')) {
const strippedImageId = imageId.split('studies/')[1];
const splitImageId = strippedImageId.split('/');
return {
StudyInstanceUID: splitImageId[0], // Note: splitImageId[1] === 'series'
SeriesInstanceUID: splitImageId[2], // Note: splitImageId[3] === 'instances'
SOPInstanceUID: splitImageId[4],
};
}
if (imageId.includes('wado?requestType=WADO')) {
const qs = queryString.parse(imageId);
return {
StudyInstanceUID: qs.studyUID,
SeriesInstanceUID: qs.seriesUID,
SOPInstanceUID: qs.objectUID,
};
} else {
// Maybe its a non-standard imageId
return this.imageIdToUIDs.get(imageId);
}
}
}
export default MetadataProvider;
const metadataProvider = new MetadataProvider();
export default metadataProvider;
const WADO_IMAGE_LOADER_TAGS = {
// CornerstoneWADOImageLoader specific
GENERAL_SERIES_MODULE: 'generalSeriesModule',
PATIENT_STUDY_MODULE: 'patientStudyModule',
IMAGE_PLANE_MODULE: 'imagePlaneModule',
IMAGE_PIXEL_MODULE: 'imagePixelModule',
VOI_LUT_MODULE: 'voiLutModule',
MODALITY_LUT_MODULE: 'modalityLutModule',
SOP_COMMON_MODULE: 'sopCommonModule',
PET_ISOTOPE_MODULE: 'petIsotopeModule',
OVERLAY_PLANE_MODULE: 'overlayPlaneModule',
// react-cornerstone-viewport specifc
PATIENT_MODULE: 'patientModule',
GENERAL_IMAGE_MODULE: 'generalImageModule',
GENERAL_STUDY_MODULE: 'generalStudyModule',
CINE_MODULE: 'cineModule',
};
const INSTANCE = 'instance';
@@ -1,9 +1,8 @@
import { studyMetadataManager, updateMetaDataManager } from '../utils';
import { studyMetadataManager } from '../utils';
import OHIFError from './OHIFError';
import { StudyMetadata } from './metadata/StudyMetadata';
import { StudyMetadataSource } from './StudyMetadataSource.js';
import { StudySummary } from './metadata/StudySummary';
import { retrieveStudyMetadata } from '../studies/retrieveStudyMetadata.js';
export class OHIFStudyMetadataSource extends StudyMetadataSource {
@@ -19,12 +18,12 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
/**
* Load study info (OHIF.viewer.Studies) and study metadata (OHIF.viewer.StudyMetadataList) for a given study.
* @param {StudySummary|StudyMetadata} study of StudySummary or StudyMetadata object.
* @param {StudyMetadata} study StudyMetadata object.
*/
loadStudy(study) {
if (!(study instanceof StudyMetadata) && !(study instanceof StudySummary)) {
if (!(study instanceof StudyMetadata)) {
throw new OHIFError(
'OHIFStudyMetadataSource::loadStudy study is not an instance of StudySummary or StudyMetadata'
'OHIFStudyMetadataSource::loadStudy study is not an instance of StudyMetadata'
);
}
@@ -33,7 +32,7 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
if (study instanceof StudyMetadata) {
const alreadyLoaded = OHIF.viewer.Studies.findBy({
studyInstanceUid: studyInstanceUID,
StudyInstanceUID: studyInstanceUID,
});
if (!alreadyLoaded) {
@@ -49,7 +48,7 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
// Create study metadata object
const studyMetadata = new StudyMetadata(
studyInfo,
studyInfo.studyInstanceUid
studyInfo.StudyInstanceUID
);
// Get Study display sets
@@ -72,10 +71,6 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
// Set some studyInfo properties
studyInfo.selected = true;
studyInfo.displaySets = studyMetadata.getDisplaySets();
// Updates WADO-RS metaDataManager
updateMetaDataManager(studyInfo);
studyMetadataManager.add(studyMetadata);
}
}
@@ -1,55 +0,0 @@
import StudySummary from './StudySummary';
/**
* Constants
*/
const STRING = 'string';
const propertyReplacementMap = {
modalities: 'ModalitiesInStudy',
patientBirthdate: 'PatientBirthDate',
};
/**
* OHIF Viewers specialized version of StudySummary class
*/
export class OHIFStudySummary extends StudySummary {
// @Override
addTags(tagMap) {
const _hasOwn = Object.prototype.hasOwnProperty;
const _tagMap = Object.create(null);
for (let property in tagMap) {
if (_hasOwn.call(tagMap, property)) {
let standardProperty = OHIFStudySummary.getStandardPropertyName(
property
);
if (standardProperty) {
_tagMap[standardProperty] = tagMap[property];
}
}
}
super.addTags(_tagMap);
}
/**
* Turns a non-standard, OHIF specific, DICOM property name into a standard one.
* @param {string} property A string representing a non-conforming keyword.
* @returns {string|undefined} Returns a standard-conforming property name.
*/
static getStandardPropertyName(property) {
let standardProperty;
if (typeof property === STRING && property.charAt(0) !== '_') {
if (property in propertyReplacementMap) {
standardProperty = propertyReplacementMap[propertyReplacementMap];
} else {
standardProperty = property
.replace(/^sop/, 'SOP')
.replace(/Uid$/, 'UID')
.replace(/Id$/, 'ID');
standardProperty =
standardProperty.charAt(0).toUpperCase() + standardProperty.substr(1);
}
}
return standardProperty;
}
}
@@ -60,8 +60,8 @@ class BaseLoadingListener {
}
_getProgressId() {
const displaySetInstanceUid = this.stack.displaySetInstanceUid;
return 'StackProgress:' + displaySetInstanceUid;
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
return 'StackProgress:' + displaySetInstanceUID;
}
_clearProgress() {
@@ -362,7 +362,7 @@ class StackLoadingListener extends BaseLoadingListener {
_logProgress() {
const totalFramesCount = this.stack.imageIds.length;
const displaySetInstanceUid = this.stack.displaySetInstanceUid;
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
let progressBar = '[';
for (let i = 0; i < totalFramesCount; i++) {
@@ -371,7 +371,7 @@ class StackLoadingListener extends BaseLoadingListener {
}
progressBar += ']';
log.info(`${displaySetInstanceUid}: ${progressBar}`);
log.info(`${displaySetInstanceUID}: ${progressBar}`);
}
}
@@ -388,12 +388,12 @@ class StudyLoadingListener {
return;
}
const displaySetInstanceUid = stack.displaySetInstanceUid;
const displaySetInstanceUID = stack.displaySetInstanceUID;
if (!this.listeners[displaySetInstanceUid]) {
if (!this.listeners[displaySetInstanceUID]) {
const listener = this._createListener(stack, stackMetaData);
if (listener) {
this.listeners[displaySetInstanceUid] = listener;
this.listeners[displaySetInstanceUID] = listener;
}
}
}
@@ -424,12 +424,12 @@ class StudyLoadingListener {
}
clear() {
const displaySetInstanceUids = Object.keys(this.listeners);
const length = displaySetInstanceUids.length;
const displaySetInstanceUIDs = Object.keys(this.listeners);
const length = displaySetInstanceUIDs.length;
for (let i = 0; i < length; i++) {
const displaySetInstanceUid = displaySetInstanceUids[i];
const displaySet = this.listeners[displaySetInstanceUid];
const displaySetInstanceUID = displaySetInstanceUIDs[i];
const displaySet = this.listeners[displaySetInstanceUID];
displaySet.destroy();
}
@@ -19,7 +19,7 @@ export class StudyMetadataSource {
/**
* Load study info and study metadata for a given study into the viewer.
* @param {StudySummary|StudyMetadata} study of StudySummary or StudyMetadata object.
* @param {StudyMetadata} study StudyMetadata object.
*/
loadStudy(study) {
/**
+12 -6
View File
@@ -94,17 +94,23 @@ export class StudyPrefetcher {
}
getStudy(image) {
const studyMetadata = cornerstone.metaData.get('study', image.imageId);
const StudyInstanceUID = cornerstone.metaData.get(
'StudyInstanceUID',
image.imageId
);
return OHIF.viewer.Studies.find(
study => study.studyInstanceUid === studyMetadata.studyInstanceUid
study => study.StudyInstanceUID === StudyInstanceUID
);
}
getSeries(study, image) {
const seriesMetadata = cornerstone.metaData.get('series', image.imageId);
const SeriesInstanceUID = cornerstone.metaData.get(
'SeriesInstanceUID',
image.imageId
);
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
return studyMetadata.getSeriesByUID(seriesMetadata.seriesInstanceUid);
return studyMetadata.getSeriesByUID(SeriesInstanceUID);
}
getInstance(series, image) {
@@ -112,13 +118,13 @@ export class StudyPrefetcher {
'instance',
image.imageId
);
return series.getInstanceByUID(instanceMetadata.sopInstanceUid);
return series.getInstanceByUID(instanceMetadata.SOPInstanceUID);
}
getActiveDisplaySet(displaySets, instance) {
return displaySets.find(displaySet => {
return displaySet.images.some(displaySetImage => {
return displaySetImage.sopInstanceUid === instance.sopInstanceUid;
return displaySetImage.SOPInstanceUID === instance.SOPInstanceUID;
});
});
}
@@ -1,17 +0,0 @@
// Only imported in environment w/ `window`
// So we need to mock these for tests
import Mousetrap from 'mousetrap';
import pausePlugin from 'mousetrap/plugins/pause/mousetrap-pause.js';
import recordPlugin from 'mousetrap/plugins/record/mousetrap-record.js';
// import pausePlugin from './pausePlugin.js';
// import recordPlugin from './recordPlugin.js';
// // // TODO: May need to bind these so Mousetrap = this in plugins;
// pausePlugin(Mousetrap);
// recordPlugin(Mousetrap);
// console.log(Mousetrap);
// console.log(Object.keys(Mousetrap));
export default Mousetrap;
+2 -9
View File
@@ -1,15 +1,10 @@
import {
InstanceMetadata,
SeriesMetadata,
StudyMetadata,
StudySummary,
} from './metadata';
import { InstanceMetadata, SeriesMetadata, StudyMetadata } from './metadata';
import CommandsManager from './CommandsManager.js';
import { DICOMFileLoadingListener } from './StudyLoadingListener';
import HotkeysManager from './HotkeysManager.js';
import ImageSet from './ImageSet';
import MetadataProvider from './MetadataProvider.js';
import MetadataProvider from './MetadataProvider';
import OHIFError from './OHIFError.js';
import { OHIFStudyMetadataSource } from './OHIFStudyMetadataSource';
import { StackLoadingListener } from './StudyLoadingListener';
@@ -31,7 +26,6 @@ export {
StudyMetadata,
SeriesMetadata,
InstanceMetadata,
StudySummary,
TypeSafeCollection,
OHIFError,
StudyMetadataSource,
@@ -50,7 +44,6 @@ const classes = {
StudyMetadata,
SeriesMetadata,
InstanceMetadata,
StudySummary,
TypeSafeCollection,
OHIFError,
StudyMetadataSource,
@@ -8,20 +8,12 @@ import OHIFError from '../OHIFError.js';
const UNDEFINED = 'undefined';
const STRING = 'string';
const STUDY_INSTANCE_UID = 'x0020000d';
const SERIES_INSTANCE_UID = 'x0020000e';
export class InstanceMetadata extends Metadata {
constructor(data, uid) {
super(data, uid);
// Initialize Private Properties
Object.defineProperties(this, {
_sopInstanceUID: {
configurable: true, // configurable so that it can be redefined in sub-classes...
enumerable: false,
writable: true,
value: null,
},
_imageId: {
configurable: true, // configurable so that it can be redefined in sub-classes...
enumerable: false,
@@ -43,14 +35,14 @@ export class InstanceMetadata extends Metadata {
*/
_definePublicProperties() {
/**
* Property: this.sopInstanceUID
* Property: this.SOPInstanceUID
* Same as this.getSOPInstanceUID()
* It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
* sopInstanceCollection.findBy({
* sopInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
* SOPInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
* });
*/
Object.defineProperty(this, 'sopInstanceUID', {
Object.defineProperty(this, 'SOPInstanceUID', {
configurable: false,
enumerable: false,
get: function() {
@@ -67,21 +59,21 @@ export class InstanceMetadata extends Metadata {
* Returns the StudyInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
*/
getStudyInstanceUID() {
return this.getTagValue(STUDY_INSTANCE_UID, null);
return this.getTagValue('StudyInstanceUID', null);
}
/**
* Returns the SeriesInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
*/
getSeriesInstanceUID() {
return this.getTagValue(SERIES_INSTANCE_UID, null);
return this.getTagValue('SeriesInstanceUID', null);
}
/**
* Returns the SOPInstanceUID of the current instance.
*/
getSOPInstanceUID() {
return this._sopInstanceUID;
return this.getTagValue('SOPInstanceUID', null);
}
// @TODO: Improve this... (E.g.: blob data)
@@ -127,13 +119,6 @@ export class InstanceMetadata extends Metadata {
return typeof value === STRING ? parseInt(value) : value;
}
/**
* @deprecated Please use getTagValue instead.
*/
getRawValue(tagOrProperty, defaultValue) {
return this.getTagValue(tagOrProperty, defaultValue);
}
/**
* This function should be overriden by specialized classes in order to allow client libraries or viewers to take advantage of the Study Metadata API.
*/
@@ -1,5 +1,4 @@
import { InstanceMetadata } from './InstanceMetadata';
import { DICOMTagDescriptions } from '../../utils/DICOMTagDescriptions.js';
import getImageId from '../../utils/getImageId.js';
export class OHIFInstanceMetadata extends InstanceMetadata {
@@ -20,7 +19,7 @@ export class OHIFInstanceMetadata extends InstanceMetadata {
configurable: false,
enumerable: false,
writable: false,
value: instance.sopInstanceUid,
value: instance.SOPInstanceUID,
},
_study: {
configurable: false,
@@ -56,16 +55,16 @@ export class OHIFInstanceMetadata extends InstanceMetadata {
return this._cache[tagOrProperty];
}
const propertyName = OHIFInstanceMetadata.getPropertyName(tagOrProperty);
const instanceData = this._instance.metadata;
// Search property value in the whole study metadata chain...
let rawValue;
if (propertyName in this._instance) {
rawValue = this._instance[propertyName];
} else if (propertyName in this._series) {
rawValue = this._series[propertyName];
} else if (propertyName in this._study) {
rawValue = this._study[propertyName];
if (tagOrProperty in instanceData) {
rawValue = instanceData[tagOrProperty];
} else if (tagOrProperty in this._series) {
rawValue = this._series[tagOrProperty];
} else if (tagOrProperty in this._study) {
rawValue = this._study[tagOrProperty];
}
if (rawValue !== void 0) {
@@ -79,12 +78,10 @@ export class OHIFInstanceMetadata extends InstanceMetadata {
// Override
tagExists(tagOrProperty) {
const propertyName = OHIFInstanceMetadata.getPropertyName(tagOrProperty);
return (
propertyName in this._instance ||
propertyName in this._series ||
propertyName in this._study
tagOrProperty in this._instance.metadata ||
tagOrProperty in this._series ||
tagOrProperty in this._study
);
}
@@ -97,28 +94,4 @@ export class OHIFInstanceMetadata extends InstanceMetadata {
return this._imageId;
}
/**
* Static methods
*/
// @TODO: The current mapping of standard DICOM property names to local property names is not optimal.
// The inconsistency in property naming makes this function increasingly complex.
// A possible solution to improve this would be adapt retriveMetadata names to use DICOM standard names as in dicomTagDescriptions.js
static getPropertyName(tagOrProperty) {
let propertyName;
const tagInfo = DICOMTagDescriptions.find(tagOrProperty);
if (tagInfo !== void 0) {
// This function tries to translate standard DICOM property names into local naming convention.
propertyName = tagInfo.keyword
.replace(/^SOP/, 'sop')
.replace(/UID$/, 'Uid')
.replace(/ID$/, 'Id');
propertyName =
propertyName.charAt(0).toLowerCase() + propertyName.substr(1);
}
return propertyName;
}
}
@@ -18,7 +18,7 @@ export class OHIFSeriesMetadata extends SeriesMetadata {
configurable: false,
enumerable: false,
writable: false,
value: series.seriesInstanceUid,
value: series.SeriesInstanceUID,
});
// populate internal list of instances...
@@ -18,11 +18,11 @@ export class OHIFStudyMetadata extends StudyMetadata {
configurable: false,
enumerable: false,
writable: false,
value: study.studyInstanceUid,
value: study.StudyInstanceUID,
});
// populate internal list of series
study.seriesList.forEach(series => {
study.series.forEach(series => {
this.addSeries(new OHIFSeriesMetadata(series, study));
});
}
+9 -9
View File
@@ -83,10 +83,10 @@ well (again, flexibility and extensibility are design goals). Most implemented
methods rely on the implementation of an unimplemented method. For example, the
method `getStringValue` from `InstanceMetadata` class, which has indeed been
implemented and is meant to retrieve a metadata value as a string, internally
calls the `getRawValue` method which _was NOT implemented_ and is meant to query
calls the `getTagValue` method which _was NOT implemented_ and is meant to query
the internal data structures for the requested metadata value and return it _as
is_. Used in that way, an application would not benefit much from the already
implemented methods. On the other hand, by simply overriding the `getRawValue`
implemented methods. On the other hand, by simply overriding the `getTagValue`
method on a specialized class to deal with the intrinsics of its internal data
structures, this very application would now benefit from all already implemented
methods.
@@ -99,12 +99,12 @@ The following code snippet tries to illustrate the idea:
class InstanceMetadata {
[ ... ]
getRawValue(tagOrProperty, defaultValue) {
getTagValue(tagOrProperty, defaultValue) {
// Please implement this method in a specialized subclass...
}
[ ... ]
getStringValue(tagOrProperty, index, defaultValue) {
let rawValue = this.getRawValue(tagOrProperty, '');
let rawValue = this.getTagValue(tagOrProperty, '');
// parse the returned value into a string...
[ ... ]
return stringValue;
@@ -117,11 +117,11 @@ class InstanceMetadata {
class MyFancyAppInstanceMetadata extends InstanceMetadata {
// Overriding this method will make all methods implemented in the super class
// that rely on it to be immediately available...
getRawValue(tagOrProperty, defaultValue) {
let rawValue;
getTagValue(tagOrProperty, defaultValue) {
let tagValue;
// retrieve raw value from internal data structures...
[ ... ]
return rawValue;
return tagValue;
}
}
@@ -135,9 +135,9 @@ if (sopInstaceMetadata instanceof MyFancyAppInstanceMetadata) { // true
if (sopInstaceMetadata instanceof InstanceMetadata) { // also true
// this code will also be executed...
}
// The following will also work since the internal "getRawValue" call inside
// The following will also work since the internal "getTagValue" call inside
// "getStringValue" method will now be satisfied... (thanks to the override)
let patientName = sopInstaceMetadata.getStringValue('PatientName', '');
let PatientName = sopInstaceMetadata.getStringValue('PatientName', '');
[ ... ]
```
@@ -35,6 +35,12 @@ export class StudyMetadata extends Metadata {
writable: false,
value: [],
},
_derivedDisplaySets: {
configurable: false,
enumerable: false,
writable: false,
value: [],
},
_firstSeries: {
configurable: false,
enumerable: false,
@@ -110,18 +116,22 @@ export class StudyMetadata extends Metadata {
return;
}
const sopClassUids = getSopClassUids(series);
const sopClassUIDs = getSopClassUIDs(series);
if (sopClassHandlerModules && sopClassHandlerModules.length > 0) {
const displaySet = _getDisplaySetFromSopClassModule(
sopClassHandlerModules,
series,
study,
sopClassUids
sopClassUIDs
);
if (displaySet) {
displaySet.sopClassModule = true;
displaySets.push(displaySet);
displaySet.isDerived
? this._addDerivedDisplaySet(displaySet)
: displaySets.push(displaySet);
return displaySets;
}
}
@@ -135,10 +145,10 @@ export class StudyMetadata extends Metadata {
// series into another display set.
const stackableInstances = [];
series.forEachInstance(instance => {
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
// All imaging modalities must have a valid value for SOPClassUID (x00080016) or Rows (x00280010)
if (
!isImage(instance.getRawValue('x00080016')) &&
!instance.getRawValue('x00280010')
!isImage(instance.getTagValue('SOPClassUID')) &&
!instance.getTagValue('Rows')
) {
return;
}
@@ -149,23 +159,23 @@ export class StudyMetadata extends Metadata {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
sopClassUIDs,
isClip: true,
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID for drag/drop purposes
numImageFrames: instance.getTagValue('NumberOfFrames'), // Override the default value of instances.length
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.modality)) {
} else if (isSingleImageModality(instance.Modality)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
seriesInstanceUid: series.getSeriesInstanceUID(),
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
sopClassUIDs,
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID
SeriesInstanceUID: series.getSeriesInstanceUID(),
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else {
@@ -175,9 +185,9 @@ export class StudyMetadata extends Metadata {
if (stackableInstances.length) {
const displaySet = makeDisplaySet(series, stackableInstances);
displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID());
displaySet.setAttribute('StudyInstanceUID', study.getStudyInstanceUID());
displaySet.setAttributes({
sopClassUids,
sopClassUIDs,
});
displaySets.push(displaySet);
}
@@ -186,9 +196,73 @@ export class StudyMetadata extends Metadata {
}
/**
* Creates a set of series to be placed in the Study Metadata
* The series that appear in the Study Metadata must represent
* imaging modalities.
* Adds the displaySets to the studies list of derived displaySets.
* @param {object} displaySet The displaySet to append to the derived displaysets list.
*/
_addDerivedDisplaySet(displaySet) {
this._derivedDisplaySets.push(displaySet);
// --> Perhaps that logic should exist in the extension sop class handler and this be a dumb list.
// TODO -> Get x Modality by referencedSeriesInstanceUid, FoR, etc.
}
/**
* Returns a list of derived datasets in the study, filtered by the given filter.
* @param {object} filter An object containing search filters
* @param {object} filter.modality
* @param {object} filter.referencedSeriesInstanceUID
* @param {object} filter.referencedFrameOfReferenceUID
* @return {Array} filtered derived display sets
*/
getDerivedDatasets(filter) {
const {
modality,
referencedSeriesInstanceUID,
referencedFrameOfReferenceUID,
} = filter;
let filteredDerivedDisplaySets = this._derivedDisplaySets;
if (modality) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet => displaySet.Modality === Modality
);
}
if (referencedSeriesInstanceUID) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet => {
if (!displaySet.metadata.ReferencedSeriesSequence) {
return false;
}
const ReferencedSeriesSequence = Array.isArray(
displaySet.metadata.ReferencedSeriesSequence
)
? displaySet.metadata.ReferencedSeriesSequence
: [displaySet.metadata.ReferencedSeriesSequence];
return ReferencedSeriesSequence.some(
ReferencedSeries =>
ReferencedSeries.SeriesInstanceUID === referencedSeriesInstanceUID
);
}
);
}
if (referencedFrameOfReferenceUID) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet =>
displaySet.ReferencedFrameOfReferenceUID ===
ReferencedFrameOfReferenceUID
);
}
return filteredDerivedDisplaySets;
}
/**
* Creates a set of displaySets to be placed in the Study Metadata
* The displaySets that appear in the Study Metadata must represent
* imaging modalities. A series may be split into one or more displaySets.
*
* Furthermore, for drag/drop functionality,
* it is easiest if the stack objects also contain information about
@@ -416,7 +490,7 @@ export class StudyMetadata extends Metadata {
* needs series sorted by the same criteria used for sorting display sets.
*/
sortSeriesByDisplaySets() {
// Object for mapping display sets' index by seriesInstanceUid
// Object for mapping display sets' index by SeriesInstanceUID
const displaySetsMapping = {};
// Loop through each display set to create the mapping
@@ -428,8 +502,8 @@ export class StudyMetadata extends Metadata {
}
// In case of multiframe studies, just get the first index occurence
if (displaySetsMapping[displaySet.seriesInstanceUid] === void 0) {
displaySetsMapping[displaySet.seriesInstanceUid] = index;
if (displaySetsMapping[displaySet.SeriesInstanceUID] === void 0) {
displaySetsMapping[displaySet.SeriesInstanceUID] = index;
}
});
@@ -482,6 +556,22 @@ export class StudyMetadata extends Metadata {
return series;
}
/**
* Get the first image id given display instance uid.
* @return {string} The image id.
*/
getFirstImageId(displaySetInstanceUID) {
try {
const displaySet = this.findDisplaySet(
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID
);
return displaySet.images[0].getImageId();
} catch (error) {
console.error('Failed to retrieve image metadata');
return null;
}
}
/**
* Get the first instance of the current study retaining a consistent result across multiple calls.
* @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
@@ -581,8 +671,7 @@ export class StudyMetadata extends Metadata {
const dwc = api.DICOMwebClient;
const isMultiFrame = instance => {
// NumberOfFrames (0028,0008)
return instance.getRawValue('x00280008') > 1;
return instance.getTagValue('NumberOfFrames') > 1;
};
const makeDisplaySet = (series, instances) => {
@@ -592,15 +681,15 @@ const makeDisplaySet = (series, instances) => {
// set appropriate attributes to image set...
imageSet.setAttributes({
displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID
seriesDate: seriesData.seriesDate,
seriesTime: seriesData.seriesTime,
seriesInstanceUid: series.getSeriesInstanceUID(),
seriesNumber: instance.getRawValue('x00200011'),
seriesDescription: instance.getRawValue('x0008103e'),
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
SeriesDate: seriesData.SeriesDate,
SeriesTime: seriesData.SeriesTime,
SeriesInstanceUID: series.getSeriesInstanceUID(),
SeriesNumber: instance.getTagValue('SeriesNumber'),
SeriesDescription: instance.getTagValue('SeriesDescription'),
numImageFrames: instances.length,
frameRate: instance.getRawValue('x00181063'),
modality: instance.getRawValue('x00080060'),
frameRate: instance.getTagValue('FrameTime'),
Modality: instance.getTagValue('Modality'),
isMultiFrame: isMultiFrame(instance),
});
@@ -610,22 +699,26 @@ const makeDisplaySet = (series, instances) => {
imageSet.sortBy((a, b) => {
// Sort by InstanceNumber (0020,0013)
return (
(parseInt(a.getRawValue('x00200013', 0)) || 0) -
(parseInt(b.getRawValue('x00200013', 0)) || 0)
(parseInt(a.getTagValue('InstanceNumber', 0)) || 0) -
(parseInt(b.getTagValue('InstanceNumber', 0)) || 0)
);
});
}
// Include the first image instance number (after sorted)
imageSet.setAttribute(
'instanceNumber',
imageSet.getImage(0).getRawValue('x00200013')
'InstanceNumber',
imageSet.getImage(0).getTagValue('InstanceNumber')
);
const isReconstructable = isDisplaySetReconstructable(series, instances);
const isReconstructable = isDisplaySetReconstructable(instances);
imageSet.isReconstructable = isReconstructable.value;
if (shallSort && imageSet.isReconstructable) {
imageSet.sortByImagePositionPatient();
}
if (isReconstructable.missingFrames) {
// TODO -> This is currently unused, but may be used for reconstructing
// Volumes with gaps later on.
@@ -635,57 +728,57 @@ const makeDisplaySet = (series, instances) => {
return imageSet;
};
const isSingleImageModality = modality => {
return modality === 'CR' || modality === 'MG' || modality === 'DX';
const isSingleImageModality = Modality => {
return Modality === 'CR' || Modality === 'MG' || Modality === 'DX';
};
function getSopClassUids(series) {
const uniqueSopClassUidsInSeries = new Set();
function getSopClassUIDs(series) {
const uniqueSopClassUIDsInSeries = new Set();
series.forEachInstance(instance => {
const instanceSopClassUid = instance.getRawValue('x00080016');
const instanceSopClassUID = instance.getTagValue('SOPClassUID');
uniqueSopClassUidsInSeries.add(instanceSopClassUid);
uniqueSopClassUIDsInSeries.add(instanceSopClassUID);
});
const sopClassUids = Array.from(uniqueSopClassUidsInSeries);
const sopClassUIDs = Array.from(uniqueSopClassUIDsInSeries);
return sopClassUids;
return sopClassUIDs;
}
/**
* @private
* @param {SeriesMetadata} series
* @param {StudyMetadata} study
* @param {string[]} sopClassUids
* @param {string[]} sopClassUIDs
*/
function _getDisplaySetFromSopClassModule(
sopClassHandlerExtensions, // TODO: Update Usage
series,
study,
sopClassUids
sopClassUIDs
) {
// TODO: For now only use the plugins if all instances have the same sopClassUid
if (sopClassUids.length !== 1) {
// TODO: For now only use the plugins if all instances have the same SOPClassUID
if (sopClassUIDs.length !== 1) {
console.warn(
'getDisplaySetFromSopClassPlugin: More than one SOPClassUid in the same series is not yet supported.'
'getDisplaySetFromSopClassPlugin: More than one SOPClassUID in the same series is not yet supported.'
);
return;
}
const sopClassUid = sopClassUids[0];
const SOPClassUID = sopClassUIDs[0];
const sopClassHandlerModules = sopClassHandlerExtensions.map(extension => {
return extension.module;
});
const handlersForSopClassUid = sopClassHandlerModules.filter(module => {
return module.sopClassUids.includes(sopClassUid);
const handlersForSopClassUID = sopClassHandlerModules.filter(module => {
return module.sopClassUIDs.includes(SOPClassUID);
});
// TODO: Sort by something, so we can determine which plugin to use
if (!handlersForSopClassUid || !handlersForSopClassUid.length) {
if (!handlersForSopClassUID || !handlersForSopClassUID.length) {
return;
}
const plugin = handlersForSopClassUid[0];
const plugin = handlersForSopClassUID[0];
const headers = DICOMWeb.getAuthorizationHeader();
const dicomWebClient = new dwc({
url: study.getData().wadoRoot,
@@ -698,15 +791,15 @@ function _getDisplaySetFromSopClassModule(
dicomWebClient,
headers
);
if (displaySet && !displaySet.modality) {
if (displaySet && !displaySet.Modality) {
const instance = series.getFirstInstance();
displaySet.modality = instance.getRawValue('x00080060');
displaySet.Modality = instance.getTagValue('Modality');
}
return displaySet;
}
/**
* Sort series primarily by modality (i.e., series with references to other
* Sort series primarily by Modality (i.e., series with references to other
* series like SEG, KO or PR are grouped in the end of the list) and then by
* series number:
*
@@ -724,8 +817,8 @@ function _getDisplaySetFromSopClassModule(
*/
function seriesSortingCriteria(a, b) {
const isLowPriorityA = isLowPriorityModality(a.modality);
const isLowPriorityB = isLowPriorityModality(b.modality);
const isLowPriorityA = isLowPriorityModality(a.Modality);
const isLowPriorityB = isLowPriorityModality(b.Modality);
if (!isLowPriorityA && isLowPriorityB) {
return -1;
}
@@ -742,7 +835,7 @@ function seriesSortingCriteria(a, b) {
*/
function sortBySeriesNumber(a, b) {
const seriesNumberAIsGreaterOrUndefined =
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
a.SeriesNumber > b.SeriesNumber || (!a.SeriesNumber && b.SeriesNumber);
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
}
Loaded 100 of 323 files, more files were not shown because too many files have changed in this diff. Show more