Compare commits

...
Author SHA1 Message Date
ohif-bot 2a7787132a chore(version): Update package versions [skip ci] 2023-09-11 14:40:31 +00:00
ohif-bot 3cc35e7ed5 chore(version): version.json [skip ci] 2023-09-11 14:40:22 +00:00
Joe Boccanfuso de0a742733 chore(prettier): fix end-of-line warnings (#3642) 2023-09-11 10:31:40 -04:00
ohif-bot c158279a53 chore(version): Update package versions [skip ci] 2023-09-11 13:19:30 +00:00
ohif-bot 12acc396d6 chore(version): version.json [skip ci] 2023-09-11 13:19:23 +00:00
Joe Boccanfuso a6e668f8d4 docs(google cloud healthcare): Added recipe docs for setting up Google Cloud Healthcare API (#3641) 2023-09-11 09:11:25 -04:00
ohif-bot 6dd74b6371 chore(version): Update package versions [skip ci] 2023-09-06 18:17:59 +00:00
ohif-bot 908fb8e14f chore(version): version.json [skip ci] 2023-09-06 18:17:50 +00:00
Alireza 94f7cfb08e fix(hotkeys): preserve hotkeys if changed, and reduce re-rendering (#3635) 2023-09-06 14:09:26 -04:00
ohif-bot 6c8364835b chore(version): Update package versions [skip ci] 2023-09-06 13:53:48 +00:00
ohif-bot a39929d4d5 chore(version): version.json [skip ci] 2023-09-06 13:53:40 +00:00
Salim Kanoun dd6a8812d1 export(checkbox): add missing checkbox export (#3629) 2023-09-06 09:46:01 -04:00
ohif-bot ce72929367 chore(version): Update package versions [skip ci] 2023-09-06 13:34:21 +00:00
ohif-bot 38ab1ae5a6 chore(version): version.json [skip ci] 2023-09-06 13:34:13 +00:00
M.DandJoe Boccanfuso 69115da06d feat(ImageOverlayViewerTool): add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images (#3163)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2023-09-06 09:24:17 -04:00
ohif-bot ef58893c27 chore(version): Update package versions [skip ci] 2023-09-05 17:13:27 +00:00
ohif-bot 7987f0effa chore(version): version.json [skip ci] 2023-09-05 17:13:19 +00:00
Joe BoccanfusoandAlireza 3ce72254b3 fix(nginx archive recipe): Fixes to various configuration files. (#3624)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2023-09-05 13:05:36 -04:00
ohif-bot 6d47dce4dd chore(version): Update package versions [skip ci] 2023-09-01 20:26:45 +00:00
ohif-bot f7cddd0842 chore(version): version.json [skip ci] 2023-09-01 20:26:38 +00:00
Alireza 9045ddaedc chore(prettier): full repo linting and code reformatting once and for all (#3627) 2023-09-01 16:19:39 -04:00
ohif-bot 01500a2d4c chore(version): version.json [skip ci] 2023-08-30 22:46:26 +00:00
Alireza b170e7f5ae chore(prettier): update prettier to fix publish (#3623) 2023-08-30 18:38:57 -04:00
ohif-bot 2d7da9c0bb chore(version): version.json [skip ci] 2023-08-30 20:54:03 +00:00
AlirezaandBill Wallace 4c6ff873e8 feat(grid): remove viewportIndex and only rely on viewportId (#3591)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2023-08-30 16:46:55 -04:00
ohif-bot 5dafac7c92 chore(version): Update package versions [skip ci] 2023-08-30 13:38:56 +00:00
ohif-bot 7ecaf0da5d chore(version): version.json [skip ci] 2023-08-30 13:38:49 +00:00
Joe Boccanfuso adedc8c382 feat(data source UI config): Popup the configuration dialogue whenever a data source is not fully configured (#3620) 2023-08-30 09:27:48 -04:00
ohif-bot 5856ae2464 chore(version): Update package versions [skip ci] 2023-08-29 16:44:37 +00:00
ohif-bot 84b7de5d96 chore(version): version.json [skip ci] 2023-08-29 16:44:29 +00:00
mccle 35fc30c535 fix(OpenIdConnectRoutes): fix handleUnauthenticated (#3617) 2023-08-29 12:36:41 -04:00
ohif-bot eafc716d00 chore(version): Update package versions [skip ci] 2023-08-29 16:33:17 +00:00
ohif-bot 88d695cf4f chore(version): version.json [skip ci] 2023-08-29 16:33:10 +00:00
Joe Boccanfuso 44f101d3f2 fix(PT Metadata): Allow for PatientWeight to be missing from the metadata (#3621) 2023-08-29 12:21:21 -04:00
ohif-bot a9d6869198 chore(version): Update package versions [skip ci] 2023-08-29 15:27:52 +00:00
ohif-bot 77c102f993 chore(version): version.json [skip ci] 2023-08-29 15:27:46 +00:00
dxlin 75f61f85fe fi(buffer): buffer undefined (#3590) 2023-08-29 11:20:01 -04:00
ohif-bot 9fbdf37f44 chore(version): Update package versions [skip ci] 2023-08-25 18:27:00 +00:00
ohif-bot 7ecd2f1628 chore(version): version.json [skip ci] 2023-08-25 18:26:53 +00:00
Joe Boccanfuso a336992971 feat(cloud data source config): GUI and API for configuring a cloud data source with Google cloud healthcare implementation (#3589) 2023-08-25 14:19:17 -04:00
ohif-bot a2ef2b0fcb chore(version): Update package versions [skip ci] 2023-08-23 13:43:54 +00:00
ohif-bot eb57c2f8bd chore(version): version.json [skip ci] 2023-08-23 13:43:46 +00:00
Alireza 65b49aeb1b fix(memory leak): array buffer was sticking around in volume viewports (#3611) 2023-08-23 09:35:44 -04:00
ohif-bot fe0ee050ed chore(version): Update package versions [skip ci] 2023-08-16 20:56:02 +00:00
ohif-bot ee6f399787 chore(version): version.json [skip ci] 2023-08-16 20:55:59 +00:00
Igor Octaviano b117a54aa1 fix(rgb): Fix rgb inconsistencies by updating cs3d image loader (#3604) 2023-08-16 16:48:13 -04:00
ohif-bot ede6300099 chore(version): Update package versions [skip ci] 2023-08-16 16:59:14 +00:00
ohif-bot a8717c8625 chore(version): version.json [skip ci] 2023-08-16 16:59:10 +00:00
Joe Boccanfuso 0f9b36906f feat(thumbnail highlight): Thumbnails of hydrated series are now highlighted (#3594) 2023-08-16 12:51:26 -04:00
ohif-bot 3fe676638b chore(version): Update package versions [skip ci] 2023-08-16 15:44:41 +00:00
ohif-bot d69dab70e1 chore(version): version.json [skip ci] 2023-08-16 15:44:38 +00:00
rodrigobasilio2022 f845f87716 feat(handler): Add handler for unsupported sopclassUIDs (#3601) 2023-08-16 11:37:28 -04:00
ohif-bot de6976df3b chore(version): Update package versions [skip ci] 2023-08-16 14:47:46 +00:00
ohif-bot ea9903e92b chore(version): version.json [skip ci] 2023-08-16 14:47:42 +00:00
Igor Octaviano ed0b01cd39 doc(README.md): Acknowledge IDC (README.md) #1136 (#3576) 2023-08-16 10:40:19 -04:00
ohif-bot 2c891dae60 chore(version): Update package versions [skip ci] 2023-08-16 14:33:56 +00:00
ohif-bot bb4297af35 chore(version): version.json [skip ci] 2023-08-16 14:33:52 +00:00
rodrigobasilio2022 3c4795d8cb feat(read-only config): read only ui for SEG/RT/SR (#3586) 2023-08-16 10:26:28 -04:00
ohif-bot c6d5f719e7 chore(version): Update package versions [skip ci] 2023-08-16 14:22:41 +00:00
ohif-bot a5bfae6e27 chore(version): version.json [skip ci] 2023-08-16 14:22:37 +00:00
Bill Wallace 53c16b928f feat(hp priors): Study comparison hanging protocol (#3579) 2023-08-16 10:15:12 -04:00
ohif-bot ecbe7c56e0 chore(version): Update package versions [skip ci] 2023-08-16 03:06:31 +00:00
ohif-bot c4007d4d0d chore(version): version.json [skip ci] 2023-08-16 03:06:27 +00:00
rodrigobasilio2022 19408c987f feat(errorHandling): Create fallback page for Server, Study and Series (#3592) 2023-08-15 22:59:29 -04:00
ohif-bot 9c51f17c60 chore(version): Update package versions [skip ci] 2023-08-15 11:29:22 +00:00
ohif-bot e664fffbc0 chore(version): version.json [skip ci] 2023-08-15 11:29:18 +00:00
Bill Wallace 884577a23d fix: Integration test instability (#3597)
There are a couple of changes here which should improve stability of the tests.  Hopefully that will be sufficient, but it is difficult to tell for sure if it is complete.
2023-08-15 07:20:45 -04:00
ohif-bot 799d0793fd chore(version): Update package versions [skip ci] 2023-08-14 15:07:20 +00:00
ohif-bot 7074c6d806 chore(version): version.json [skip ci] 2023-08-14 15:07:16 +00:00
Patrick D. Lloyd 41dc424d4a fix(Docker): Fix ARM64 Docker builds by adding node-gyp dependencies (#3595) (#3596) 2023-08-14 10:46:41 -04:00
ohif-bot 88ef7b733f chore(version): Update package versions [skip ci] 2023-08-11 22:23:34 +00:00
ohif-bot 2c38f6ba8c chore(version): version.json [skip ci] 2023-08-11 22:23:31 +00:00
rodrigobasilio2022andIgor Octaviano 5302e5b62b feat(Thumbnail): Display set messages support & displaying of series inconsistencies in the thumbnail (#3499)
Co-authored-by: Igor Octaviano <igoroctaviano@gmail.com>
2023-08-11 19:16:25 -03:00
ohif-bot aedb9d1576 chore(version): Update package versions [skip ci] 2023-08-09 15:47:02 +00:00
ohif-bot daf734e09e chore(version): version.json [skip ci] 2023-08-09 15:46:59 +00:00
rodrigobasilio2022 b55518e08e chore(exports): Exposes hydration functions from seg and rt extensions (#3585) 2023-08-09 11:39:17 -04:00
ohif-bot ca1287d99e chore(version): Update package versions [skip ci] 2023-08-09 14:15:07 +00:00
ohif-bot 86e6d28ade chore(version): version.json [skip ci] 2023-08-09 14:15:03 +00:00
Alireza 5d435424e2 lint(curly): consistent use of curly brackets in codebase (#3584) 2023-08-09 10:07:33 -04:00
ohif-bot 9ae3d9f769 chore(version): Update package versions [skip ci] 2023-08-09 12:16:53 +00:00
ohif-bot 348acb9091 chore(version): version.json [skip ci] 2023-08-09 12:16:50 +00:00
Alireza 8adf663248 fix(state synchronization): SEG and RT viewports should keep their before hydration voi (#3560) 2023-08-09 08:08:23 -04:00
ohif-bot 4f037ae55e chore(version): Update package versions [skip ci] 2023-08-08 18:48:02 +00:00
ohif-bot 55a50c9d38 chore(version): version.json [skip ci] 2023-08-08 18:47:58 +00:00
Alireza 156c1ba60b perf(segmentation): make segmentation read 3x faster (#3577) 2023-08-08 14:39:31 -04:00
ohif-bot ec76ea24dd chore(version): Update package versions [skip ci] 2023-08-08 14:13:33 +00:00
ohif-bot 431b307fab chore(version): version.json [skip ci] 2023-08-08 14:13:30 +00:00
Igor Octaviano 0493a0d587 feat(Mode): Ability to overwrite mode configuration and small improvements (#3580) 2023-08-08 10:05:20 -04:00
ohif-bot 58d38495f0 chore(version): Update package versions [skip ci] 2023-07-27 16:47:28 +00:00
ohif-bot 862f255be2 chore(version): version.json [skip ci] 2023-07-27 16:47:24 +00:00
Alireza 72207e6c78 dev(yarn link): make it possible to yarn link easily with cornerstone3D and update docs (#3563) 2023-07-27 12:39:12 -04:00
ohif-bot 6d3b3d191f chore(version): Update package versions [skip ci] 2023-07-27 13:59:21 +00:00
ohif-bot d8867d3804 chore(version): version.json [skip ci] 2023-07-27 13:59:17 +00:00
Andres Diaz-Pinto 019dedce34 docs(readme): fix broken DICOMWeb link in main Readme (#3561)
Signed-off-by: Andres <diazandr3s@gmail.com>
2023-07-27 09:51:29 -04:00
ohif-bot 4d104add05 chore(version): Update package versions [skip ci] 2023-07-26 15:42:03 +00:00
ohif-bot 924a5478f3 chore(version): version.json [skip ci] 2023-07-26 15:41:59 +00:00
Alireza d8f628b125 fix(rerendering): reduce the re-rendering of the viewport grid (#3558) 2023-07-26 11:34:50 -04:00
ohif-bot cab8d322c2 chore(version): Update package versions [skip ci] 2023-07-21 12:34:48 +00:00
ohif-bot 3ef26cc670 chore(version): version.json [skip ci] 2023-07-21 12:34:45 +00:00
Wenqi Li 1efe2db662 docs: fixes a README.md typo (#3553) 2023-07-21 08:27:51 -04:00
ohif-bot 72933af984 chore(version): Update package versions [skip ci] 2023-07-20 16:17:02 +00:00
ohif-bot 2645e3c7aa chore(version): version.json [skip ci] 2023-07-20 16:16:58 +00:00
Alireza 81ab52784a fix(oauth): handle no user scenario (#3556) 2023-07-20 12:08:54 -04:00
ohif-bot 09a302250c chore(version): Update package versions [skip ci] 2023-07-20 15:50:41 +00:00
ohif-bot 783bab7c9d chore(version): version.json [skip ci] 2023-07-20 15:50:38 +00:00
Joe Boccanfuso 7bbc213f58 feat(DataSource): Allow for dynamic data source configuration and creation (#3543) 2023-07-20 11:43:05 -04:00
ohif-bot 6d88978710 chore(version): Update package versions [skip ci] 2023-07-19 19:28:14 +00:00
ohif-bot 32bb553b05 chore(version): version.json [skip ci] 2023-07-19 19:28:11 +00:00
Bill Wallace d5cbbb037d Fix the wrong aspect ratio load (#3534) 2023-07-19 15:21:19 -04:00
ohif-bot b904ea0f23 chore(version): Update package versions [skip ci] 2023-07-19 13:49:16 +00:00
ohif-bot 839923e578 chore(version): version.json [skip ci] 2023-07-19 13:49:13 +00:00
Bill Wallace 030e1e4104 chore(test): Integration test wait until rendered/queried instead of timed (#3540) 2023-07-19 09:41:47 -04:00
ohif-bot 265c1fbb10 chore(version): Update package versions [skip ci] 2023-07-13 13:13:19 +00:00
ohif-bot f3aa8a8b61 chore(version): version.json [skip ci] 2023-07-13 13:13:14 +00:00
Joe Boccanfuso 18156bb7b1 fix(PT measurement units): Use the modality unit that arrives from cornerstone3d instead of calculating the modality unit (again) here. (#3533) 2023-07-13 09:03:17 -04:00
ohif-bot 93a009f1a6 chore(version): Update package versions [skip ci] 2023-07-12 14:26:38 +00:00
ohif-bot 11471e6fc1 chore(version): version.json [skip ci] 2023-07-12 14:26:35 +00:00
Joe Boccanfuso 7d286a07f2 fix(Worklist): use the flexbox gap style property for spacing between mode buttons (#3532) 2023-07-12 10:19:03 -04:00
ohif-bot 19418df8b5 chore(version): Update package versions [skip ci] 2023-07-12 12:35:35 +00:00
ohif-bot c9eb0250dd chore(version): version.json [skip ci] 2023-07-12 12:35:32 +00:00
Joe Boccanfuso c3cd265430 fix(Measurement tracking): Side panel study browser can now be closed and measurements are still tracked (#3530) 2023-07-12 08:28:48 -04:00
ohif-bot 913264d289 chore(version): Update package versions [skip ci] 2023-07-07 16:44:37 +00:00
ohif-bot 74acc02c37 chore(version): version.json [skip ci] 2023-07-07 16:44:34 +00:00
m00n620 453ff0be4f fix(cli): extension template (#3512) 2023-07-07 12:37:36 -04:00
ohif-bot 2e040d550d chore(version): Update package versions [skip ci] 2023-07-07 16:01:21 +00:00
ohif-bot bfa1b1c7c0 chore(version): version.json [skip ci] 2023-07-07 16:01:17 +00:00
Sofien-Sellami a3e7ff479a fix(tmtv): jumpToClick Tool for TMTV mode (#3525) 2023-07-07 11:53:54 -04:00
ohif-bot 87f361516c chore(version): Update package versions [skip ci] 2023-07-07 15:37:42 +00:00
ohif-bot 5eaf6168f6 chore(version): version.json [skip ci] 2023-07-07 15:37:38 +00:00
Sofien-Sellami 5ab74f5020 ui(InputNumber): adding label element in InputNumber Component and … (#3501) 2023-07-07 11:30:52 -04:00
ohif-bot 92273b25b3 chore(version): Update package versions [skip ci] 2023-07-06 16:23:25 +00:00
ohif-bot 3f296bdeb5 chore(version): version.json [skip ci] 2023-07-06 16:23:22 +00:00
Attila Hornok 0600858055 docs(ohif): broken link in the docs (#3509) 2023-07-06 12:16:09 -04:00
Alireza 4a184d0be7 fix(invert): allows volume viewport to invert and sync their invert status (#3514) 2023-07-06 12:09:34 -04:00
ohif-bot f3a4f861b4 chore(version): Update package versions [skip ci] 2023-07-05 13:11:37 +00:00
ohif-bot fc19912162 chore(version): version.json [skip ci] 2023-07-05 13:11:33 +00:00
Joe Boccanfuso 61595ce7d6 feat(Docker): Facilitate the ability to run the OHIF docker image over SSL (#3511) 2023-07-05 09:03:43 -04:00
ohif-bot ffd53eff85 chore(version): Update package versions [skip ci] 2023-07-05 12:50:14 +00:00
ohif-bot 52a43c771a chore(version): version.json [skip ci] 2023-07-05 12:50:10 +00:00
Joe Boccanfuso c3c132d268 fix(tracked-measurement): SR viewports disable jump to measurement (#3518) 2023-07-05 08:43:01 -04:00
ohif-bot e742328ac1 chore(version): Update package versions [skip ci] 2023-07-04 18:46:19 +00:00
ohif-bot 90647b63a0 chore(version): version.json [skip ci] 2023-07-04 18:46:16 +00:00
Joe Boccanfuso ed44ad7f4b fix(segmentation): Use the series description for the segmentation label (#3517) 2023-07-04 14:39:39 -04:00
ohif-bot b0a456a46c chore(version): Update package versions [skip ci] 2023-07-04 16:41:18 +00:00
ohif-bot 8b7db45641 chore(version): version.json [skip ci] 2023-07-04 16:41:14 +00:00
Joe Boccanfuso b5599dde4e fix(utils): remove undefined/null values from the PerFrameFunctionalGroupsSequence in combineFrameInstance. (#3515) 2023-07-04 12:33:51 -04:00
ohif-bot f7b7131d70 chore(version): Update package versions [skip ci] 2023-07-04 16:05:36 +00:00
ohif-bot d646c15c27 chore(version): version.json [skip ci] 2023-07-04 16:05:32 +00:00
Bill WallaceandAlireza 64f8bb580a fix(colormap): update opacityMapping to opacity and use simple list renderer(#3464)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2023-07-04 11:57:59 -04:00
ohif-bot ce29bbe795 chore(version): Update package versions [skip ci] 2023-07-04 13:04:17 +00:00
ohif-bot 0bbcb4269d chore(version): version.json [skip ci] 2023-07-04 13:04:14 +00:00
Alireza 3efa793e60 fix(utils): remove undefined values in combineFrameInstance (#3513) 2023-07-04 08:57:14 -04:00
ohif-bot 3b50bd8cc4 chore(version): Update package versions [skip ci] 2023-07-03 14:41:24 +00:00
ohif-bot cd553e29b2 chore(version): version.json [skip ci] 2023-07-03 14:41:21 +00:00
mccle 12dd5a2619 fix(typeError): Address SliceThickness.toFixed TypeError (#3510) 2023-07-03 10:31:46 -04:00
ohif-bot f7a435c2f3 chore(version): Update package versions [skip ci] 2023-06-29 19:56:51 +00:00
ohif-bot 19f1da702f chore(version): version.json [skip ci] 2023-06-29 19:56:47 +00:00
Sofien-Sellami 14974b6973 fix(tmtv): Fusion_Viewport to allow displaySets to be changed (#3502)
Allows drag and drop of new sets of viewports to change them one at a time.
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2023-06-29 15:48:52 -04:00
ohif-bot b4dbe5656b chore(version): Update package versions [skip ci] 2023-06-29 15:24:35 +00:00
ohif-bot c6f7349fd7 chore(version): version.json [skip ci] 2023-06-29 15:24:32 +00:00
Sofien-Sellami abe939bd20 fix(hp): Remove filtered displaySets from parameter from study protocolEngine.findMatch() (#3343)
The changes look good and are running now successfully.
2023-06-29 11:17:08 -04:00
ohif-bot 4d4dd45c23 chore(version): Update package versions [skip ci] 2023-06-29 15:04:16 +00:00
ohif-bot 7671b9faf4 chore(version): version.json [skip ci] 2023-06-29 15:04:12 +00:00
Sofien-SellamiandBill Wallace e653961858 Add support for array input for all constraints in HangingProtocolService validator (#3322)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2023-06-29 10:56:40 -04:00
ohif-bot aeb61f40da chore(version): Update package versions [skip ci] 2023-06-29 14:51:44 +00:00
ohif-bot b5fb2461dd chore(version): version.json [skip ci] 2023-06-29 14:51:41 +00:00
Joe Boccanfuso 15ad8f29cd fix(date formatting): Show study date as a fallback for an empty series date and show 'No Study Date' as a back up for both (#3483) 2023-06-29 10:43:56 -04:00
ohif-bot fa37593696 chore(version): Update package versions [skip ci] 2023-06-29 14:38:20 +00:00
ohif-bot 08eed4cdf9 chore(version): version.json [skip ci] 2023-06-29 14:38:15 +00:00
Sofien-Sellami b5b7d323c8 fix: allow volume3d viewport for SUV rendering (#3494)
Add checks to include volume3d viewports.
2023-06-29 10:30:41 -04:00
ohif-bot 8a8ae237d2 chore(version): Update package versions [skip ci] 2023-06-27 15:52:28 +00:00
ohif-bot cf1e00fee0 chore(version): version.json [skip ci] 2023-06-27 15:52:24 +00:00
Joe Boccanfuso ef4f28690a fix(DICOM Upload): use correct Accept header for upload (#3498) 2023-06-27 11:44:46 -04:00
ohif-bot c98211ccb5 chore(version): Update package versions [skip ci] 2023-06-23 00:55:55 +00:00
ohif-bot 71342fac8f chore(version): version.json [skip ci] 2023-06-23 00:55:52 +00:00
Alireza 52f419d188 feat(transferSyntax): Add customizable Transfer syntax through datasource config (#3406) 2023-06-22 20:47:50 -04:00
Salim Kanoun ca3a0b56f7 update old configs 2023-06-22 21:26:26 +02:00
Salim Kanoun 1a91f210ab rename 2023-06-20 22:18:14 +02:00
ohif-bot 3503e6e9f4 chore(version): Update package versions [skip ci] 2023-06-20 14:33:52 +00:00
ohif-bot 0c51444066 chore(version): version.json [skip ci] 2023-06-20 14:33:48 +00:00
Alireza 83c453d1f3 chore(version): update cs3d version and add iframe documentation (#3474) 2023-06-20 10:26:05 -04:00
Salim Kanoun fa73a458bd WIP (not tested yet) 2023-06-20 09:34:20 +00:00
ohif-bot 5670a1699d chore(version): Update package versions [skip ci] 2023-06-19 14:44:15 +00:00
ohif-bot c529c0da9c chore(version): version.json [skip ci] 2023-06-19 14:44:10 +00:00
dxlin d51211f6b9 fix(SR export): prevent empty sr description when export (#3173) 2023-06-19 10:35:53 -04:00
ohif-bot 2d8400f9d8 chore(version): Update package versions [skip ci] 2023-06-19 14:10:15 +00:00
ohif-bot e12242a15c chore(version): version.json [skip ci] 2023-06-19 14:10:11 +00:00
Davy 8baf9deaf0 fix(i18n): add more fields i18n (#3262) 2023-06-19 10:02:48 -04:00
ohif-bot a34a0b1a3a chore(version): Update package versions [skip ci] 2023-06-19 13:47:55 +00:00
ohif-bot e1545b67da chore(version): version.json [skip ci] 2023-06-19 13:47:52 +00:00
wangxuan d75305e70d fi(proptype): Invalid prop WhiteLabeling of type object supplied... (#3152) 2023-06-19 09:40:31 -04:00
ohif-bot 242beb7b46 chore(version): Update package versions [skip ci] 2023-06-19 13:39:11 +00:00
ohif-bot c5d4e97115 chore(version): version.json [skip ci] 2023-06-19 13:39:07 +00:00
Igor OctavianoandIgor Octaviano 59cdc4ba9a fix(SEG): Missing SpacingBetweenSlices prevents SEG rendering (#3344)
Co-authored-by: Igor Octaviano <igor.octaviano@radicalimaging.com>
2023-06-19 09:31:04 -04:00
ohif-bot 971c594791 chore(version): Update package versions [skip ci] 2023-06-19 13:22:44 +00:00
ohif-bot c55136f55e chore(version): version.json [skip ci] 2023-06-19 13:22:41 +00:00
Jemshith T K 63e12dd651 docs(ohif-cli): update ohif-cli.md (#3461) 2023-06-19 09:15:30 -04:00
ohif-bot c75c6af22f chore(version): Update package versions [skip ci] 2023-06-19 13:09:00 +00:00
ohif-bot 3c777ba58b chore(version): version.json [skip ci] 2023-06-19 13:08:56 +00:00
dxlin e6f73a2025 fix(RT) : Make ContourImageSequence check under ROIContourSequence->ContourImageSequence optional (#3476) 2023-06-19 09:01:23 -04:00
ohif-bot 652e61a417 chore(version): Update package versions [skip ci] 2023-06-15 17:08:06 +00:00
ohif-bot 48bcdd6b50 chore(version): version.json [skip ci] 2023-06-15 17:08:01 +00:00
Joe Boccanfuso cc73f15c8c fix(Button): redesign of existing Button component for consistency (#3473) 2023-06-15 12:58:21 -04:00
ohif-bot 3dce70e624 chore(version): Update package versions [skip ci] 2023-06-13 12:59:35 +00:00
ohif-bot 1d00893c49 chore(version): version.json [skip ci] 2023-06-13 12:59:31 +00:00
Sofien-Sellami 2890635675 fix(overlay): Fix undefined volumes in customizable viewport overlay (#3435) 2023-06-13 08:51:01 -04:00
ohif-bot a6d967a37e chore(version): Update package versions [skip ci] 2023-06-13 02:41:20 +00:00
ohif-bot f23a4500f5 chore(version): version.json [skip ci] 2023-06-13 02:41:17 +00:00
Alireza cadb559619 docs(readme): correct the docs images (#3470) 2023-06-12 22:33:54 -04:00
Salim Kanoun ad3ef3e8ef Merge branch 'master' into customizable-TS 2023-06-09 21:45:03 +02:00
Salim Kanoun 18c6a47dca Merge branch 'master' into customizable-TS 2023-06-07 13:08:49 +02:00
Salim Kanoun 93f30f06c2 Fix tests 2023-06-06 16:57:19 +02:00
Salim Kanoun 50d2640876 accept both accept header and transfer syntax uid 2023-06-05 22:32:29 +02:00
Salim Kanoun 10e399d7b8 Merge branch 'master' into customizable-TS 2023-06-04 12:43:16 +02:00
Salim Kanoun e4937e7473 update comments 2023-06-01 23:35:13 +02:00
Salim Kanoun 62dd2e78bd null safe 2023-06-01 23:32:16 +02:00
Salim Kanoun c3654b7ddf rework TS 2023-06-01 23:18:54 +02:00
Salim Kanoun de60586b71 fixes 2023-05-29 23:39:06 +02:00
Salim Kanoun f670678cf0 WIP accept header 2023-05-29 23:28:17 +02:00
Salim Kanoun 56633a67d9 Merge branch 'v3-stable' into customizable-TS 2023-05-29 23:26:36 +02:00
Salim Kanoun 8e2535a308 temp disabled of requested ts 2023-05-29 10:38:27 +02:00
Salim Kanoun 6da1734b6e Moving requested ts in createDicomWebApi (WIP) 2023-05-29 10:38:04 +02:00
Salim Kanoun 5ae6e9caf4 restaure config 2023-05-18 20:05:28 +02:00
Salim Kanoun 5e4043d99c add source of type correspondancies 2023-05-18 19:21:46 +02:00
Salim Kanoun 0e991d6bde config documentation 2023-05-18 19:13:36 +02:00
Salim Kanoun 2f63a9db80 init commit making TS customizable in OHIF settings 2023-05-18 19:06:26 +02:00
Salim Kanoun bedc379e97 Merge pull request #4 from OHIF/v3-stable
V3 stable
2023-05-18 17:35:16 +02:00
Salim Kanoun 5b7dc9e9c3 Merge pull request #3 from OHIF/v3-stable
V3 stable
2023-04-05 14:02:23 +02:00
Salim Kanoun 00099c0136 Merge remote-tracking branch 'upstream/v3-stable' into v3-stable 2023-03-12 08:01:06 +01:00
Salim Kanoun 799d648a90 Merge pull request #2 from OHIF/v3-stable
V3 stable
2022-12-28 21:25:05 +01:00
Salim Kanoun d5c347cecd Merge pull request #1 from OHIF/v3-stable
V3 stable
2022-11-24 17:01:48 +01:00
1274 changed files with 35837 additions and 35242 deletions

No files matched your search

+5 -8
View File
@@ -269,8 +269,7 @@ jobs:
git config --global user.name "ohif-bot"
- run:
name: Authenticate with NPM registry
command:
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
@@ -289,8 +288,7 @@ jobs:
node ./publish-version.mjs
- run:
name: Again set the NPM registry (was deleted in the version script)
command:
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: publish package dist
command: |
@@ -337,8 +335,7 @@ jobs:
- setup_remote_docker:
docker_layer_caching: false
- run:
name:
Build and push Docker image from the master branch (beta releases)
name: Build and push Docker image from the master branch (beta releases)
command: |
echo $(ls -l)
@@ -417,8 +414,8 @@ workflows:
store_artifacts: false
working_directory: platform/app
build:
yarn test:data && npx cross-env QUICK_BUILD=true
APP_CONFIG=config/dicomweb-server.js yarn run build
yarn test:data && npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
+2 -1
View File
@@ -1,5 +1,6 @@
server {
listen ${PORT};
listen ${PORT} default_server;
listen [::]:${PORT} default_server;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
@@ -0,0 +1,22 @@
server {
listen ${SSL_PORT} ssl http2 default_server;
listen [::]:${SSL_PORT} ssl http2 default_server;
ssl_certificate /etc/ssl/certs/ssl-certificate.crt;
ssl_certificate_key /etc/ssl/private/ssl-private-key.key;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
+11 -1
View File
@@ -1,6 +1,16 @@
#!/bin/sh
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
if [ -n "$SSL_PORT" ]
then
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template > /etc/nginx/conf.d/default.conf
else
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
fi
if [ -n "$APP_CONFIG" ]
then
echo "$APP_CONFIG" > /usr/share/nginx/html/app-config.js
fi
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
+7
View File
@@ -1,6 +1,12 @@
# Reduces size of context and hides
# files from Docker (can't COPY or ADD these)
# Note that typically the Docker context for various OHIF containers is the
# directory of this file (i.e. the root of the source). As such, this is
# the .dockerignore file for ALL Docker containers that are built. For example,
# the Docker containers built from the recipes in ./platform/app/.recipes will
# have this file as their .dockerignore.
# Output
dist/
build/
@@ -28,3 +34,4 @@ dockerfile
.vscode/
coverage/
docs/
testdata/
+5 -6
View File
@@ -1,10 +1,5 @@
{
"plugins": [
"@typescript-eslint",
"import",
"eslint-plugin-tsdoc",
"prettier"
],
"plugins": ["@typescript-eslint", "import", "eslint-plugin-tsdoc", "prettier"],
"extends": [
"react-app",
"eslint:recommended",
@@ -21,6 +16,10 @@
"version": "detect"
}
},
"rules": {
// Enforce consistent brace style for all control statements for readability
"curly": "error"
},
"globals": {
"cy": true,
"before": true,
+5 -7
View File
@@ -39,8 +39,7 @@ body:
attributes:
label: The current behavior
description:
'A clear and concise description of what happens instead of the expected
behavior.'
'A clear and concise description of what happens instead of the expected behavior.'
validations:
required: true
@@ -48,8 +47,7 @@ body:
id: expected_behavior
attributes:
label: The expected behavior
description:
'A clear and concise description of what you expected to happen.'
description: 'A clear and concise description of what you expected to happen.'
validations:
required: true
@@ -81,6 +79,6 @@ body:
- type: markdown
attributes:
value: >
> :warning: Reports we cannot reproduce are at risk of being marked
stale and > closed. The more information you can provide, the more
likely we are to look > into and address your issue.
> :warning: Reports we cannot reproduce are at risk of being marked stale and > closed. The
more information you can provide, the more likely we are to look > into and address your
issue.
+3 -5
View File
@@ -20,17 +20,15 @@ body:
attributes:
label: 'What feature or change would you like to see made?'
description:
'Please include as much detail as possible including possibly mock up
screen shots, workflow or logic flow diagrams etc.'
'Please include as much detail as possible including possibly mock up screen shots, workflow
or logic flow diagrams etc.'
placeholder: '...'
validations:
required: true
- type: textarea
attributes:
label: 'Why should we prioritize this feature?'
description:
'Discuss if and how the requested feature interacts with existing
features.'
description: 'Discuss if and how the requested feature interacts with existing features.'
placeholder: '...'
validations:
required: true
+2 -3
View File
@@ -19,8 +19,7 @@ exemptLabels:
staleLabel: 'Stale :baguette_bread:'
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
This issue has been automatically marked as stale because it has not had recent activity. It will
be closed if no further activity occurs. Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
+1
View File
@@ -1,3 +1,4 @@
[submodule "testdata"]
path = testdata
url = https://github.com/OHIF/viewer-testdata-dicomweb.git
branch = main
+17 -19
View File
@@ -1,23 +1,21 @@
<html>
<head>
<title>OHIF Viewer: Deploy Preview</title>
</head>
<head>
<title>OHIF Viewer: Deploy Preview</title>
</head>
<body>
<h1>Index of Previews</h1>
<ul>
<li>
<a href="/pwa">OHIF Viewer</a>
</li>
<li>
<a href="/docs">Documentation</a>
</li>
<li>
<a href="/ui">UI: Component Library</a>
</li>
</ul>
</body>
<body>
<h1>Index of Previews</h1>
<ul>
<li>
<a href="/pwa">OHIF Viewer</a>
</li>
<li>
<a href="/docs">Documentation</a>
</li>
<li>
<a href="/ui">UI: Component Library</a>
</li>
</ul>
</body>
</html>
+5 -2
View File
@@ -1,9 +1,12 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5",
"printWidth": 80,
"printWidth": 100,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid"
"arrowParens": "avoid",
"singleAttributePerLine": true,
"endOfLine": "auto"
}
+2 -2
View File
@@ -5,11 +5,11 @@ function excludeNodeModulesExcept(modules) {
if (pathSep == '\\')
// must be quoted for use in a regexp:
pathSep = '\\\\';
var moduleRegExps = modules.map(function(modName) {
var moduleRegExps = modules.map(function (modName) {
return new RegExp('node_modules' + pathSep + modName);
});
return function(modulePath) {
return function (modulePath) {
if (/node_modules/.test(modulePath)) {
for (var i = 0; i < moduleRegExps.length; i++)
if (moduleRegExps[i].test(modulePath)) return false;
+1 -3
View File
@@ -1,9 +1,7 @@
const autoprefixer = require('autoprefixer');
const path = require('path');
const tailwindcss = require('tailwindcss');
const tailwindConfigPath = path.resolve(
'../../platform/app/tailwind.config.js'
);
const tailwindConfigPath = path.resolve('../../platform/app/tailwind.config.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const devMode = process.env.NODE_ENV !== 'production';
+5 -1
View File
@@ -33,7 +33,11 @@ function transpileJavaScript(mode) {
rootMode: 'upward',
envName: mode,
cacheCompression: false,
cacheDirectory: true,
// Note: This was causing a lot of issues with yarn link of the cornerstone
// only set this to true if you don't have a yarn link to external libs
// otherwise expect the lib changes not to be reflected in the dev server
// as it will be cached
cacheDirectory: false,
},
};
}
+21 -29
View File
@@ -7,8 +7,7 @@ const fs = require('fs');
const webpack = require('webpack');
// ~~ PLUGINS
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ PackageJSON
@@ -26,11 +25,9 @@ const QUICK_BUILD = process.env.QUICK_BUILD;
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
// read from ../version.txt
const VERSION_NUMBER =
fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
const VERSION_NUMBER = fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
const COMMIT_HASH =
fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8') || '';
const COMMIT_HASH = fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8') || '';
//
dotenv.config();
@@ -73,21 +70,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
children: false,
warnings: true,
},
devServer: {
open: true,
port: 3000,
historyApiFallback: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
cache: {
type: 'filesystem',
},
module: {
noParse: [/(codec)/, /(dicomicc)/],
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'source-map-loader',
},
transpileJavaScriptRule(mode),
loadWebWorkersRule,
// loadShadersRule,
@@ -109,10 +102,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
alias: {
// Viewer project
'@': path.resolve(__dirname, '../platform/app/src'),
'@components': path.resolve(
__dirname,
'../platform/app/src/components'
),
'@components': path.resolve(__dirname, '../platform/app/src/components'),
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
@@ -135,7 +125,12 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
// symlinked resources are resolved to their real path, not their symlinked location
symlinks: true,
fallback: { fs: false, path: false, zlib: false },
fallback: {
fs: false,
path: false,
zlib: false,
buffer: require.resolve('buffer'),
},
},
plugins: [
new webpack.DefinePlugin({
@@ -149,15 +144,12 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(
process.env.LOCIZE_PROJECTID || ''
),
'process.env.LOCIZE_API_KEY': JSON.stringify(
process.env.LOCIZE_API_KEY || ''
),
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(
process.env.REACT_APP_I18N_DEBUG || ''
),
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''),
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
+131
View File
@@ -0,0 +1,131 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
### Bug Fixes
* **hotkeys:** preserve hotkeys if changed, and reduce re-rendering ([#3635](https://github.com/OHIF/Viewers/issues/3635)) ([94f7cfb](https://github.com/OHIF/Viewers/commit/94f7cfb08e3490488394efc42ef089ebe55e86be))
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
### Features
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
### Bug Fixes
* **nginx archive recipe:** Fixes to various configuration files. ([#3624](https://github.com/OHIF/Viewers/issues/3624)) ([3ce7225](https://github.com/OHIF/Viewers/commit/3ce72254b390f32c9aa207a0589e688805e2659d))
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
### Features
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
### Bug Fixes
* **OpenIdConnectRoutes:** fix handleUnauthenticated ([#3617](https://github.com/OHIF/Viewers/issues/3617)) ([35fc30c](https://github.com/OHIF/Viewers/commit/35fc30c5359d8199cc38ffa670c08687d2672f11))
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
### Bug Fixes
* **PT Metadata:** Allow for PatientWeight to be missing from the metadata ([#3621](https://github.com/OHIF/Viewers/issues/3621)) ([44f101d](https://github.com/OHIF/Viewers/commit/44f101d3f2b3204b67e31f4e4939062e65a246ee))
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
+7 -1
View File
@@ -38,6 +38,7 @@ COPY platform /usr/src/app/platform
# Copy Files
FROM node:16.15.0-slim as builder
RUN apt-get update && apt-get install -y build-essential python3
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
@@ -61,7 +62,7 @@ RUN yarn run build
# Stage 3: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.23.1-alpine as final
FROM nginxinc/nginx-unprivileged:1.25-alpine as final
#RUN apk add --no-cache bash
ENV PORT=80
RUN rm /etc/nginx/conf.d/default.conf
@@ -69,5 +70,10 @@ USER nginx
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
# The nginx user cannot chmod it, so change to root.
USER root
RUN chmod 666 /usr/share/nginx/html/app-config.js
USER nginx
ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+17 -11
View File
@@ -3,7 +3,7 @@
<div align="center">
<h1>OHIF Medical Imaging Viewer</h1>
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p>
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/using/dicomweb/">DICOMweb</a>.</p>
</div>
@@ -34,13 +34,15 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
<!-- [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) -->
<!-- prettier-ignore-end -->
| | |
| :-: | :--- |
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking |
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations |
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols |
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy |
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering |
| | | |
| :-: | :--- | :--- |
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.275741864483510678566144889372061815320) |
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
## About
@@ -161,7 +163,7 @@ yarn install
These commands are available from the root directory. Each project directory
also supports a number of commands that can be found in their respective
`README.md` and `project.json` files.
`README.md` and `package.json` files.
| Yarn Commands | Description |
| ---------------------------- | ------------------------------------------------------------- |
@@ -247,7 +249,7 @@ or, for v1, please cite:
> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334)
**Note:** If you use or find this repository helpful, please take the time to
star this repository on Github. This is an easy way for us to assess adoption
star this repository on GitHub. This is an easy way for us to assess adoption
and it can help us obtain future funding for the project.
This work is supported primarily by the National Institutes of Health, National
@@ -255,7 +257,11 @@ Cancer Institute, Informatics Technology for Cancer Research (ITCR) program,
under a
[grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104).
This project is tested with BrowserStack. Thank you for supporting open source
[NCI Imaging Data Commons (IDC) project](https://imaging.datacommons.cancer.gov/) supported the development of new features and bug fixes marked with ["IDC:priority"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Apriority),
["IDC:candidate"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acandidate) or ["IDC:collaboration"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acollaboration). NCI Imaging Data Commons is supported by contract number 19X037Q from
Leidos Biomedical Research under Task Order HHSN26100071 from NCI. [IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized version of the OHIF Viewer.
This project is tested with BrowserStack. Thank you for supporting open-source!
## License
+9 -9
View File
@@ -3,22 +3,22 @@ const { extendDefaultPlugins } = require('svgo');
module.exports = {
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
plugins: [
[
'inline-react-svg',
{
svgo: {
plugins: extendDefaultPlugins([
plugins: [
{
name: 'removeViewBox',
active: false,
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
]),
],
},
},
],
+1 -1
View File
@@ -1 +1 @@
14369414cf0193f00243ae5c9f8415cf7079a753
de0a74273371ca0f39e8678a098a94db8d88e7a5
+2 -6
View File
@@ -54,11 +54,7 @@ const getCommandsModule = () => ({
const ExampleContext = React.createContext();
function ExampleContextProvider({ children }) {
return (
<ExampleContext.Provider value={{ example: 'value' }}>
{children}
</ExampleContext.Provider>
);
return <ExampleContext.Provider value={{ example: 'value' }}>{children}</ExampleContext.Provider>;
}
const getContextModule = () => [
@@ -121,7 +117,7 @@ const getSopClassHandlerModule = (/* ... */) => {
const getToolbarModule = () => {};
// displaySet, viewportIndex, dataSource
// displaySet, dataSource
const getViewportModule = () => {
const wrappedViewport = props => {
return (
@@ -3,8 +3,7 @@ const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const pkg = require('./../package.json');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
@@ -38,13 +37,7 @@ module.exports = (env, argv) => {
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [
/\b(vtk.js)/,
/\b(dcmjs)/,
/\b(gl-matrix)/,
/^@ohif/,
/^@cornerstonejs/,
],
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
@@ -0,0 +1,110 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
@@ -10,7 +10,7 @@ module.exports = {
modules: 'commonjs',
debug: false,
},
"@babel/preset-typescript",
'@babel/preset-typescript',
],
'@babel/preset-react',
],
@@ -26,7 +26,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
"@babel/preset-typescript",
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
@@ -35,7 +35,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
"@babel/preset-typescript",
'@babel/preset-typescript',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.69",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -31,10 +31,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.2",
"@ohif/extension-cornerstone": "3.7.0-beta.2",
"@ohif/extension-default": "3.7.0-beta.2",
"@ohif/i18n": "3.7.0-beta.2",
"@ohif/core": "3.7.0-beta.69",
"@ohif/extension-cornerstone": "3.7.0-beta.69",
"@ohif/extension-default": "3.7.0-beta.69",
"@ohif/i18n": "3.7.0-beta.69",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -7,11 +7,7 @@ const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
let loadPromises = {};
function _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
) {
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
const instance = instances[0];
const {
@@ -56,10 +52,7 @@ function _getDisplaySetsFromSeries(
};
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
if (
instance.ReferencedFrameOfReferenceSequence &&
!instance.ReferencedSeriesSequence
) {
if (instance.ReferencedFrameOfReferenceSequence && !instance.ReferencedSeriesSequence) {
instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
instance.ReferencedFrameOfReferenceSequence
);
@@ -72,8 +65,7 @@ function _getDisplaySetsFromSeries(
const referencedSeries = referencedSeriesSequence[0];
displaySet.referencedImages =
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => {
@@ -88,14 +80,12 @@ function _getDisplaySetsFromSeries(
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID =
referencedDisplaySet.displaySetInstanceUID;
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
return referencedDisplaySet;
};
displaySet.load = ({ headers }) =>
_load(displaySet, servicesManager, extensionManager, headers);
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
return [displaySet];
}
@@ -194,11 +184,7 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
name: 'dicom-rt',
sopClassUids,
getDisplaySetsFromSeries: instances => {
return _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
);
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
},
},
];
@@ -2,11 +2,10 @@ import { id } from './id';
import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import hydrateRTDisplaySet from './utils/_hydrateRT';
const Component = React.lazy(() => {
return import(
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport'
);
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
});
const OHIFCornerstoneRTViewport = props => {
@@ -36,12 +35,14 @@ const extension: Types.Extensions.Extension = {
getViewportModule({
servicesManager,
extensionManager,
commandsManager,
}: Types.Extensions.ExtensionParams) {
const ExtendedOHIFCornerstoneRTViewport = props => {
return (
<OHIFCornerstoneRTViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);
@@ -59,3 +60,4 @@ const extension: Types.Extensions.Extension = {
};
export default extension;
export { hydrateRTDisplaySet };
@@ -25,23 +25,13 @@ async function checkAndLoadContourData(instance, datasource) {
if (Array.isArray(contourData)) {
promisesMap.has(referencedROINumber)
? promisesMap
.get(referencedROINumber)
.push(Promise.resolve(contourData))
: promisesMap.set(referencedROINumber, [
Promise.resolve(contourData),
]);
? promisesMap.get(referencedROINumber).push(Promise.resolve(contourData))
: promisesMap.set(referencedROINumber, [Promise.resolve(contourData)]);
} else if (contourData && contourData.BulkDataURI) {
const bulkDataURI = contourData.BulkDataURI;
if (
!datasource ||
!datasource.retrieve ||
!datasource.retrieve.bulkDataURI
) {
return Promise.reject(
'Invalid datasource object or retrieve function'
);
if (!datasource || !datasource.retrieve || !datasource.retrieve.bulkDataURI) {
return Promise.reject('Invalid datasource object or retrieve function');
}
const bulkDataPromise = datasource.retrieve.bulkDataURI({
@@ -74,10 +64,7 @@ async function checkAndLoadContourData(instance, datasource) {
ROIContour.ContourSequence.forEach((Contour, index) => {
const promise = resolvedPromises[index];
if (promise.status === 'fulfilled') {
if (
Array.isArray(promise.value) &&
promise.value.every(Number.isFinite)
) {
if (Array.isArray(promise.value) && promise.value.every(Number.isFinite)) {
// If promise.value is already an array of numbers, use it directly
Contour.ContourData = promise.value;
} else {
@@ -85,13 +72,8 @@ async function checkAndLoadContourData(instance, datasource) {
const uint8Array = new Uint8Array(promise.value);
const textDecoder = new TextDecoder();
const dataUint8Array = textDecoder.decode(uint8Array);
if (
typeof dataUint8Array === 'string' &&
dataUint8Array.includes('\\')
) {
Contour.ContourData = dataUint8Array
.split('\\')
.map(parseFloat);
if (typeof dataUint8Array === 'string' && dataUint8Array.includes('\\')) {
Contour.ContourData = dataUint8Array.split('\\').map(parseFloat);
} else {
Contour.ContourData = [];
}
@@ -120,9 +102,8 @@ export default async function loadRTStruct(
const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
referencedDisplaySet
);
const imageIdSopInstanceUidPairs =
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
// Set here is loading is asynchronous.
// If this function throws its set back to false.
@@ -137,20 +118,14 @@ export default async function loadRTStruct(
);
const dicomData = DicomMessage.readFile(segArrayBuffer);
const rtStructDataset = DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
const rtStructDataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
instance = rtStructDataset;
} else {
await checkAndLoadContourData(instance, dataSource);
}
const {
StructureSetROISequence,
ROIContourSequence,
RTROIObservationsSequence,
} = instance;
const { StructureSetROISequence, ROIContourSequence, RTROIObservationsSequence } = instance;
// Define our structure set entry and add it to the rtstruct module state.
const structureSet = {
@@ -174,19 +149,9 @@ export default async function loadRTStruct(
const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) {
const {
ContourImageSequence,
ContourData,
NumberOfContourPoints,
ContourGeometricType,
} = ContourSequenceArray[c];
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
ContourSequenceArray[c];
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
if (!imageId) {
continue;
}
let isSupported = false;
const points = [];
@@ -235,9 +200,7 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry
? imageIdSopInstanceUidPairsEntry.imageId
: null;
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
@@ -258,8 +221,7 @@ function _setROIContourMetadata(
isSupported
) {
const StructureSetROI = StructureSetROISequence.find(
structureSetROI =>
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
structureSetROI => structureSetROI.ROINumber === ROIContour.ReferencedROINumber
);
const ROIContourData = {
@@ -299,23 +261,15 @@ function _setROIContourDataColor(ROIContour, ROIContourData) {
}
}
function _setROIContourRTROIObservations(
ROIContourData,
RTROIObservationsSequence,
ROINumber
) {
function _setROIContourRTROIObservations(ROIContourData, RTROIObservationsSequence, ROINumber) {
const RTROIObservations = RTROIObservationsSequence.find(
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
);
if (RTROIObservations) {
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
const {
ObservationNumber,
ROIObservationDescription,
RTROIInterpretedType,
ROIInterpreter,
} = RTROIObservations;
const { ObservationNumber, ROIObservationDescription, RTROIInterpretedType, ROIInterpreter } =
RTROIObservations;
ROIContourData.RTROIObservations = {
ObservationNumber,
@@ -1,13 +1,6 @@
async function _hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
servicesManager,
}) {
const {
segmentationService,
hangingProtocolService,
viewportGridService,
} = servicesManager.services;
async function _hydrateRTDisplaySet({ rtDisplaySet, viewportId, servicesManager }) {
const { segmentationService, hangingProtocolService, viewportGridService } =
servicesManager.services;
const displaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
@@ -27,7 +20,7 @@ async function _hydrateRTDisplaySet({
const { viewports } = viewportGridService.getState();
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex,
viewportId,
displaySetInstanceUID
);
@@ -39,8 +32,8 @@ async function _hydrateRTDisplaySet({
// is being displayed, if so we need to update the viewport to use volume viewport
// (if already is not using it) since Cornerstone3D currently only supports
// volume viewport for segmentation
viewports.forEach((viewport, index) => {
if (index === viewportIndex) {
viewports.forEach(viewport => {
if (viewport.viewportId === viewportId) {
return;
}
@@ -51,9 +44,16 @@ async function _hydrateRTDisplaySet({
if (shouldDisplaySeg) {
updatedViewports.push({
viewportIndex: index,
viewportId: viewport.viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not.
needsRerendering: true,
initialImageOptions: {
preset: 'middle',
},
@@ -1,10 +1,5 @@
function createRTToolGroupAndAddTools(
ToolGroupService,
customizationService,
toolGroupId
) {
const { tools } =
customizationService.get('cornerstone.overlayViewportTools') ?? {};
function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
}
@@ -1,3 +1,4 @@
import { ButtonEnums } from '@ohif/ui';
import hydrateRTDisplaySet from './_hydrateRT';
const RESPONSE = {
@@ -9,21 +10,23 @@ const RESPONSE = {
function promptHydrateRT({
servicesManager,
rtDisplaySet,
viewportIndex,
viewportId,
toolGroupId = 'default',
preHydrateCallbacks,
}) {
const { uiViewportDialogService } = servicesManager.services;
return new Promise(async function(resolve, reject) {
const promptResult = await _askHydrate(
uiViewportDialogService,
viewportIndex
);
return new Promise(async function (resolve, reject) {
const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
viewportId,
toolGroupId,
servicesManager,
});
@@ -33,17 +36,17 @@ function promptHydrateRT({
});
}
function _askHydrate(uiViewportDialogService, viewportIndex) {
return new Promise(function(resolve, reject) {
function _askHydrate(uiViewportDialogService, viewportId) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
type: 'secondary',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
type: 'primary',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
},
@@ -54,7 +57,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
};
uiViewportDialogService.show({
viewportIndex,
viewportId,
type: 'info',
message,
actions,
@@ -1,11 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
import {
ViewportActionBar,
useViewportGrid,
LoadingIndicatorTotalPercent,
} from '@ohif/ui';
import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
import _hydrateRTdisplaySet from '../utils/_hydrateRT';
import promptHydrateRT from '../utils/promptHydrateRT';
@@ -21,10 +17,10 @@ function OHIFCornerstoneRTViewport(props) {
children,
displaySets,
viewportOptions,
viewportIndex,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const {
@@ -35,7 +31,9 @@ function OHIFCornerstoneRTViewport(props) {
customizationService,
} = servicesManager.services;
const toolGroupId = `${RT_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
const viewportId = viewportOptions.viewportId;
const toolGroupId = `${RT_TOOLGROUP_BASE_NAME}-${viewportId}`;
// RT viewport will always have a single display set
if (displaySets.length > 1) {
@@ -66,12 +64,10 @@ function OHIFCornerstoneRTViewport(props) {
// refs
const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportIndex } = viewportGrid;
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
referencedDisplaySetRef.current = {
displaySet: referencedDisplaySet,
@@ -91,14 +87,20 @@ function OHIFCornerstoneRTViewport(props) {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportId,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const {
displaySet: referencedDisplaySet,
} = referencedDisplaySetRef.current;
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
@@ -115,7 +117,7 @@ function OHIFCornerstoneRTViewport(props) {
onElementDisabled={onElementDisabled}
></Component>
);
}, [viewportIndex, rtDisplaySet, toolGroupId]);
}, [viewportId, rtDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
@@ -136,11 +138,7 @@ function OHIFCornerstoneRTViewport(props) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(
segmentationId,
newSelectedSegmentIndex,
toolGroupId
);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
@@ -153,31 +151,28 @@ function OHIFCornerstoneRTViewport(props) {
promptHydrateRT({
servicesManager,
viewportIndex,
viewportId,
rtDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportIndex, rtDisplaySet, rtIsLoading]);
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
if (
evt.rtDisplaySet.displaySetInstanceUID ===
rtDisplaySet.displaySetInstanceUID
) {
if (evt.rtDisplaySet.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) {
setRtIsLoading(false);
}
if (evt.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message:
'Overlapping segments detected which is not currently supported',
message: 'Overlapping segments detected which is not currently supported',
type: 'warning',
});
}
@@ -212,12 +207,10 @@ function OHIFCornerstoneRTViewport(props) {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex];
if (
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
) {
const activeViewport = viewports.get(activeViewportId);
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex,
viewportId: activeViewportId,
displaySetInstanceUIDs: [],
});
}
@@ -236,19 +229,13 @@ function OHIFCornerstoneRTViewport(props) {
return;
}
toolGroup = createRTToolGroupAndAddTools(
toolGroupService,
customizationService,
toolGroupId
);
toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
setToolGroupCreated(true);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
toolGroupId
);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
toolGroupService.destroyToolGroup(toolGroupId);
};
@@ -259,9 +246,7 @@ function OHIFCornerstoneRTViewport(props) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
toolGroupId
);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [rtDisplaySet]);
@@ -282,7 +267,7 @@ function OHIFCornerstoneRTViewport(props) {
return (
child &&
React.cloneElement(child, {
viewportIndex,
viewportId,
key: index,
})
);
@@ -303,9 +288,16 @@ function OHIFCornerstoneRTViewport(props) {
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await _hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
viewportId,
servicesManager,
});
@@ -333,26 +325,22 @@ function OHIFCornerstoneRTViewport(props) {
currentSeries: SeriesNumber,
seriesDescription: `RT Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined
? `${SpacingBetweenSlices.toFixed(2)}mm`
: '',
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex flex-row w-full h-full overflow-hidden">
<div className="relative flex h-full w-full flex-row overflow-hidden">
{rtIsLoading && (
<LoadingIndicatorTotalPercent
className="w-full h-full"
className="h-full w-full"
totalNumbers={processingProgress.totalSegments}
percentComplete={processingProgress.percentComplete}
loadingText="Loading RTSTRUCT..."
@@ -367,7 +355,7 @@ function OHIFCornerstoneRTViewport(props) {
OHIFCornerstoneRTViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportIndex: PropTypes.number.isRequired,
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
@@ -13,25 +13,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
ToolTipMessage = () => (
<div>This Segmentation is loaded in the segmentation panel</div>
);
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => <Icon name="status-untracked" />;
StatusIcon = () => (
<Icon
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
}
const StatusArea = () => (
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
{!isHydrated && (
<div
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
@@ -44,7 +47,10 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
return (
<>
{ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left">
<Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea />
</Tooltip>
)}
@@ -40,13 +40,7 @@ module.exports = (env, argv) => {
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [
/\b(vtk.js)/,
/\b(dcmjs)/,
/\b(gl-matrix)/,
/^@ohif/,
/^@cornerstonejs/,
],
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
@@ -0,0 +1,110 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
@@ -10,7 +10,7 @@ module.exports = {
modules: 'commonjs',
debug: false,
},
"@babel/preset-typescript",
'@babel/preset-typescript',
],
'@babel/preset-react',
],
@@ -26,7 +26,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
"@babel/preset-typescript",
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
@@ -35,7 +35,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
"@babel/preset-typescript",
'@babel/preset-typescript',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.69",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -31,10 +31,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.2",
"@ohif/extension-cornerstone": "3.7.0-beta.2",
"@ohif/extension-default": "3.7.0-beta.2",
"@ohif/i18n": "3.7.0-beta.2",
"@ohif/core": "3.7.0-beta.69",
"@ohif/extension-cornerstone": "3.7.0-beta.69",
"@ohif/extension-default": "3.7.0-beta.69",
"@ohif/i18n": "3.7.0-beta.69",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -5,7 +5,6 @@ const segProtocol: Types.HangingProtocol.Protocol = {
// Don't store this hanging protocol as it applies to the currently active
// display set by default
// cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'Segmentations',
// Just apply this one when specifically listed
protocolMatchingRules: [],
@@ -1,21 +1,15 @@
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
import { utils } from '@ohif/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
import dcmjs from 'dcmjs';
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
import { dicomlabToRGB } from './utils/dicomlabToRGB';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
let loadPromises = {};
function _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
) {
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
const instance = instances[0];
const {
@@ -68,8 +62,7 @@ function _getDisplaySetsFromSeries(
const referencedSeries = referencedSeriesSequence[0];
displaySet.referencedImages =
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => {
@@ -84,8 +77,7 @@ function _getDisplaySetsFromSeries(
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID =
referencedDisplaySet.displaySetInstanceUID;
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
@@ -118,17 +110,13 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
// We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (
!segDisplaySet.segments ||
Object.keys(segDisplaySet.segments).length === 0
) {
const segments = await _loadSegments(
if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers
);
segDisplaySet.segments = segments;
headers,
});
}
const suppressEvents = true;
@@ -147,152 +135,61 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
return loadPromises[SOPInstanceUID];
}
async function _loadSegments(extensionManager, segDisplaySet, headers) {
async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers }) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
const { segmentationService } = servicesManager.services;
const { dicomLoaderService } = utilityModule.exports;
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
segDisplaySet,
null,
headers
);
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
const dicomData = DicomMessage.readFile(segArrayBuffer);
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
if (!Array.isArray(dataset.SegmentSequence)) {
dataset.SegmentSequence = [dataset.SegmentSequence];
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
}
const segments = _getSegments(dataset);
return segments;
const { imageIds } = cachedReferencedVolume;
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete,
});
});
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
imageIds,
arrayBuffer,
metaData,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
}
});
Object.assign(segDisplaySet, results);
}
function _segmentationExists(segDisplaySet, segmentationService) {
// This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation(
segDisplaySet.displaySetInstanceUID
);
}
function _getPixelData(dataset, segments) {
let frameSize = Math.ceil((dataset.Rows * dataset.Columns) / 8);
let nextOffset = 0;
Object.keys(segments).forEach(segmentKey => {
const segment = segments[segmentKey];
segment.numberOfFrames = segment.functionalGroups.length;
segment.size = segment.numberOfFrames * frameSize;
segment.offset = nextOffset;
nextOffset = segment.offset + segment.size;
const packedSegment = dataset.PixelData[0].slice(
segment.offset,
nextOffset
);
segment.pixelData = dcmjs.data.BitArray.unpack(packedSegment);
segment.geometry = geometryFromFunctionalGroups(
dataset,
segment.functionalGroups
);
});
return segments;
}
function geometryFromFunctionalGroups(dataset, perFrame) {
let pixelMeasures =
dataset.SharedFunctionalGroupsSequence.PixelMeasuresSequence;
let planeOrientation =
dataset.SharedFunctionalGroupsSequence.PlaneOrientationSequence;
let planePosition = perFrame[0].PlanePositionSequence; // TODO: assume sorted frames!
const geometry = {};
// NB: DICOM PixelSpacing is defined as Row then Column,
// unlike ImageOrientationPatient
let spacingBetweenSlices = pixelMeasures.SpacingBetweenSlices;
if (!spacingBetweenSlices) {
if (pixelMeasures.SliceThickness) {
console.log('Using SliceThickness as SpacingBetweenSlices');
spacingBetweenSlices = pixelMeasures.SliceThickness;
}
}
geometry.spacing = [
pixelMeasures.PixelSpacing[1],
pixelMeasures.PixelSpacing[0],
spacingBetweenSlices,
].map(Number);
geometry.dimensions = [dataset.Columns, dataset.Rows, perFrame.length].map(
Number
);
let orientation = planeOrientation.ImageOrientationPatient.map(Number);
const columnStepToPatient = orientation.slice(0, 3);
const rowStepToPatient = orientation.slice(3, 6);
geometry.planeNormal = [];
vtkMath.cross(columnStepToPatient, rowStepToPatient, geometry.planeNormal);
let firstPosition = perFrame[0].PlanePositionSequence.ImagePositionPatient.map(
Number
);
let lastPosition = perFrame[
perFrame.length - 1
].PlanePositionSequence.ImagePositionPatient.map(Number);
geometry.sliceStep = [];
vtkMath.subtract(lastPosition, firstPosition, geometry.sliceStep);
vtkMath.normalize(geometry.sliceStep);
geometry.direction = columnStepToPatient
.concat(rowStepToPatient)
.concat(geometry.sliceStep);
geometry.origin = planePosition.ImagePositionPatient.map(Number);
return geometry;
}
function _getSegments(dataset) {
const segments = {};
dataset.SegmentSequence.forEach(segment => {
const cielab = segment.RecommendedDisplayCIELabValue;
const rgba = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
Math.round(x * 255)
);
rgba.push(255);
const segmentNumber = segment.SegmentNumber;
segments[segmentNumber] = {
color: rgba,
functionalGroups: [],
offset: null,
size: null,
pixelData: null,
label: segment.SegmentLabel,
};
});
// make a list of functional groups per segment
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
const segmentNumber =
functionalGroup.SegmentIdentificationSequence.ReferencedSegmentNumber;
segments[segmentNumber].functionalGroups.push(functionalGroup);
});
return _getPixelData(dataset, segments);
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
);
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
+7 -12
View File
@@ -6,11 +6,10 @@ import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import PanelSegmentation from './panels/PanelSegmentation';
import getHangingProtocolModule from './getHangingProtocolModule';
import hydrateSEGDisplaySet from './utils/_hydrateSEG';
const Component = React.lazy(() => {
return import(
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport'
);
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
});
const OHIFCornerstoneSEGViewport = props => {
@@ -37,11 +36,7 @@ const extension = {
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule: ({
servicesManager,
commandsManager,
extensionManager,
}): Types.Panel[] => {
getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => {
const wrappedPanelSegmentation = () => {
return (
<PanelSegmentation
@@ -69,14 +64,13 @@ const extension = {
<OHIFCornerstoneSEGViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);
};
return [
{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport },
];
return [{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport }];
},
/**
* SopClassHandlerModule should provide a list of sop class handlers that will be
@@ -88,4 +82,5 @@ const extension = {
getHangingProtocolModule,
};
export default extension;
export default extension;
export { hydrateSEGDisplaySet };
@@ -2,14 +2,13 @@ import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable } from '@ohif/ui';
import callInputDialog from './callInputDialog';
import { useAppConfig } from '@state';
import { useTranslation } from 'react-i18next';
export default function PanelSegmentation({
servicesManager,
commandsManager,
}) {
export default function PanelSegmentation({ servicesManager, commandsManager }) {
const { segmentationService, uiDialogService } = servicesManager.services;
const [appConfig] = useAppConfig();
const disableEditing = appConfig?.disableEditing;
const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
@@ -17,9 +16,7 @@ export default function PanelSegmentation({
segmentationService.getConfiguration()
);
const [segmentations, setSegmentations] = useState(() =>
segmentationService.getSegmentations()
);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
const [isMinimized, setIsMinimized] = useState({});
@@ -76,32 +73,20 @@ export default function PanelSegmentation({
};
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(
segmentationId
);
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
return toolGroupIds;
};
const onSegmentClick = (segmentationId, segmentIndex) => {
segmentationService.setActiveSegmentForSegmentation(
segmentationId,
segmentIndex
);
segmentationService.setActiveSegmentForSegmentation(segmentationId, segmentIndex);
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
// const toolGroupId =
segmentationService.setActiveSegmentationForToolGroup(
segmentationId,
toolGroupId
);
segmentationService.jumpToSegmentCenter(
segmentationId,
segmentIndex,
toolGroupId
);
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
});
};
@@ -116,11 +101,7 @@ export default function PanelSegmentation({
return;
}
segmentationService.setSegmentLabelForSegmentation(
segmentationId,
segmentIndex,
label
);
segmentationService.setSegmentLabelForSegmentation(segmentationId, segmentIndex, label);
});
};
@@ -189,7 +170,7 @@ export default function PanelSegmentation({
);
return (
<div className="flex flex-col flex-auto min-h-0 justify-between mt-1">
<div className="mt-1 flex min-h-0 flex-auto flex-col justify-between">
{/* show segmentation table */}
{segmentations?.length ? (
<SegmentationGroupTable
@@ -203,6 +184,7 @@ export default function PanelSegmentation({
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
disableEditing={disableEditing}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
@@ -210,25 +192,13 @@ export default function PanelSegmentation({
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'renderOutline',
value
)
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'outlineOpacity',
value
)
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
}
setRenderFill={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'renderFill',
value
)
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(
@@ -238,25 +208,13 @@ export default function PanelSegmentation({
)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'outlineWidthActive',
value
)
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
}
setFillAlpha={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'fillAlpha',
value
)
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'fillAlphaInactive',
value
)
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
/>
) : null}
@@ -1,5 +1,5 @@
import React from 'react';
import { Input, Dialog } from '@ohif/ui';
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
function callInputDialog(uiDialogService, label, callback) {
const dialogId = 'enter-segment-label';
@@ -29,8 +29,8 @@ function callInputDialog(uiDialogService, label, callback) {
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Confirm', type: 'secondary' },
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
@@ -39,7 +39,7 @@ function callInputDialog(uiDialogService, label, callback) {
label="Enter the segment label"
labelClassName="text-white text-[14px] leading-[1.2]"
autoFocus
className="bg-black border-primary-main"
className="border-primary-main bg-black"
type="text"
value={value.label}
onChange={event => {
@@ -1,13 +1,10 @@
async function _hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
viewportId: targetViewportId,
servicesManager,
}) {
const {
segmentationService,
hangingProtocolService,
viewportGridService,
} = servicesManager.services;
const { segmentationService, hangingProtocolService, viewportGridService } =
servicesManager.services;
const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
@@ -27,7 +24,7 @@ async function _hydrateSEGDisplaySet({
const { viewports } = viewportGridService.getState();
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex,
targetViewportId,
displaySetInstanceUID
);
@@ -37,8 +34,8 @@ async function _hydrateSEGDisplaySet({
// is being displayed, if so we need to update the viewport to use volume viewport
// (if already is not using it) since Cornerstone3D currently only supports
// volume viewport for segmentation
viewports.forEach((viewport, index) => {
if (index === viewportIndex) {
viewports.forEach((viewport, viewportId) => {
if (targetViewportId === viewportId) {
return;
}
@@ -49,9 +46,16 @@ async function _hydrateSEGDisplaySet({
if (shouldDisplaySeg) {
updatedViewports.push({
viewportIndex: index,
viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not.
needsRerendering: true,
initialImageOptions: {
preset: 'middle',
},
@@ -0,0 +1,14 @@
import dcmjs from 'dcmjs';
/**
* Converts a CIELAB color to an RGB color using the dcmjs library.
* @param cielab - The CIELAB color to convert.
* @returns The RGB color as an array of three integers between 0 and 255.
*/
function dicomlabToRGB(cielab: number[]): number[] {
const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x => Math.round(x * 255));
return rgb;
}
export { dicomlabToRGB };
@@ -1,10 +1,5 @@
function createSEGToolGroupAndAddTools(
ToolGroupService,
customizationService,
toolGroupId
) {
const { tools } =
customizationService.get('cornerstone.overlayViewportTools') ?? {};
function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
}
@@ -1,3 +1,4 @@
import { ButtonEnums } from '@ohif/ui';
import hydrateSEGDisplaySet from './_hydrateSEG';
const RESPONSE = {
@@ -6,23 +7,20 @@ const RESPONSE = {
HYDRATE_SEG: 5,
};
function promptHydrateSEG({
servicesManager,
segDisplaySet,
viewportIndex,
}) {
function promptHydrateSEG({ servicesManager, segDisplaySet, viewportId, preHydrateCallbacks }) {
const { uiViewportDialogService } = servicesManager.services;
return new Promise(async function(resolve, reject) {
const promptResult = await _askHydrate(
uiViewportDialogService,
viewportIndex
);
return new Promise(async function (resolve, reject) {
const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
viewportId,
servicesManager,
});
@@ -31,17 +29,17 @@ function promptHydrateSEG({
});
}
function _askHydrate(uiViewportDialogService, viewportIndex) {
return new Promise(function(resolve, reject) {
function _askHydrate(uiViewportDialogService, viewportId) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
type: 'secondary',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
type: 'primary',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
},
@@ -52,7 +50,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
};
uiViewportDialogService.show({
viewportIndex,
viewportId,
type: 'info',
message,
actions,
@@ -2,11 +2,7 @@ import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils } from '@ohif/core';
import {
LoadingIndicatorTotalPercent,
useViewportGrid,
ViewportActionBar,
} from '@ohif/ui';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import hydrateSEGDisplaySet from '../utils/_hydrateSEG';
@@ -20,13 +16,14 @@ function OHIFCornerstoneSEGViewport(props) {
children,
displaySets,
viewportOptions,
viewportIndex,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { t } = useTranslation('SEGViewport');
const viewportId = viewportOptions.viewportId;
const {
displaySetService,
@@ -36,7 +33,7 @@ function OHIFCornerstoneSEGViewport(props) {
customizationService,
} = servicesManager.services;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
// SEG viewport will always have a single display set
if (displaySets.length > 1) {
@@ -48,7 +45,6 @@ function OHIFCornerstoneSEGViewport(props) {
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
const [selectedSegment, setSelectedSegment] = useState(1);
// Hydration means that the SEG is opened and segments are loaded into the
@@ -67,11 +63,12 @@ function OHIFCornerstoneSEGViewport(props) {
// refs
const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportIndex } = viewportGrid;
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet
referencedDisplaySet,
segDisplaySet
);
referencedDisplaySetRef.current = {
@@ -92,14 +89,20 @@ function OHIFCornerstoneSEGViewport(props) {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportId,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const {
displaySet: referencedDisplaySet,
} = referencedDisplaySetRef.current;
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
@@ -117,7 +120,7 @@ function OHIFCornerstoneSEGViewport(props) {
// initialImageIndex={initialImageIndex}
></Component>
);
}, [viewportIndex, segDisplaySet, toolGroupId]);
}, [viewportId, segDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
@@ -139,11 +142,7 @@ function OHIFCornerstoneSEGViewport(props) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(
segmentationId,
newSelectedSegmentIndex,
toolGroupId
);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
@@ -156,31 +155,28 @@ function OHIFCornerstoneSEGViewport(props) {
promptHydrateSEG({
servicesManager,
viewportIndex,
viewportId,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportIndex, segDisplaySet, segIsLoading]);
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
if (
evt.segDisplaySet.displaySetInstanceUID ===
segDisplaySet.displaySetInstanceUID
) {
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
setSegIsLoading(false);
}
if (evt.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message:
'Overlapping segments detected which is not currently supported',
message: 'Overlapping segments detected which is not currently supported',
type: 'warning',
});
}
@@ -215,12 +211,10 @@ function OHIFCornerstoneSEGViewport(props) {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex];
if (
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
) {
const activeViewport = viewports.get(activeViewportId);
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex,
viewportId: activeViewportId,
displaySetInstanceUIDs: [],
});
}
@@ -241,19 +235,11 @@ function OHIFCornerstoneSEGViewport(props) {
// This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations.
toolGroup = createSEGToolGroupAndAddTools(
toolGroupService,
customizationService,
toolGroupId
);
setToolGroupCreated(true);
toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
toolGroupId
);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
// Only destroy the viewport specific implementation
toolGroupService.destroyToolGroup(toolGroupId);
@@ -265,9 +251,7 @@ function OHIFCornerstoneSEGViewport(props) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
toolGroupId
);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [segDisplaySet]);
@@ -288,7 +272,7 @@ function OHIFCornerstoneSEGViewport(props) {
return (
child &&
React.cloneElement(child, {
viewportIndex,
viewportId,
key: index,
})
);
@@ -308,15 +292,21 @@ function OHIFCornerstoneSEGViewport(props) {
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
viewportId,
servicesManager,
});
setIsHydrated(isHydrated);
};
return (
<>
<ViewportActionBar
@@ -337,26 +327,23 @@ function OHIFCornerstoneSEGViewport(props) {
studyDate: formatDate(StudyDate),
seriesDescription: `SEG Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
thickness: SliceThickness ? utils.roundNumber(SliceThickness, 2) : '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing:
SpacingBetweenSlices !== undefined
? `${SpacingBetweenSlices.toFixed(2)}mm`
: '',
SpacingBetweenSlices !== undefined ? utils.roundNumber(SpacingBetweenSlices, 2) : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex flex-row w-full h-full overflow-hidden">
<div className="relative flex h-full w-full flex-row overflow-hidden">
{segIsLoading && (
<LoadingIndicatorTotalPercent
className="w-full h-full"
className="h-full w-full"
totalNumbers={processingProgress.totalSegments}
percentComplete={processingProgress.percentComplete}
loadingText="Loading SEG..."
@@ -371,7 +358,7 @@ function OHIFCornerstoneSEGViewport(props) {
OHIFCornerstoneSEGViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportIndex: PropTypes.number.isRequired,
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
@@ -381,20 +368,28 @@ OHIFCornerstoneSEGViewport.defaultProps = {
customProps: {},
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet) {
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const {
SharedFunctionalGroupsSequence: [SharedFunctionalGroup],
} = segDisplaySet.instance;
const {
PixelMeasuresSequence: [PixelMeasures],
} = SharedFunctionalGroup;
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
PatientID: image0.PatientID,
PatientName: image0.PatientName,
PatientSex: image0.PatientSex,
PatientAge: image0.PatientAge,
SliceThickness: image0.SliceThickness,
SliceThickness: image0.SliceThickness || SliceThickness,
StudyDate: image0.StudyDate,
SeriesDescription: image0.SeriesDescription,
SeriesInstanceUID: image0.SeriesInstanceUID,
SeriesNumber: image0.SeriesNumber,
ManufacturerModelName: image0.ManufacturerModelName,
SpacingBetweenSlices: image0.SpacingBetweenSlices,
SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
};
return referencedDisplaySetMetadata;
@@ -2,37 +2,39 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Tooltip } from '@ohif/ui';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
const {t} = useTranslation("Common");
const loadStr = t("LOAD");
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
ToolTipMessage = () => (
<div>This Segmentation is loaded in the segmentation panel</div>
);
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => <Icon name="status-untracked" />;
case false:
StatusIcon = () => (
<Icon
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
const StatusArea = () => (
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
{!isHydrated && (
<div
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
@@ -42,11 +44,13 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
</div>
);
return (
<>
{ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left">
<Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea />
</Tooltip>
)}
@@ -41,13 +41,7 @@ module.exports = (env, argv) => {
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [
/\b(vtk.js)/,
/\b(dcmjs)/,
/\b(gl-matrix)/,
/^@ohif/,
/^@cornerstonejs/,
],
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
@@ -0,0 +1,119 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
### Features
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
### Features
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.69",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -32,10 +32,10 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.2",
"@ohif/extension-cornerstone": "3.7.0-beta.2",
"@ohif/extension-measurement-tracking": "3.7.0-beta.2",
"@ohif/ui": "3.7.0-beta.2",
"@ohif/core": "3.7.0-beta.69",
"@ohif/extension-cornerstone": "3.7.0-beta.69",
"@ohif/extension-measurement-tracking": "3.7.0-beta.69",
"@ohif/ui": "3.7.0-beta.69",
"dcmjs": "^0.29.5",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -44,9 +44,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.1.8",
"@cornerstonejs/core": "^1.1.8",
"@cornerstonejs/tools": "^1.1.8",
"@cornerstonejs/adapters": "^1.13.2",
"@cornerstonejs/core": "^1.13.2",
"@cornerstonejs/tools": "^1.13.2",
"classnames": "^2.3.2"
}
}
@@ -17,11 +17,7 @@ const { log } = OHIF;
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
*
*/
const _generateReport = (
measurementData,
additionalFindingTypes,
options = {}
) => {
const _generateReport = (measurementData, additionalFindingTypes, options = {}) => {
const filteredToolState = getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
@@ -54,16 +50,8 @@ const commandsModule = ({}) => {
* as opposed to Finding Sites.
* that you wish to serialize.
*/
downloadReport: ({
measurementData,
additionalFindingTypes,
options = {},
}) => {
const srDataset = actions.generateReport(
measurementData,
additionalFindingTypes,
options
);
downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => {
const srDataset = actions.generateReport(measurementData, additionalFindingTypes, options);
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
@@ -91,28 +79,19 @@ const commandsModule = ({}) => {
log.info('[DICOMSR] storeMeasurements');
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
log.error(
'[DICOMSR] datasource has no dataSource.store.dicom endpoint!'
);
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
return Promise.reject({});
}
try {
const naturalizedReport = _generateReport(
measurementData,
additionalFindingTypes,
options
);
const naturalizedReport = _generateReport(measurementData, additionalFindingTypes, options);
const { StudyInstanceUID, ContentSequence } = naturalizedReport;
// The content sequence has 5 or more elements, of which
// the `[4]` element contains the annotation data, so this is
// checking that there is some annotation data present.
if (!ContentSequence?.[4].ContentSequence?.length) {
console.log(
'naturalizedReport missing imaging content',
naturalizedReport
);
console.log('naturalizedReport missing imaging content', naturalizedReport);
throw new Error('Invalid report, no content');
}
@@ -130,12 +109,8 @@ const commandsModule = ({}) => {
return naturalizedReport;
} catch (error) {
console.warn(error);
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
throw new Error(
error.message || 'Error while saving the measurements.'
);
log.error(`[DICOMSR] Error while saving the measurements: ${error.message}`);
throw new Error(error.message || 'Error while saving the measurements.');
}
},
};
@@ -5,7 +5,6 @@ const srProtocol: Types.HangingProtocol.Protocol = {
// Don't store this hanging protocol as it applies to the currently active
// display set by default
// cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'SR Key Images',
// Just apply this one when specifically listed
protocolMatchingRules: [],
@@ -29,9 +29,7 @@ const validateSameStudyUID = (uid: string, instances): void => {
instances.forEach(it => {
if (it.StudyInstanceUID !== uid) {
console.warn('Not all instances have the same UID', uid, it);
throw new Error(
`Instances ${it.SOPInstanceUID} does not belong to ${uid}`
);
throw new Error(`Instances ${it.SOPInstanceUID} does not belong to ${uid}`);
}
});
};
@@ -51,10 +49,7 @@ const CodeNameCodeSequenceValues = {
const CodingSchemeDesignators = {
SRT: 'SRT',
CornerstoneCodeSchemes: [
Cornerstone3DCodeScheme.CodingSchemeDesignator,
'CST4',
],
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
};
const RELATIONSHIP_TYPE = {
@@ -71,10 +66,7 @@ const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
* @param instances is a list of instances from THIS series that are not
* in this DICOM SR Display Set already.
*/
function addInstances(
instances: InstanceMetadata[],
displaySetService: DisplaySetService
) {
function addInstances(instances: InstanceMetadata[], displaySetService: DisplaySetService) {
this.instances.push(...instances);
utils.sortStudyInstances(this.instances);
// The last instance is the newest one, so is the one most interesting.
@@ -93,11 +85,7 @@ function addInstances(
* @param servicesManager is the services that can be used for creating
* @returns The list of display sets created for the given instances object
*/
function _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
) {
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
// If the series has no instances, stop here
if (!instances || !instances.length) {
throw new Error('No instances were provided');
@@ -123,8 +111,7 @@ function _getDisplaySetsFromSeries(
if (
!ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.ImagingMeasurementReport
ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
) {
servicesManager.services.uiNotificationService.show({
title: 'DICOM SR',
@@ -184,36 +171,21 @@ function _load(displaySet, servicesManager, extensionManager) {
// Check currently added displaySets and add measurements if the sources exist.
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
activeDisplaySet,
dataSource
);
_checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource);
});
// Subscribe to new displaySets as the source may come in after.
displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
data => {
const { displaySetsAdded } = data;
// If there are still some measurements that have not yet been loaded into cornerstone,
// See if we can load them onto any of the new displaySets.
displaySetsAdded.forEach(newDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
newDisplaySet,
dataSource
);
});
}
);
displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => {
const { displaySetsAdded } = data;
// If there are still some measurements that have not yet been loaded into cornerstone,
// See if we can load them onto any of the new displaySets.
displaySetsAdded.forEach(newDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(displaySet, newDisplaySet, dataSource);
});
});
}
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
dataSource
) {
function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource) {
let unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
@@ -248,8 +220,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
const { coords } = measurement;
coords.forEach(coord => {
const SOPInstanceUID =
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
SOPInstanceUIDs.push(SOPInstanceUID);
@@ -257,9 +228,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
});
});
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(
newDisplaySet
);
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
for (const imageId of imageIdsForDisplaySet) {
if (!unloadedMeasurements.length) {
@@ -267,25 +236,13 @@ function _checkIfCanAddMeasurementsToDisplaySet(
return;
}
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(
imageId
);
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j];
if (
_measurementReferencesSOPInstanceUID(
measurement,
SOPInstanceUID,
frameNumber
)
) {
addMeasurement(
measurement,
imageId,
newDisplaySet.displaySetInstanceUID
);
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
unloadedMeasurements.splice(j, 1);
}
@@ -294,11 +251,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
}
function _measurementReferencesSOPInstanceUID(
measurement,
SOPInstanceUID,
frameNumber
) {
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
const { coords } = measurement;
// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
@@ -308,8 +261,9 @@ function _measurementReferencesSOPInstanceUID(
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
1;
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber))
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
return false;
}
for (let j = 0; j < coords.length; j++) {
const coord = coords[j];
@@ -323,11 +277,7 @@ function _measurementReferencesSOPInstanceUID(
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
);
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
@@ -342,30 +292,22 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
function _getMeasurements(ImagingMeasurementReportContentSequence) {
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.ImagingMeasurements
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
);
const MeasurementGroups = _getSequenceAsArray(
ImagingMeasurements.ContentSequence
).filter(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.MeasurementGroup
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
);
const mergedContentSequencesByTrackingUniqueIdentifiers = _getMergedContentSequencesByTrackingUniqueIdentifiers(
MeasurementGroups
);
const mergedContentSequencesByTrackingUniqueIdentifiers =
_getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
const measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
trackingUniqueIdentifier => {
const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
];
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
const measurement = _processMeasurement(mergedContentSequence);
@@ -378,15 +320,11 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
return measurements;
}
function _getMergedContentSequencesByTrackingUniqueIdentifiers(
MeasurementGroups
) {
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
MeasurementGroups.forEach(MeasurementGroup => {
const ContentSequence = _getSequenceAsArray(
MeasurementGroup.ContentSequence
);
const ContentSequence = _getSequenceAsArray(MeasurementGroup.ContentSequence);
const TrackingUniqueIdentifierItem = ContentSequence.find(
item =>
@@ -395,22 +333,16 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
);
if (!TrackingUniqueIdentifierItem) {
console.warn(
'No Tracking Unique Identifier, skipping ambiguous measurement.'
);
console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
}
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
if (
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
] === undefined
) {
if (mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] === undefined) {
// Add the full ContentSequence
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
] = [...ContentSequence];
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] = [
...ContentSequence,
];
} else {
// Add the ContentSequence minus the tracking identifier, as we have this
// Information in the merged ContentSequence anyway.
@@ -419,9 +351,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
item.ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
) {
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
].push(item);
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier].push(item);
}
});
}
@@ -446,18 +376,12 @@ function _processTID1410Measurement(mergedContentSequence) {
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
const graphicItem = mergedContentSequence.find(
group => group.ValueType === 'SCOORD'
);
const graphicItem = mergedContentSequence.find(group => group.ValueType === 'SCOORD');
const UIDREFContentItem = mergedContentSequence.find(
group => group.ValueType === 'UIDREF'
);
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
const TrackingIdentifierContentItem = mergedContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingIdentifier
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
);
if (!graphicItem) {
@@ -467,9 +391,7 @@ function _processTID1410Measurement(mergedContentSequence) {
return;
}
const NUMContentItems = mergedContentSequence.filter(
group => group.ValueType === 'NUM'
);
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
const measurement = {
loaded: false,
@@ -484,10 +406,7 @@ function _processTID1410Measurement(mergedContentSequence) {
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
)
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
);
}
});
@@ -496,32 +415,22 @@ function _processTID1410Measurement(mergedContentSequence) {
}
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(
group => group.ValueType === 'NUM'
);
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
const UIDREFContentItem = mergedContentSequence.find(
group => group.ValueType === 'UIDREF'
);
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
const TrackingIdentifierContentItem = mergedContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingIdentifier
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
);
const finding = mergedContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.Finding
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.Finding
);
const findingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.SRT &&
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite
item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SRT &&
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
);
const measurement = {
@@ -537,8 +446,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
finding.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
@@ -553,8 +461,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
);
if (cornerstoneFreeTextFindingSite) {
@@ -566,18 +473,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
}
NUMContentItems.forEach(item => {
const {
ConceptNameCodeSequence,
ContentSequence,
MeasuredValueSequence,
} = item;
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
const { ValueType } = ContentSequence;
if (!ValueType === 'SCOORD') {
console.warn(
`Graphic ${ValueType} not currently supported, skipping annotation.`
);
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
return;
}
@@ -590,10 +491,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
)
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
);
}
});
@@ -633,17 +531,12 @@ function _getCoordsFromSCOORDOrSCOORD3D(item) {
return coords;
}
function _getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
) {
function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
const { CodeMeaning } = ConceptNameCodeSequence;
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence;
const formatedNumericValue = NumericValue
? Number(NumericValue).toFixed(2)
: '';
const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
return {
label: CodeMeaning,
@@ -653,24 +546,20 @@ function _getLabelFromMeasuredValueSequence(
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.ImageLibrary
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
);
const ImageLibraryGroup = _getSequenceAsArray(
ImageLibrary.ContentSequence
).find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.ImageLibraryGroup
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
);
const referencedImages = [];
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
const { ReferencedSOPSequence } = item;
if (!ReferencedSOPSequence) return;
if (!ReferencedSOPSequence) {
return;
}
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
if (ref.ReferencedSOPClassUID) {
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
@@ -687,7 +576,9 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
}
function _getSequenceAsArray(sequence) {
if (!sequence) return [];
if (!sequence) {
return [];
}
return Array.isArray(sequence) ? sequence : [sequence];
}
@@ -1,8 +1,6 @@
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule, {
srProtocol,
} from './getHangingProtocolModule';
import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule';
import onModeEnter from './onModeEnter';
import getCommandsModule from './commandsModule';
import preRegistration from './init';
@@ -12,9 +10,7 @@ import hydrateStructuredReport from './utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
const Component = React.lazy(() => {
return import(
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport'
);
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport');
});
const OHIFCornerstoneSRViewport = props => {
+1 -3
View File
@@ -18,9 +18,7 @@ import toolNames from './tools/toolNames';
/**
* @param {object} configuration
*/
export default function init({
configuration = {},
}: Types.Extensions.ExtensionParams): void {
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
addTool(DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool, {});
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
@@ -40,49 +40,32 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
isPointNearTool = () => null;
getHandleNearImagePoint = () => null;
renderAnnotation = (
enabledElement: Types.IEnabledElement,
svgDrawingHelper: any
): void => {
renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => {
const { viewport } = enabledElement;
const { element } = viewport;
let annotations = annotation.state.getAnnotations(
this.getToolName(),
element
);
let annotations = annotation.state.getAnnotations(this.getToolName(), element);
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
if (!annotations?.length) {
return;
}
annotations = this.filterInteractableAnnotationsForElement(
element,
annotations
);
annotations = this.filterInteractableAnnotationsForElement(element, annotations);
if (!annotations?.length) {
return;
}
const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(
element
);
const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(element);
const {
activeIndex,
trackingUniqueIdentifiers,
} = trackingUniqueIdentifiersForElement;
const { activeIndex, trackingUniqueIdentifiers } = trackingUniqueIdentifiersForElement;
const activeTrackingUniqueIdentifier =
trackingUniqueIdentifiers[activeIndex];
const activeTrackingUniqueIdentifier = trackingUniqueIdentifiers[activeIndex];
// Filter toolData to only render the data for the active SR.
const filteredAnnotations = annotations.filter(annotation =>
trackingUniqueIdentifiers.includes(
annotation.data?.cachedStats?.TrackingUniqueIdentifier
)
trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier)
);
if (!viewport._actors?.size) {
@@ -138,8 +121,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
break;
case SCOORD_TYPES.ELLIPSE:
renderMethod = this.renderEllipse;
canvasCoordinatesAdapter =
utilities.math.ellipse.getCanvasEllipseCorners;
canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners;
break;
default:
throw new Error(`Unsupported GraphicType: ${GraphicType}`);
@@ -221,15 +203,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
renderableData.map((data, index) => {
canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
const handleGroupUID = '0';
drawing.drawHandles(
svgDrawingHelper,
annotationUID,
handleGroupUID,
canvasCoordinates,
{
color: options.color,
}
);
drawing.drawHandles(svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, {
color: options.color,
});
});
}
@@ -248,10 +224,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
canvasCoordinates.push(viewport.worldToCanvas(point));
// We get the other point for the arrow by using the image size
const imagePixelModule = metaData.get(
'imagePixelModule',
referencedImageId
);
const imagePixelModule = metaData.get('imagePixelModule', referencedImageId);
let xOffset = 10;
let yOffset = 10;
@@ -309,23 +282,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const rotation = viewport.getRotation();
canvasCoordinates = ellipsePointsWorld.map(p =>
viewport.worldToCanvas(p)
);
canvasCoordinates = ellipsePointsWorld.map(p => viewport.worldToCanvas(p));
let canvasCorners;
if (rotation == 90 || rotation == 270) {
canvasCorners = <Array<Types.Point2>>(
utilities.math.ellipse.getCanvasEllipseCorners([
canvasCoordinates[2],
canvasCoordinates[3],
canvasCoordinates[0],
canvasCoordinates[1],
])
);
canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners([
canvasCoordinates[2],
canvasCoordinates[3],
canvasCoordinates[0],
canvasCoordinates[1],
]) as Array<Types.Point2>;
} else {
canvasCorners = <Array<Types.Point2>>(
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates)
);
canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners(
canvasCoordinates
) as Array<Types.Point2>;
}
const lineUID = `${index}`;
@@ -368,23 +337,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
}
const textLines = this._getTextBoxLinesFromLabels(label);
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(
adaptedCanvasCoordinates
);
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(
canvasTextBoxCoords
);
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
const textBoxPosition = viewport.worldToCanvas(
annotation.data.handles.textBox.worldPosition
);
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
const textBoxUID = '1';
const textBoxOptions = this.getLinkedTextBoxStyle(
styleSpecifier,
annotation
);
const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
const boundingBox = drawing.drawLinkedTextBox(
svgDrawingHelper,
@@ -27,15 +27,11 @@ function setTrackingUniqueIdentifiersForElement(
};
}
function setActiveTrackingUniqueIdentifierForElement(
element,
TrackingUniqueIdentifier
) {
function setActiveTrackingUniqueIdentifierForElement(element, TrackingUniqueIdentifier) {
const enabledElement = getEnabledElement(element);
const { viewport } = enabledElement;
const trackingIdentifiersForElement =
state.trackingIdentifiersByViewportId[viewport.id];
const trackingIdentifiersForElement = state.trackingIdentifiersByViewportId[viewport.id];
if (trackingIdentifiersForElement) {
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(
@@ -8,11 +8,7 @@ const EPSILON = 1e-4;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
export default function addMeasurement(
measurement,
imageId,
displaySetInstanceUID
) {
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
// TODO -> Render rotated ellipse .
const toolName = toolNames.DICOMSRDisplay;
@@ -31,12 +27,7 @@ export default function addMeasurement(
}
measurementData.renderableData[GraphicType].push(
_getRenderableData(
GraphicType,
GraphicData,
imageId,
measurement.TrackingIdentifier
)
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
);
});
@@ -86,12 +77,7 @@ export default function addMeasurement(
delete measurement.coords;
}
function _getRenderableData(
GraphicType,
GraphicData,
imageId,
TrackingIdentifier
) {
function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) {
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
let renderableData: csTypes.Point3[];
@@ -207,38 +193,22 @@ function _getRenderableData(
throw new Error('imageId does not have imagePlaneModule metadata');
}
const {
columnCosines,
}: { columnCosines: csTypes.Point3 } = imagePlaneModule;
const { columnCosines }: { columnCosines: csTypes.Point3 } = imagePlaneModule;
// find which axis is parallel to the columnCosines
const columnCosinesVec = vec3.fromValues(...columnCosines);
const projectedMajorAxisOnColVec = Math.abs(
vec3.dot(columnCosinesVec, majorAxisVec)
);
const projectedMinorAxisOnColVec = Math.abs(
vec3.dot(columnCosinesVec, minorAxisVec)
);
const projectedMajorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, majorAxisVec));
const projectedMinorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, minorAxisVec));
const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec);
const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec);
renderableData = [];
if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) {
renderableData = [
pointsWorld[0],
pointsWorld[1],
pointsWorld[2],
pointsWorld[3],
];
renderableData = [pointsWorld[0], pointsWorld[1], pointsWorld[2], pointsWorld[3]];
} else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) {
renderableData = [
pointsWorld[2],
pointsWorld[3],
pointsWorld[0],
pointsWorld[1],
];
renderableData = [pointsWorld[2], pointsWorld[3], pointsWorld[0], pointsWorld[1]];
} else {
console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED');
}
@@ -1,10 +1,6 @@
import { addTool } from '@cornerstonejs/tools';
export default function addToolInstance(
name: string,
toolClass,
configuration?
): void {
export default function addToolInstance(name: string, toolClass, configuration?): void {
class InstanceClass extends toolClass {
static toolName = name;
}
@@ -3,12 +3,11 @@ import { DisplaySetService, classes } from '@ohif/core';
const ImageSet = classes.ImageSet;
const findInstance = (measurement, displaySetService: DisplaySetService) => {
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } =
measurement;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
if (!referencedDisplaySet.images) return;
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } = measurement;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!referencedDisplaySet.images) {
return;
}
return referencedDisplaySet.images.find(it => it.SOPInstanceUID === sopUid);
};
@@ -16,16 +15,17 @@ const findInstance = (measurement, displaySetService: DisplaySetService) => {
* contained within the provided display set.
* @return an array of instances referenced.
*/
const findReferencedInstances = (
displaySetService: DisplaySetService,
displaySet
) => {
const findReferencedInstances = (displaySetService: DisplaySetService, displaySet) => {
const instances = [];
const instanceById = {};
for (const measurement of displaySet.measurements) {
const { imageId } = measurement;
if (!imageId) continue;
if (instanceById[imageId]) continue;
if (!imageId) {
continue;
}
if (instanceById[imageId]) {
continue;
}
const instance = findInstance(measurement, displaySetService);
if (!instance) {
@@ -50,7 +50,7 @@ const findReferencedInstances = (
const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
const instances = findReferencedInstances(displaySetService, displaySet);
// This will be a member function of the created image set
const updateInstances = function() {
const updateInstances = function () {
this.images.splice(
0,
this.images.length,
@@ -9,11 +9,12 @@ const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
let instanceFound;
displaySets.find(displaySet => {
if (!displaySet.images) return false;
if (!displaySet.images) {
return false;
}
instanceFound = displaySet.images.find(
instanceMetadata =>
instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
instanceMetadata => instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
);
return !!instanceFound;
@@ -18,10 +18,7 @@ const findMostRecentStructuredReport = studies => {
}
if (isStructuredReportSeries(series)) {
if (
!mostRecentStructuredReport ||
compareSeriesDate(series, mostRecentStructuredReport)
) {
if (!mostRecentStructuredReport || compareSeriesDate(series, mostRecentStructuredReport)) {
mostRecentStructuredReport = series;
}
}
@@ -38,10 +35,7 @@ const findMostRecentStructuredReport = studies => {
* @returns {boolean}
*/
const isStructuredReportSeries = series => {
const supportedSopClassUIDs = [
'1.2.840.10008.5.1.4.1.1.88.22',
'1.2.840.10008.5.1.4.1.1.11.1',
];
const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22', '1.2.840.10008.5.1.4.1.1.11.1'];
const firstInstance = series.getFirstInstance();
const SOPClassUID = firstInstance.getData().metadata.SOPClassUID;
@@ -2,17 +2,12 @@ import OHIF from '@ohif/core';
import { annotation } from '@cornerstonejs/tools';
const { log } = OHIF;
function getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
) {
function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) {
const filteredToolState = {};
function addToFilteredToolState(annotation, toolType) {
if (!annotation.metadata?.referencedImageId) {
log.warn(
`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`
);
log.warn(`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`);
return;
}
@@ -30,9 +25,7 @@ function getFilteredCornerstoneToolState(
};
}
const measurementDataI = measurementData.find(
md => md.uid === annotation.annotationUID
);
const measurementDataI = measurementData.find(md => md.uid === annotation.annotationUID);
const toolData = imageIdSpecificToolState[toolType].data;
let { finding } = measurementDataI;
@@ -77,9 +70,7 @@ function getFilteredCornerstoneToolState(
for (let i = 0; i < framesOfReference.length; i++) {
const frameOfReference = framesOfReference[i];
const frameOfReferenceAnnotations = annotationManager.getAnnotations(
frameOfReference
);
const frameOfReferenceAnnotations = annotationManager.getAnnotations(frameOfReference);
const toolTypes = Object.keys(frameOfReferenceAnnotations);
@@ -91,9 +82,7 @@ function getFilteredCornerstoneToolState(
if (annotations) {
for (let k = 0; k < annotations.length; k++) {
const annotation = annotations[k];
const uidIndex = uids.findIndex(
uid => uid === annotation.annotationUID
);
const uidIndex = uids.findIndex(uid => uid === annotation.annotationUID);
if (uidIndex !== -1) {
addToFilteredToolState(annotation, toolType);
@@ -9,9 +9,7 @@
export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData;
let freeTextLabel = findingSites.find(
fs => fs.CodeValue === 'CORNERSTONEFREETEXT'
);
let freeTextLabel = findingSites.find(fs => fs.CodeValue === 'CORNERSTONEFREETEXT');
if (freeTextLabel) {
return freeTextLabel.CodeMeaning;
@@ -2,6 +2,8 @@ import { utilities, metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
const { locking } = CsAnnotation;
const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
@@ -12,20 +14,26 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => {
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') return;
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
return;
}
const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`;
const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning };
return ret;
};
const convertSites = (codingValues, sites) => {
if (!sites || !sites.length) return;
if (!sites || !sites.length) {
return;
}
const ret = [];
// Do as a loop to convert away from Proxy instances
for (let i = 0; i < sites.length; i++) {
// Deal with irregular conversion from dcmjs
const site = convertCode(codingValues, sites[i][0] || sites[i]);
if (site) ret.push(site);
if (site) {
ret.push(site);
}
}
return (ret.length && ret) || undefined;
};
@@ -35,23 +43,16 @@ const convertSites = (codingValues, sites) => {
*
*/
export default function hydrateStructuredReport(
{ servicesManager, extensionManager },
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
const disableEditing = appConfig?.disableEditing;
const dataSource = extensionManager.getActiveDataSource()[0];
const {
measurementService,
displaySetService,
customizationService,
} = servicesManager.services;
const { measurementService, displaySetService, customizationService } = servicesManager.services;
const codingValues = customizationService.getCustomization(
'codingValues',
{}
);
const displaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const codingValues = customizationService.getCustomization('codingValues', {});
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// TODO -> We should define a strict versioning somewhere.
const mappings = measurementService.getSourceMappings(
@@ -115,16 +116,14 @@ export default function hydrateStructuredReport(
// TODO: notification if no hydratable?
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType =
hydratableMeasurementsInSR[annotationType];
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber =
(toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
@@ -140,10 +139,7 @@ export default function hydrateStructuredReport(
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get(
'instance',
imageId
);
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
SeriesInstanceUIDs.push(SeriesInstanceUID);
@@ -152,23 +148,19 @@ export default function hydrateStructuredReport(
if (!targetStudyInstanceUID) {
targetStudyInstanceUID = StudyInstanceUID;
} else if (targetStudyInstanceUID !== StudyInstanceUID) {
console.warn(
'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'
);
console.warn('NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.');
}
}
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType =
hydratableMeasurementsInSR[annotationType];
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber =
(toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
@@ -198,21 +190,13 @@ export default function hydrateStructuredReport(
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
annotation.data.finding = convertCode(
codingValues,
toolData.finding?.[0]
);
annotation.data.findingSites = convertSites(
codingValues,
toolData.findingSites
);
annotation.data.finding = convertCode(codingValues, toolData.finding?.[0]);
annotation.data.findingSites = convertSites(codingValues, toolData.findingSites);
annotation.data.site = annotation.data.findingSites?.[0];
const matchingMapping = mappings.find(
m => m.annotationType === annotationType
);
const matchingMapping = mappings.find(m => m.annotationType === annotationType);
measurementService.addRawMeasurement(
const newAnnotationUID = measurementService.addRawMeasurement(
source,
annotationType,
{ annotation },
@@ -220,6 +204,11 @@ export default function hydrateStructuredReport(
dataSource
);
if (disableEditing) {
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
locking.setAnnotationLocked(addedAnnotation, true);
}
if (!imageIds.includes(imageId)) {
imageIds.push(imageId);
}
@@ -245,15 +234,14 @@ function _mapLegacyDataSet(dataset) {
);
// Retrieve the Measurements themselves
const measurementGroups = toArray(
imagingMeasurementContent.ContentSequence
).filter(codeMeaningEquals(GROUP));
const measurementGroups = toArray(imagingMeasurementContent.ContentSequence).filter(
codeMeaningEquals(GROUP)
);
// For each of the supported measurement types, compute the measurement data
const measurementData = {};
const cornerstoneToolClasses =
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const cornerstoneToolClasses = MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const registeredToolClasses = [];
@@ -263,13 +251,10 @@ function _mapLegacyDataSet(dataset) {
});
measurementGroups.forEach((measurementGroup, index) => {
const measurementGroupContentSequence = toArray(
measurementGroup.ContentSequence
);
const measurementGroupContentSequence = toArray(measurementGroup.ContentSequence);
const TrackingIdentifierGroup = measurementGroupContentSequence.find(
contentItem =>
contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
contentItem => contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
);
const TrackingIdentifier = TrackingIdentifierGroup.TextValue;
@@ -1,8 +1,7 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const cornerstoneAdapters =
adaptersSR.Cornerstone3D.MeasurementReport
.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
@@ -24,8 +23,7 @@ export default function isRehydratable(displaySet, mappings) {
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
adapterKey =>
typeof cornerstoneAdapters[adapterKey]
.isValidCornerstoneTrackingIdentifier === 'function'
typeof cornerstoneAdapters[adapterKey].isValidCornerstoneTrackingIdentifier === 'function'
);
const adapters = [];
@@ -48,19 +46,13 @@ export default function isRehydratable(displaySet, mappings) {
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
return adapter.isValidCornerstoneTrackingIdentifier(
mappedTrackingIdentifier
);
return adapter.isValidCornerstoneTrackingIdentifier(mappedTrackingIdentifier);
});
if (hydratable) {
return true;
}
console.log(
'Measurement is not rehydratable',
TrackingIdentifier,
measurements[i]
);
console.log('Measurement is not rehydratable', TrackingIdentifier, measurements[i]);
}
console.log('No measurements found which were rehydratable');
@@ -7,11 +7,11 @@ import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSR
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
const { formatDate } = utils;
const MEASUREMENT_TRACKING_EXTENSION_ID =
'@ohif/extension-measurement-tracking';
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
@@ -20,18 +20,18 @@ function OHIFCornerstoneSRViewport(props) {
children,
dataSource,
displaySets,
viewportIndex,
viewportLabel,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const {
displaySetService,
cornerstoneViewportService,
measurementService,
} = servicesManager.services;
const [appConfig] = useAppConfig();
const { displaySetService, cornerstoneViewportService, measurementService } =
servicesManager.services;
const viewportId = viewportOptions.viewportId;
// SR viewport will always have a single display set
if (displaySets.length > 1) {
@@ -43,15 +43,10 @@ function OHIFCornerstoneSRViewport(props) {
const [viewportGrid, viewportGridService] = useViewportGrid();
const [measurementSelected, setMeasurementSelected] = useState(0);
const [measurementCount, setMeasurementCount] = useState(1);
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(
null
);
const [
referencedDisplaySetMetadata,
setReferencedDisplaySetMetadata,
] = useState(null);
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null);
const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null);
const [element, setElement] = useState(null);
const { viewports, activeViewportIndex } = viewportGrid;
const { viewports, activeViewportId } = viewportGrid;
// Optional hook into tracking extension, if present.
let trackedMeasurements;
@@ -76,16 +71,14 @@ function OHIFCornerstoneSRViewport(props) {
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
measurementService.clearMeasurements();
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager },
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(
SeriesInstanceUIDs[0]
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
{
viewportIndex: activeViewportIndex,
viewportId: activeViewportId,
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
},
]);
@@ -123,11 +116,7 @@ function OHIFCornerstoneSRViewport(props) {
const updateViewport = useCallback(
newMeasurementSelected => {
const {
StudyInstanceUID,
displaySetInstanceUID,
sopClassUids,
} = srDisplaySet;
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) {
return;
@@ -136,9 +125,7 @@ function OHIFCornerstoneSRViewport(props) {
if (sopClassUids && sopClassUids.length > 1) {
// Todo: what happens if there are multiple SOP Classes? Why we are
// not throwing an error?
console.warn(
'More than one SOPClassUID in the same series is not yet supported.'
);
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
_getViewportReferencedDisplaySetData(
@@ -160,19 +147,11 @@ function OHIFCornerstoneSRViewport(props) {
// imageIdIndex will handle it by updating the viewport, but if they
// are the same we just need to use measurementService to jump to the
// new measurement
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const csViewport = cornerstoneViewportService.getCornerstoneViewport(
viewportInfo.getViewportId()
);
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const imageIds = csViewport.getImageIds();
const imageIdIndex = imageIds.indexOf(
measurements[newMeasurementSelected].imageId
);
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
if (imageIdIndex !== -1) {
csViewport.setImageIdIndex(imageIdIndex);
@@ -180,7 +159,7 @@ function OHIFCornerstoneSRViewport(props) {
}
});
},
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportIndex]
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
);
const getCornerstoneViewport = useCallback(() => {
@@ -225,9 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
}}
onElementEnabled={onElementEnabled}
initialImageIndex={initialImageIndex}
isJumpToMeasurementDisabled={true}
></Component>
);
}, [activeImageDisplaySetData, viewportIndex, measurementSelected]);
}, [activeImageDisplaySetData, viewportId, measurementSelected]);
const onMeasurementChange = useCallback(
direction => {
@@ -250,12 +230,7 @@ function OHIFCornerstoneSRViewport(props) {
setTrackingIdentifiers(newMeasurementSelected);
updateViewport(newMeasurementSelected);
},
[
measurementSelected,
measurementCount,
updateViewport,
setTrackingIdentifiers,
]
[measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers]
);
/**
@@ -265,12 +240,10 @@ function OHIFCornerstoneSRViewport(props) {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex];
if (
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
) {
const activeViewport = viewports[activeViewportId];
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex,
viewportId: activeViewportId,
displaySetInstanceUIDs: [],
});
}
@@ -337,7 +310,7 @@ function OHIFCornerstoneSRViewport(props) {
return (
child &&
React.cloneElement(child, {
viewportIndex,
viewportId,
key: index,
})
);
@@ -369,7 +342,7 @@ function OHIFCornerstoneSRViewport(props) {
getStatusComponent={() =>
_getStatusComponent({
srDisplaySet,
viewportIndex,
viewportId,
isTracked: false,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
@@ -383,23 +356,19 @@ function OHIFCornerstoneSRViewport(props) {
currentSeries: SeriesNumber,
seriesDescription: SeriesDescription || '',
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined
? `${SpacingBetweenSlices.toFixed(2)}mm`
: '',
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex flex-row w-full h-full overflow-hidden">
<div className="relative flex h-full w-full flex-row overflow-hidden">
{getCornerstoneViewport()}
{childrenWithProps}
</div>
@@ -409,7 +378,7 @@ function OHIFCornerstoneSRViewport(props) {
OHIFCornerstoneSRViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportIndex: PropTypes.number.isRequired,
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
viewportLabel: PropTypes.string,
@@ -434,9 +403,7 @@ async function _getViewportReferencedDisplaySetData(
const { displaySetInstanceUID } = measurement;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
@@ -458,7 +425,7 @@ async function _getViewportReferencedDisplaySetData(
function _getStatusComponent({
srDisplaySet,
viewportIndex,
viewportId,
isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
@@ -466,7 +433,7 @@ function _getStatusComponent({
const handleMouseUp = () => {
sendTrackedMeasurementsEvent('HYDRATE_SR', {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportIndex,
viewportId,
});
};
@@ -476,8 +443,7 @@ function _getStatusComponent({
// 1 - Incompatible
// 2 - Locked
// 3 - Rehydratable / Open
const state =
isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
const state = isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
let ToolTipMessage = null;
let StatusIcon = null;
@@ -507,22 +473,25 @@ function _getStatusComponent({
);
break;
case 3:
StatusIcon = () => <Icon name="status-untracked" />;
ToolTipMessage = () => (
<div>{`Click ${loadStr} to restore measurements.`}</div>
StatusIcon = () => (
<Icon
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</div>;
}
const StatusArea = () => (
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SR</span>
</div>
{state === 3 && (
<div
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={handleMouseUp}
>
@@ -535,7 +504,10 @@ function _getStatusComponent({
return (
<>
{ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left">
<Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea />
</Tooltip>
)}
@@ -40,13 +40,7 @@ module.exports = (env, argv) => {
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [
/\b(vtk.js)/,
/\b(dcmjs)/,
/\b(gl-matrix)/,
/^@ohif/,
/^@cornerstonejs/,
],
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
+116
View File
@@ -0,0 +1,116 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
### Features
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
### Features
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.69",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -36,9 +36,9 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.1.8",
"@ohif/core": "3.7.0-beta.2",
"@ohif/ui": "3.7.0-beta.2",
"@cornerstonejs/dicom-image-loader": "^1.13.2",
"@ohif/core": "3.7.0-beta.69",
"@ohif/ui": "3.7.0-beta.69",
"dcmjs": "^0.29.6",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -52,10 +52,10 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.1.8",
"@cornerstonejs/core": "^1.1.8",
"@cornerstonejs/streaming-image-volume-loader": "^1.1.8",
"@cornerstonejs/tools": "^1.1.8",
"@cornerstonejs/adapters": "^1.13.2",
"@cornerstonejs/core": "^1.13.2",
"@cornerstonejs/streaming-image-volume-loader": "^1.13.2",
"@cornerstonejs/tools": "^1.13.2",
"@kitware/vtk.js": "27.3.1",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
@@ -10,17 +10,8 @@ import {
utilities as csUtils,
} from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import {
CinePlayer,
useCine,
useViewportGrid,
Notification,
useViewportDialog,
} from '@ohif/ui';
import {
IStackViewport,
IVolumeViewport,
} from '@cornerstonejs/core/dist/esm/types';
import { Notification, useViewportDialog } from '@ohif/ui';
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
import { setEnabledElement } from '../state';
@@ -28,6 +19,8 @@ import './OHIFCornerstoneViewport.css';
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import CornerstoneServices from '../types/CornerstoneServices';
import CinePlayer from '../components/CinePlayer';
import { Types } from '@ohif/core';
const STACK = 'stack';
@@ -46,24 +39,19 @@ function areEqual(prevProps, nextProps) {
return false;
}
if (
prevProps.viewportOptions.orientation !==
nextProps.viewportOptions.orientation
) {
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
return false;
}
if (
prevProps.viewportOptions.toolGroupId !==
nextProps.viewportOptions.toolGroupId
) {
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
return false;
}
if (
prevProps.viewportOptions.viewportType !==
nextProps.viewportOptions.viewportType
) {
if (prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType) {
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
return false;
}
@@ -79,8 +67,7 @@ function areEqual(prevProps, nextProps) {
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID ===
prevDisplaySet.displaySetInstanceUID
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
@@ -95,9 +82,7 @@ function areEqual(prevProps, nextProps) {
// check if their imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (
foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId
) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
return false;
}
}
@@ -111,25 +96,24 @@ function areEqual(prevProps, nextProps) {
// Then we don't need to worry about the re-renders if the props change.
const OHIFCornerstoneViewport = React.memo(props => {
const {
viewportIndex,
displaySets,
dataSource,
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
onElementDisabled,
isJumpToMeasurementDisabled,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIndex,
} = props;
const viewportId = viewportOptions.viewportId;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportIndex }] = useViewportGrid();
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef();
const {
@@ -145,74 +129,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
} = servicesManager.services as CornerstoneServices;
const [viewportDialogState] = useViewportDialog();
const cineHandler = () => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
return;
}
const cine = cines[viewportIndex];
const isPlaying = cine.isPlaying || false;
const frameRate = cine.frameRate || 24;
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
}
};
useEffect(() => {
eventTarget.addEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
return () => {
cineService.setCine({ id: viewportIndex, isPlaying: false });
eventTarget.removeEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
};
}, [enabledVPElement]);
useEffect(() => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
};
}, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]);
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
itemId: 'cine',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
context: 'CORNERSTONE',
},
],
});
};
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
@@ -227,54 +143,17 @@ const OHIFCornerstoneViewport = React.memo(props => {
}
}, [elementRef]);
const storePresentation = () => {
const currentPresentation = cornerstoneViewportService.getPresentation(
viewportIndex
);
if (!currentPresentation || !currentPresentation.presentationIds) return;
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { presentationIds } = currentPresentation;
const { lutPresentationId, positionPresentationId } = presentationIds || {};
const storeState = {};
if (lutPresentationId) {
storeState.lutPresentationStore = {
...lutPresentationStore,
[lutPresentationId]: currentPresentation,
};
}
if (positionPresentationId) {
storeState.positionPresentationStore = {
...positionPresentationStore,
[positionPresentationId]: currentPresentation,
};
}
stateSyncService.store(storeState);
};
const cleanUpServices = useCallback(
viewportInfo => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
const cleanUpServices = useCallback(() => {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
if (!viewportInfo) {
return;
}
const viewportId = viewportInfo.getViewportId();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
syncGroups
);
}, [viewportIndex, viewportOptions.viewportId]);
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
},
[viewportId]
);
const elementEnabledHandler = useCallback(
evt => {
@@ -284,71 +163,48 @@ const OHIFCornerstoneViewport = React.memo(props => {
}
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(
viewportId
);
const viewportIndex = viewportInfo.getViewportIndex();
setEnabledElement(viewportIndex, element);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
setEnabledElement(viewportId, element);
setEnabledVPElement(element);
const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.addViewportToToolGroup(
viewportId,
renderingEngineId,
toolGroupId
);
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
syncGroupService.addViewportToSyncGroup(
viewportId,
renderingEngineId,
syncGroups
);
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
if (onElementEnabled) {
onElementEnabled(evt);
}
},
[viewportIndex, onElementEnabled, toolGroupService]
[viewportId, onElementEnabled, toolGroupService]
);
// disable the element upon unmounting
useEffect(() => {
cornerstoneViewportService.enableViewport(
viewportIndex,
viewportOptions,
elementRef.current
);
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
eventTarget.addEventListener(
Enums.Events.ELEMENT_ENABLED,
elementEnabledHandler
);
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
setImageScrollBarHeight();
return () => {
storePresentation();
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
cleanUpServices();
if (!viewportInfo) {
return;
}
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
cornerstoneViewportService.disableElement(viewportIndex);
cleanUpServices(viewportInfo);
cornerstoneViewportService.storePresentation({ viewportId });
if (onElementDisabled) {
onElementDisabled(viewportInfo);
}
eventTarget.removeEventListener(
Enums.Events.ELEMENT_ENABLED,
elementEnabledHandler
);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
}, []);
@@ -363,10 +219,15 @@ const OHIFCornerstoneViewport = React.memo(props => {
useEffect(() => {
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async invalidatedDisplaySetInstanceUID => {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
async ({
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
invalidateData,
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
return;
}
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
@@ -378,18 +239,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(
viewportIndex,
newViewportData,
keepCamera
);
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
}
}
);
return () => {
unsubscribe();
};
}, [viewportIndex]);
}, [viewportId]);
useEffect(() => {
// handle the default viewportType to be stack
@@ -408,33 +265,37 @@ const OHIFCornerstoneViewport = React.memo(props => {
// The presentation state will have been stored previously by closing
// a viewport. Otherwise, this viewport will be unchanged and the
// presentation information will be directly carried over.
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const { presentationIds } = viewportOptions;
const presentations = {
positionPresentation:
positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation:
lutPresentationStore[presentationIds?.lutPresentationId],
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
};
let measurement;
if (cacheJumpToMeasurementEvent?.viewportIndex === viewportIndex) {
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
if (viewportOptions.needsRerendering) {
viewportOptions.needsRerendering = false;
}
cornerstoneViewportService.setViewportData(
viewportIndex,
viewportId,
viewportData,
viewportOptions,
displaySetOptions,
presentations
);
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
}
@@ -454,11 +315,15 @@ const OHIFCornerstoneViewport = React.memo(props => {
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
measurementService,
displaySetService,
elementRef,
viewportIndex,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
@@ -468,7 +333,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
measurementService,
displaySetService,
elementRef,
viewportIndex,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
@@ -477,17 +342,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportIndex]);
}, [displaySets, elementRef, viewportId]);
return (
<React.Fragment>
<div className="viewport-wrapper">
<ReactResizeDetector
handleWidth
handleHeight
skipOnMount={true} // Todo: make these configurable
refreshMode={'debounce'}
refreshRate={200} // transition amount in side panel
refreshMode="debounce"
refreshRate={50} // Wait 50 ms after last move to render
onResize={onResize}
targetRef={elementRef.current}
/>
@@ -499,34 +361,20 @@ const OHIFCornerstoneViewport = React.memo(props => {
ref={elementRef}
></div>
<CornerstoneOverlays
viewportIndex={viewportIndex}
viewportId={viewportId}
toolBarService={toolbarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
{isCineEnabled && (
<CinePlayer
className="absolute left-1/2 -translate-x-1/2 bottom-3"
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
id: activeViewportIndex,
isPlaying,
})
}
onFrameRateChange={frameRate =>
cineService.setCine({
id: activeViewportIndex,
frameRate,
})
}
/>
)}
<CinePlayer
enabledVPElement={enabledVPElement}
viewportId={viewportId}
servicesManager={servicesManager}
/>
</div>
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
message={viewportDialogState.message}
@@ -545,35 +393,35 @@ function _subscribeToJumpToMeasurementEvents(
measurementService,
displaySetService,
elementRef,
viewportIndex,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
) {
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
);
const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
props => {
cacheJumpToMeasurementEvent = props;
const { viewportIndex: jumpIndex, measurement, isConsumed } = props;
if (!measurement || isConsumed) return;
const { viewportId: jumpId, measurement, isConsumed } = props;
if (!measurement || isConsumed) {
return;
}
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
// Decide on which viewport should handle this
cacheJumpToMeasurementEvent.cornerstoneViewport = cornerstoneViewportService.getViewportIndexToJump(
jumpIndex,
measurement.displaySetInstanceUID,
{ referencedImageId: measurement.referencedImageId }
);
cacheJumpToMeasurementEvent.cornerstoneViewport =
cornerstoneViewportService.getViewportIdToJump(
jumpId,
measurement.displaySetInstanceUID,
{ referencedImageId: measurement.referencedImageId }
);
}
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportIndex) {
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
return;
}
_jumpToMeasurement(
measurement,
elementRef,
viewportIndex,
viewportId,
measurementService,
displaySetService,
viewportGridService,
@@ -590,20 +438,22 @@ function _checkForCachedJumpToMeasurementEvents(
measurementService,
displaySetService,
elementRef,
viewportIndex,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
) {
if (!cacheJumpToMeasurementEvent) return;
if (!cacheJumpToMeasurementEvent) {
return;
}
if (cacheJumpToMeasurementEvent.isConsumed) {
cacheJumpToMeasurementEvent = null;
return;
}
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
);
if (!displaysUIDs?.length) return;
const displaysUIDs = displaySets.map(displaySet => displaySet.displaySetInstanceUID);
if (!displaysUIDs?.length) {
return;
}
// Jump to measurement if the measurement exists
const { measurement } = cacheJumpToMeasurementEvent;
@@ -612,7 +462,7 @@ function _checkForCachedJumpToMeasurementEvents(
_jumpToMeasurement(
measurement,
elementRef,
viewportIndex,
viewportId,
measurementService,
displaySetService,
viewportGridService,
@@ -625,7 +475,7 @@ function _checkForCachedJumpToMeasurementEvents(
function _jumpToMeasurement(
measurement,
targetElementRef,
viewportIndex,
viewportId,
measurementService,
displaySetService,
viewportGridService,
@@ -639,27 +489,19 @@ function _jumpToMeasurement(
return;
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
// to set it properly
// setCornerstoneMeasurementActive(measurement);
viewportGridService.setActiveViewportIndex(viewportIndex);
viewportGridService.setActiveViewportId(viewportId);
const enabledElement = getEnabledElement(targetElement);
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (enabledElement) {
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
const viewport = enabledElement.viewport as
| IStackViewport
| IVolumeViewport;
const viewport = enabledElement.viewport as IStackViewport | IVolumeViewport;
let imageIdIndex = 0;
let viewportCameraDirectionMatch = true;
@@ -667,14 +509,9 @@ function _jumpToMeasurement(
if (viewport instanceof StackViewport) {
const imageIds = viewport.getImageIds();
imageIdIndex = imageIds.findIndex(imageId => {
const {
SOPInstanceUID: aSOPInstanceUID,
frameNumber: aFrameNumber,
} = getSOPInstanceAttributes(imageId);
return (
aSOPInstanceUID === SOPInstanceUID &&
(!frameNumber || frameNumber === aFrameNumber)
);
const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } =
getSOPInstanceAttributes(imageId);
return aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber);
});
} else {
// for volume viewport we can't rely on the imageIdIndex since it can be
@@ -689,10 +526,7 @@ function _jumpToMeasurement(
// should compare abs for both planes since the direction can be flipped
if (
measurementViewPlane &&
!csUtils.isEqual(
measurementViewPlane.map(Math.abs),
viewportViewPlane.map(Math.abs)
)
!csUtils.isEqual(measurementViewPlane.map(Math.abs), viewportViewPlane.map(Math.abs))
) {
viewportCameraDirectionMatch = false;
}
@@ -716,21 +550,22 @@ function _jumpToMeasurement(
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
OHIFCornerstoneViewport.defaultProps = {
isJumpToMeasurementDisabled: false,
};
OHIFCornerstoneViewport.propTypes = {
viewportIndex: PropTypes.number.isRequired,
displaySets: PropTypes.array.isRequired,
dataSource: PropTypes.object.isRequired,
viewportOptions: PropTypes.object,
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
servicesManager: PropTypes.object.isRequired,
onElementEnabled: PropTypes.func,
isJumpToMeasurementDisabled: PropTypes.bool,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIdOrIndex: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
initialImageIdOrIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default OHIFCornerstoneViewport;
@@ -6,7 +6,7 @@ import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
function CornerstoneOverlays(props) {
const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
const { viewportId, element, scrollbarHeight, servicesManager } = props;
const { cornerstoneViewportService } = servicesManager.services;
const [imageSliceData, setImageSliceData] = useState({
imageIndex: 0,
@@ -18,7 +18,7 @@ function CornerstoneOverlays(props) {
const { unsubscribe } = cornerstoneViewportService.subscribe(
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
props => {
if (props.viewportIndex !== viewportIndex) {
if (props.viewportId !== viewportId) {
return;
}
@@ -29,15 +29,14 @@ function CornerstoneOverlays(props) {
return () => {
unsubscribe();
};
}, [viewportIndex]);
}, [viewportId]);
if (!element) {
return null;
}
if (viewportData) {
const viewportInfo =
cornerstoneViewportService.getViewportInfoByIndex(viewportIndex);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) {
return null;
@@ -47,7 +46,7 @@ function CornerstoneOverlays(props) {
return (
<div className="noselect">
<ViewportImageScrollbar
viewportIndex={viewportIndex}
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
@@ -59,7 +58,7 @@ function CornerstoneOverlays(props) {
<CustomizableViewportOverlay
imageSliceData={imageSliceData}
viewportData={viewportData}
viewportIndex={viewportIndex}
viewportId={viewportId}
servicesManager={servicesManager}
element={element}
/>
@@ -74,7 +73,7 @@ function CornerstoneOverlays(props) {
element={element}
viewportData={viewportData}
servicesManager={servicesManager}
viewportIndex={viewportIndex}
viewportId={viewportId}
/>
</div>
);
@@ -3,12 +3,7 @@ import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';
import {
formatPN,
formatDICOMDate,
formatDICOMTime,
formatNumberPrecision,
} from './utils';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { InstanceMetadata } from 'platform/core/src/types';
import { ServicesManager } from '@ohif/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
@@ -21,7 +16,6 @@ interface OverlayItemProps {
element: any;
viewportData: any;
imageSliceData: ImageSliceData;
viewportIndex: number | null;
servicesManager: ServicesManager;
instance: InstanceMetadata;
customization: any;
@@ -56,13 +50,9 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 font-light shrink-0">
{windowWidth.toFixed(0)}
</span>
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 font-light shrink-0">
{windowCenter.toFixed(0)}
</span>
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
@@ -114,14 +104,11 @@ function CustomizableViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
viewportId,
servicesManager,
}) {
const {
toolbarService,
cornerstoneViewportService,
customizationService,
} = servicesManager.services;
const { toolbarService, cornerstoneViewportService, customizationService } =
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
@@ -150,15 +137,10 @@ function CustomizableViewportOverlay({
const instanceNumber = useMemo(() => {
if (viewportData != null) {
return _getInstanceNumber(
viewportData,
viewportIndex,
imageIndex,
cornerstoneViewportService
);
return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
}
return null;
}, [viewportData, viewportIndex, imageIndex, cornerstoneViewportService]);
}, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
/**
* Initial toolbar state
@@ -179,10 +161,7 @@ function CustomizableViewportOverlay({
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
lower,
upper
);
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
setVOI({ windowCenter, windowWidth });
};
@@ -192,7 +171,7 @@ function CustomizableViewportOverlay({
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportIndex, viewportData, voi, element]);
}, [viewportId, viewportData, voi, element]);
/**
* Updating the scale when the viewport changes its zoom
@@ -205,9 +184,7 @@ function CustomizableViewportOverlay({
previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale
) {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
@@ -226,8 +203,7 @@ function CustomizableViewportOverlay({
const { spacing } = imageData;
// convert parallel scale to scale
const scale =
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale);
}
};
@@ -237,7 +213,7 @@ function CustomizableViewportOverlay({
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportIndex, viewportData, cornerstoneViewportService, element]);
}, [viewportId, viewportData, cornerstoneViewportService, element]);
/**
* Updating the active tools when the toolbar changes
@@ -262,7 +238,7 @@ function CustomizableViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
viewportId,
servicesManager,
customization: item,
formatters: {
@@ -296,7 +272,7 @@ function CustomizableViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
viewportId,
servicesManager,
customizationService,
instance,
@@ -343,9 +319,7 @@ function CustomizableViewportOverlay({
return (
<>
{items.map((item, i) => (
<div key={`bottomLeftOverlayItem_${i}`}>
{_renderOverlayItem(item)}
</div>
<div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
@@ -356,9 +330,7 @@ function CustomizableViewportOverlay({
return (
<>
{items.map((item, i) => (
<div key={`bottomRightOverlayItem_${i}`}>
{_renderOverlayItem(item)}
</div>
<div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
@@ -379,7 +351,7 @@ function _getViewportInstance(viewportData, imageIndex) {
if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageId = viewportData.data.imageIds[imageIndex];
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.volumes;
const volumes = viewportData.data;
if (volumes && volumes.length == 1) {
const volume = volumes[0];
imageId = volume.imageIds[imageIndex];
@@ -388,12 +360,7 @@ function _getViewportInstance(viewportData, imageIndex) {
return imageId ? metaData.get('instance', imageId) || {} : {};
}
function _getInstanceNumber(
viewportData,
viewportIndex,
imageIndex,
cornerstoneViewportService
) {
function _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService) {
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
@@ -406,7 +373,7 @@ function _getInstanceNumber(
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
viewportId,
cornerstoneViewportService
);
}
@@ -436,12 +403,7 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
function _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
cornerstoneViewportService
) {
function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewportService) {
const volumes = viewportData.volumes;
// Todo: support fusion of acquisition plane which has instanceNumber
@@ -452,9 +414,7 @@ function _getInstanceNumberFromVolume(
const volume = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!cornerstoneViewport) {
return;
@@ -477,8 +437,7 @@ function _getInstanceNumberFromVolume(
return {};
}
const { instanceNumber } =
metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber);
}
}
@@ -486,7 +445,7 @@ function _getInstanceNumberFromVolume(
CustomizableViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportIndex: PropTypes.number,
viewportId: PropTypes.string,
};
export default CustomizableViewportOverlay;
@@ -7,34 +7,24 @@ import { ServicesManger } from '@ohif/core';
function CornerstoneImageScrollbar({
viewportData,
viewportIndex,
viewportId,
element,
imageSliceData,
setImageSliceData,
scrollbarHeight,
servicesManager,
}) {
const {
cineService,
cornerstoneViewportService,
} = (servicesManager as ServicesManger).services;
const { cineService, cornerstoneViewportService } = (servicesManager as ServicesManger).services;
const onImageScrollbarChange = (imageIndex, viewportIndex) => {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const viewportId = viewportInfo.getViewportId();
const viewport = cornerstoneViewportService.getCornerstoneViewport(
viewportId
);
const onImageScrollbarChange = (imageIndex, viewportId) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { isCineEnabled } = cineService.getState();
if (isCineEnabled) {
// on image scrollbar change, stop the CINE if it is playing
cineService.stopClip(element);
cineService.setCine({ id: viewportIndex, isPlaying: false });
cineService.setCine({ id: viewportId, isPlaying: false });
}
csToolsUtils.jumpToSlice(viewport.element, {
@@ -48,9 +38,7 @@ function CornerstoneImageScrollbar({
return;
}
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
@@ -79,7 +67,7 @@ function CornerstoneImageScrollbar({
const { imageIndex, numberOfSlices } = sliceData;
setImageSliceData({ imageIndex, numberOfSlices });
}
}, [viewportIndex, viewportData]);
}, [viewportId, viewportData]);
useEffect(() => {
if (viewportData?.viewportType !== Enums.ViewportType.STACK) {
@@ -95,16 +83,10 @@ function CornerstoneImageScrollbar({
});
};
element.addEventListener(
Enums.Events.STACK_VIEWPORT_SCROLL,
updateStackIndex
);
element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
return () => {
element.removeEventListener(
Enums.Events.STACK_VIEWPORT_SCROLL,
updateStackIndex
);
element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
};
}, [viewportData, element]);
@@ -122,19 +104,14 @@ function CornerstoneImageScrollbar({
element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
return () => {
element.removeEventListener(
Enums.Events.VOLUME_NEW_IMAGE,
updateVolumeIndex
);
element.removeEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
};
}, [viewportData, element]);
return (
<ImageScrollbar
onChange={evt => onImageScrollbarChange(evt, viewportIndex)}
max={
imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0
}
onChange={evt => onImageScrollbarChange(evt, viewportId)}
max={imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0}
height={scrollbarHeight}
value={imageSliceData.imageIndex}
/>
@@ -143,7 +120,7 @@ function CornerstoneImageScrollbar({
CornerstoneImageScrollbar.propTypes = {
viewportData: PropTypes.object,
viewportIndex: PropTypes.number.isRequired,
viewportId: PropTypes.string.isRequired,
element: PropTypes.instanceOf(Element),
scrollbarHeight: PropTypes.string,
imageSliceData: PropTypes.object.isRequired,
@@ -33,26 +33,14 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
};
useEffect(() => {
element.addEventListener(
Enums.Events.STACK_VIEWPORT_SCROLL,
setLoadingState
);
element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, setLoadingState);
element.addEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
element.addEventListener(
Enums.Events.STACK_NEW_IMAGE,
setFinishLoadingState
);
element.addEventListener(Enums.Events.STACK_NEW_IMAGE, setFinishLoadingState);
return () => {
element.removeEventListener(
Enums.Events.STACK_VIEWPORT_SCROLL,
setLoadingState
);
element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, setLoadingState);
element.removeEventListener(
Enums.Events.STACK_NEW_IMAGE,
setFinishLoadingState
);
element.removeEventListener(Enums.Events.STACK_NEW_IMAGE, setFinishLoadingState);
element.removeEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
};
@@ -61,8 +49,8 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
if (error) {
return (
<>
<div className="bg-black opacity-50 absolute h-full w-full top-0 left-0">
<div className="flex transparent items-center justify-center w-full h-full">
<div className="absolute top-0 left-0 h-full w-full bg-black opacity-50">
<div className="transparent flex h-full w-full items-center justify-center">
<p className="text-primary-light text-xl font-light">
<h4>Error Loading Image</h4>
<p>An error has occurred.</p>
@@ -78,8 +66,8 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
return (
// IMPORTANT: we need to use the pointer-events-none class to prevent the loading indicator from
// interacting with the mouse, since scrolling should propagate to the viewport underneath
<div className="pointer-events-none bg-black opacity-50 absolute h-full w-full top-0 left-0">
<div className="flex transparent items-center justify-center w-full h-full">
<div className="pointer-events-none absolute top-0 left-0 h-full w-full bg-black opacity-50">
<div className="transparent flex h-full w-full items-center justify-center">
<p className="text-primary-light text-xl font-light">Loading...</p>
</div>
</div>
@@ -13,16 +13,13 @@ import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css';
const {
getOrientationStringLPS,
invertOrientationStringLPS,
} = utilities.orientation;
const { getOrientationStringLPS, invertOrientationStringLPS } = utilities.orientation;
function ViewportOrientationMarkers({
element,
viewportData,
imageSliceData,
viewportIndex,
viewportId,
servicesManager,
orientationMarkers = ['top', 'left'],
}) {
@@ -33,9 +30,7 @@ function ViewportOrientationMarkers({
const { cornerstoneViewportService } = servicesManager.services;
useEffect(() => {
const cameraModifiedListener = (
evt: Types.EventTypes.CameraModifiedEvent
) => {
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
const { rotation, previousCamera, camera } = evt.detail;
if (rotation !== undefined) {
@@ -57,16 +52,10 @@ function ViewportOrientationMarkers({
}
};
element.addEventListener(
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
element.addEventListener(Enums.Events.CAMERA_MODIFIED, cameraModifiedListener);
return () => {
element.removeEventListener(
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, cameraModifiedListener);
};
}, []);
@@ -85,8 +74,7 @@ function ViewportOrientationMarkers({
return false;
}
({ rowCosines, columnCosines } =
metaData.get('imagePlaneModule', imageId) || {});
({ rowCosines, columnCosines } = metaData.get('imagePlaneModule', imageId) || {});
} else {
if (!element || !getEnabledElement(element)) {
return '';
@@ -114,9 +102,7 @@ function ViewportOrientationMarkers({
flipHorizontal
);
const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
if (!ohifViewport) {
console.log('ViewportOrientationMarkers::No viewport');
@@ -126,9 +112,7 @@ function ViewportOrientationMarkers({
// Todo: probably this can be done in a better way in which we identify bright
// background
const isLight = backgroundColor
? csUtils.isEqual(backgroundColor, [1, 1, 1])
: false;
const isLight = backgroundColor ? csUtils.isEqual(backgroundColor, [1, 1, 1]) : false;
return orientationMarkers.map((m, index) => (
<div
@@ -174,13 +158,7 @@ ViewportOrientationMarkers.defaultProps = {
* @param {*} rotation in degrees
* @returns
*/
function _getOrientationMarkers(
rowCosines,
columnCosines,
rotation,
flipVertical,
flipHorizontal
) {
function _getOrientationMarkers(rowCosines, columnCosines, rotation, flipVertical, flipHorizontal) {
const rowString = getOrientationStringLPS(rowCosines);
const columnString = getOrientationStringLPS(columnCosines);
const oppositeRowString = invertOrientationStringLPS(rowString);
@@ -11,13 +11,10 @@ function CornerstoneViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
viewportId,
servicesManager,
}) {
const {
cornerstoneViewportService,
toolbarService,
} = servicesManager.services;
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
@@ -60,10 +57,7 @@ function CornerstoneViewportOverlay({
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
lower,
upper
);
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
setVOI({ windowCenter, windowWidth });
};
@@ -73,7 +67,7 @@ function CornerstoneViewportOverlay({
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportIndex, viewportData, voi, element]);
}, [viewportId, viewportData, voi, element]);
/**
* Updating the scale when the viewport changes its zoom
@@ -86,9 +80,7 @@ function CornerstoneViewportOverlay({
previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale
) {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
@@ -107,8 +99,7 @@ function CornerstoneViewportOverlay({
const { spacing } = imageData;
// convert parallel scale to scale
const scale =
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale);
}
};
@@ -118,7 +109,7 @@ function CornerstoneViewportOverlay({
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportIndex, viewportData]);
}, [viewportId, viewportData]);
const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi;
@@ -168,7 +159,7 @@ function CornerstoneViewportOverlay({
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
viewportId,
cornerstoneViewportService
);
}
@@ -183,15 +174,13 @@ function CornerstoneViewportOverlay({
</span>
</div>
);
}, [imageSliceData, viewportData, viewportIndex]);
}, [imageSliceData, viewportData, viewportId]);
if (!viewportData) {
return null;
}
const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
if (!ohifViewport) {
return null;
@@ -201,9 +190,7 @@ function CornerstoneViewportOverlay({
// Todo: probably this can be done in a better way in which we identify bright
// background
const isLight = backgroundColor
? utilities.isEqual(backgroundColor, [1, 1, 1])
: false;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
return (
<ViewportOverlay
@@ -240,7 +227,7 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
function _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
viewportId,
cornerstoneViewportService
) {
const volumes = viewportData.volumes;
@@ -253,9 +240,7 @@ function _getInstanceNumberFromVolume(
const volume = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!cornerstoneViewport) {
return;
@@ -278,8 +263,7 @@ function _getInstanceNumberFromVolume(
return {};
}
const { instanceNumber } =
metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber);
}
}
@@ -287,7 +271,7 @@ function _getInstanceNumberFromVolume(
CornerstoneViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportIndex: PropTypes.number,
viewportId: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager),
};
@@ -79,18 +79,12 @@ export function formatPN(name) {
*/
export function getCompression(imageId) {
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const {
lossyImageCompression,
lossyImageCompressionRatio,
lossyImageCompressionMethod,
} = generalImageModule;
const { lossyImageCompression, lossyImageCompressionRatio, lossyImageCompressionMethod } =
generalImageModule;
if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') {
const compressionMethod = lossyImageCompressionMethod || 'Lossy: ';
const compressionRatio = formatNumberPrecision(
lossyImageCompressionRatio,
2
);
const compressionRatio = formatNumberPrecision(lossyImageCompressionRatio, 2);
return compressionMethod + compressionRatio + ' : 1';
}
+63 -101
View File
@@ -18,6 +18,7 @@ import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { CornerstoneServices } from './types';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
function commandsModule({
servicesManager,
@@ -28,6 +29,7 @@ function commandsModule({
toolGroupService,
cineService,
toolbarService,
stateSyncService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
@@ -56,9 +58,7 @@ function commandsModule({
// filter by allowed selected tools from config property (if there is any)
const isToolAllowed =
!optionsToUse.allowedSelectedTools ||
optionsToUse.allowedSelectedTools.includes(
firstAnnotationSelected?.metadata?.toolName
);
optionsToUse.allowedSelectedTools.includes(firstAnnotationSelected?.metadata?.toolName);
if (isToolAllowed) {
optionsToUse.nearbyToolData = firstAnnotationSelected;
} else {
@@ -88,10 +88,7 @@ function commandsModule({
},
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
return (
nearbyToolData ??
cstUtils.getAnnotationNearPoint(element, canvasCoordinates)
);
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
},
getNearbyAnnotation({ element, canvasCoordinates }) {
const nearbyToolData = actions.getNearbyToolData({
@@ -108,18 +105,14 @@ function commandsModule({
}
const { renderingEngineId, viewportId } = enabledElement;
const toolGroup = ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);
const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngineId);
const toolInstance = toolGroup.getToolInstance(toolName);
return toolInstance?.constructor?.isAnnotation ?? true;
};
return nearbyToolData?.metadata?.toolName &&
isAnnotation(nearbyToolData.metadata.toolName)
return nearbyToolData?.metadata?.toolName && isAnnotation(nearbyToolData.metadata.toolName)
? nearbyToolData
: null;
},
@@ -152,11 +145,7 @@ function commandsModule({
label,
});
measurementService.update(
updatedMeasurement.uid,
updatedMeasurement,
true
);
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
},
false
);
@@ -219,27 +208,20 @@ function commandsModule({
}
}
}
measurementService.update(
updatedMeasurement.uid,
updatedMeasurement,
true
);
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
},
// Retrieve value commands
getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
setViewportActive: ({ viewportId }) => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(
viewportId
);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (!viewportInfo) {
console.warn('No viewport found for viewportId:', viewportId);
return;
}
const viewportIndex = viewportInfo.getViewportIndex();
viewportGridService.setActiveViewportIndex(viewportIndex);
viewportGridService.setActiveViewportId(viewportId);
},
arrowTextCallback: ({ callback, data }) => {
callInputDialog(uiDialogService, data, callback);
@@ -249,9 +231,7 @@ function commandsModule({
const { isCineEnabled } = cineService.getState();
cineService.setIsCineEnabled(!isCineEnabled);
toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
viewports.forEach((_, index) =>
cineService.setCine({ id: index, isPlaying: false })
);
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
},
setWindowLevel({ window, level, toolGroupId }) {
// convert to numbers
@@ -259,9 +239,7 @@ function commandsModule({
const windowCenterNum = Number(level);
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(
viewportId
);
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
@@ -271,10 +249,7 @@ function commandsModule({
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewport = renderingEngine.getViewport(viewportId);
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
windowWidthNum,
windowCenterNum
);
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
viewport.setProperties({
voiRange: {
@@ -292,7 +267,7 @@ function commandsModule({
toolbarService.recordInteraction(props);
},
setToolActive: ({ toolName, toolGroupId = null }) => {
setToolActive: ({ toolName, toolGroupId = null, toggledState }) => {
if (toolName === 'Crosshairs') {
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
@@ -309,9 +284,11 @@ function commandsModule({
}
}
const { viewports } = viewportGridService.getState() || {
viewports: [],
};
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
return;
}
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
@@ -321,14 +298,8 @@ function commandsModule({
return;
}
const filteredViewports = viewports.filter(viewport => {
if (!viewport.viewportOptions) {
return false;
}
return toolGroupViewportIds.includes(
viewport.viewportOptions.viewportId
);
const filteredViewports = Array.from(viewports.values()).filter(viewport => {
return toolGroupViewportIds.includes(viewport.viewportId);
});
if (!filteredViewports.length) {
@@ -357,6 +328,14 @@ function commandsModule({
toolGroup.setToolPassive(activeToolName);
}
}
// If there is a toggle state, then simply set the enabled/disabled state without
// setting the tool active.
if (toggledState != null) {
toggledState ? toolGroup.setToolEnabled(toolName) : toolGroup.setToolDisabled(toolName);
return;
}
// Set the new toolName to be active
toolGroup.setToolActive(toolName, {
bindings: [
@@ -367,13 +346,9 @@ function commandsModule({
});
},
showDownloadViewportModal: () => {
const { activeViewportIndex } = viewportGridService.getState();
const { activeViewportId } = viewportGridService.getState();
if (
!cornerstoneViewportService.getCornerstoneViewportByIndex(
activeViewportIndex
)
) {
if (!cornerstoneViewportService.getCornerstoneViewport(activeViewportId)) {
// Cannot download a non-cornerstone viewport (image).
uiNotificationService.show({
title: 'Download Image',
@@ -390,7 +365,7 @@ function commandsModule({
content: CornerstoneViewportDownloadForm,
title: 'Download High Quality Image',
contentProps: {
activeViewportIndex,
activeViewportId,
onClose: uiModalService.hide,
cornerstoneViewportService,
},
@@ -457,11 +432,9 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { invert } = viewport.getProperties();
viewport.setProperties({ invert: !invert });
viewport.render();
}
const { invert } = viewport.getProperties();
viewport.setProperties({ invert: !invert });
viewport.render();
},
resetViewport: () => {
const enabledElement = _getActiveViewportEnabledElement();
@@ -514,9 +487,7 @@ function commandsModule({
}
viewport = enabledElement.viewport;
} else {
viewport = cornerstoneViewportService.getCornerstoneViewport(
gridViewport.id
);
viewport = cornerstoneViewportService.getCornerstoneViewport(gridViewport.id);
}
// Get number of slices
@@ -526,14 +497,12 @@ function commandsModule({
if (viewport instanceof StackViewport) {
numberOfSlices = viewport.getImageIds().length;
} else if (viewport instanceof VolumeViewport) {
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport)
.numberOfSlices;
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices;
} else {
throw new Error('Unsupported viewport type');
}
const jumpIndex =
imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex;
const jumpIndex = imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex;
if (jumpIndex >= numberOfSlices || jumpIndex < 0) {
throw new Error(`Can't jump to ${imageIndex}`);
}
@@ -554,15 +523,8 @@ function commandsModule({
cstUtils.scroll(viewport, options);
},
setViewportColormap: ({
viewportIndex,
displaySetInstanceUID,
colormap,
immediate = false,
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, immediate = false }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const actorEntries = viewport.getActors();
@@ -578,17 +540,15 @@ function commandsModule({
viewport.render();
}
},
incrementActiveViewport: () => {
const { activeViewportIndex, viewports } = viewportGridService.getState();
const nextViewportIndex = (activeViewportIndex + 1) % viewports.length;
viewportGridService.setActiveViewportIndex(nextViewportIndex);
},
decrementActiveViewport: () => {
const { activeViewportIndex, viewports } = viewportGridService.getState();
changeActiveViewport: ({ direction = 1 }) => {
const { activeViewportId, viewports } = viewportGridService.getState();
const viewportIds = Array.from(viewports.keys());
const currentIndex = viewportIds.indexOf(activeViewportId);
const nextViewportIndex =
(activeViewportIndex - 1 + viewports.length) % viewports.length;
viewportGridService.setActiveViewportIndex(nextViewportIndex);
(currentIndex + direction + viewportIds.length) % viewportIds.length;
viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
},
toggleStackImageSync: ({ toggledState }) => {
toggleStackImageSync({
getEnabledElement,
@@ -596,19 +556,13 @@ function commandsModule({
toggledState,
});
},
toggleReferenceLines: ({ toggledState }) => {
const { activeViewportIndex } = viewportGridService.getState();
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
activeViewportIndex
);
setSourceViewportForReferenceLinesTool: ({ toggledState }) => {
const { activeViewportId } = viewportGridService.getState();
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
const viewportId = viewportInfo.getViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toggledState) {
toolGroup.setToolDisabled(ReferenceLinesTool.toolName);
}
toolGroup.setToolConfiguration(
ReferenceLinesTool.toolName,
{
@@ -616,7 +570,9 @@ function commandsModule({
},
true // overwrite
);
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
},
storePresentation: ({ viewportId }) => {
cornerstoneViewportService.storePresentation({ viewportId });
},
};
@@ -673,10 +629,11 @@ function commandsModule({
options: { rotation: -90 },
},
incrementActiveViewport: {
commandFn: actions.incrementActiveViewport,
commandFn: actions.changeActiveViewport,
},
decrementActiveViewport: {
commandFn: actions.decrementActiveViewport,
commandFn: actions.changeActiveViewport,
options: { direction: -1 },
},
flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal,
@@ -739,8 +696,13 @@ function commandsModule({
toggleStackImageSync: {
commandFn: actions.toggleStackImageSync,
},
toggleReferenceLines: {
commandFn: actions.toggleReferenceLines,
setSourceViewportForReferenceLinesTool: {
commandFn: actions.setSourceViewportForReferenceLinesTool,
},
storePresentation: {
commandFn: actions.storePresentation,
storeContexts: [],
options: {},
},
};
@@ -0,0 +1,97 @@
import React, { useEffect } from 'react';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
import { Enums, eventTarget } from '@cornerstonejs/core';
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const { toolbarService, customizationService } = servicesManager.services;
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportId }] = useViewportGrid();
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
itemId: 'cine',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
context: 'CORNERSTONE',
},
],
});
};
const cineHandler = () => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
return;
}
const cine = cines[viewportId];
const isPlaying = cine.isPlaying || false;
const frameRate = cine.frameRate || 24;
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
}
};
useEffect(() => {
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, cineHandler);
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, cineHandler);
};
}, [enabledVPElement]);
useEffect(() => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportId]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
};
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
const cine = cines[viewportId];
const isPlaying = (cine && cine.isPlaying) || false;
return (
isCineEnabled && (
<CinePlayerComponent
className="absolute left-1/2 bottom-3 -translate-x-1/2"
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
id: activeViewportId,
isPlaying,
})
}
onFrameRateChange={frameRate =>
cineService.setCine({
id: activeViewportId,
frameRate,
})
}
/>
)
);
}
export default WrappedCinePlayer;
@@ -0,0 +1,3 @@
import CinePlayer from './CinePlayer';
export default CinePlayer;
@@ -1,6 +1,23 @@
.dicom-upload-drop-area-border-dash {
background-image: repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%);
background-position: left top, left bottom, left top, right top;
background-image: repeating-linear-gradient(
to right,
#7bb2ce 0%,
#7bb2ce 50%,
transparent 50%,
transparent 100%
),
repeating-linear-gradient(to right, #7bb2ce 0%, #7bb2ce 50%, transparent 50%, transparent 100%),
repeating-linear-gradient(to bottom, #7bb2ce 0%, #7bb2ce 50%, transparent 50%, transparent 100%),
repeating-linear-gradient(to bottom, #7bb2ce 0%, #7bb2ce 50%, transparent 50%, transparent 100%);
background-position:
left top,
left bottom,
left top,
right top;
background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
background-size: 20px 3px, 20px 3px, 3px 20px, 3px 20px;
background-size:
20px 3px,
20px 3px,
3px 20px,
3px 20px;
}
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import DicomFileUploader from '../../utils/DicomFileUploader';
import DicomUploadProgress from './DicomUploadProgress';
import { Button } from '@ohif/ui';
import { Button, ButtonEnums } from '@ohif/ui';
import './DicomUpload.css';
type DicomUploadProps = {
@@ -14,19 +14,13 @@ type DicomUploadProps = {
onStarted: () => void;
};
function DicomUpload({
dataSource,
onComplete,
onStarted,
}: DicomUploadProps): ReactElement {
function DicomUpload({ dataSource, onComplete, onStarted }: DicomUploadProps): ReactElement {
const baseClassNames = 'min-h-[480px] flex flex-col bg-black select-none';
const [dicomFileUploaderArr, setDicomFileUploaderArr] = useState([]);
const onDrop = useCallback(async acceptedFiles => {
onStarted();
setDicomFileUploaderArr(
acceptedFiles.map(file => new DicomFileUploader(file, dataSource))
);
setDicomFileUploaderArr(acceptedFiles.map(file => new DicomFileUploader(file, dataSource)));
}, []);
const getDropZoneComponent = (): ReactElement => {
@@ -40,15 +34,16 @@ function DicomUpload({
{({ getRootProps }) => (
<div
{...getRootProps()}
className="m-5 dicom-upload-drop-area-border-dash flex flex-col items-center justify-center h-full"
className="dicom-upload-drop-area-border-dash m-5 flex h-full flex-col items-center justify-center"
>
<div className="flex gap-3">
<Dropzone onDrop={onDrop} noDrag>
<Dropzone
onDrop={onDrop}
noDrag
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<Button
variant="contained"
color="primary"
disabled={false}
onClick={() => {}}
>
@@ -58,13 +53,14 @@ function DicomUpload({
</div>
)}
</Dropzone>
<Dropzone onDrop={onDrop} noDrag>
<Dropzone
onDrop={onDrop}
noDrag
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<Button
variant="contained"
color="primaryDark"
border="primaryActive"
type={ButtonEnums.type.secondary}
disabled={false}
onClick={() => {}}
>
@@ -80,9 +76,7 @@ function DicomUpload({
</Dropzone>
</div>
<div className="pt-5">or drag images or folders here</div>
<div className="pt-3 text-aqua-pale text-lg">
(DICOM files supported)
</div>
<div className="text-aqua-pale pt-3 text-lg">(DICOM files supported)</div>
</div>
)}
</Dropzone>
@@ -99,9 +93,7 @@ function DicomUpload({
/>
</div>
) : (
<div className={classNames('h-[480px]', baseClassNames)}>
{getDropZoneComponent()}
</div>
<div className={classNames('h-[480px]', baseClassNames)}>{getDropZoneComponent()}</div>
)}
</>
);
@@ -1,10 +1,4 @@
import React, {
useCallback,
useEffect,
useRef,
useState,
ReactElement,
} from 'react';
import React, { useCallback, useEffect, useRef, useState, ReactElement } from 'react';
import PropTypes from 'prop-types';
import { Button, Icon, ProgressLoadingBar } from '@ohif/ui';
import DicomFileUploader, {
@@ -39,18 +33,14 @@ const BASE_INTERVAL_TIME = 15000;
// calculate the upload rate.
const UPLOAD_RATE_THRESHOLD = 75;
const NO_WRAP_ELLIPSIS_CLASS_NAMES =
'text-ellipsis whitespace-nowrap overflow-hidden';
const NO_WRAP_ELLIPSIS_CLASS_NAMES = 'text-ellipsis whitespace-nowrap overflow-hidden';
function DicomUploadProgress({
dicomFileUploaderArr,
onComplete,
}: DicomUploadProgressProps): ReactElement {
const [totalUploadSize] = useState(
dicomFileUploaderArr.reduce(
(acc, fileUploader) => acc + fileUploader.getFileSize(),
0
)
dicomFileUploaderArr.reduce((acc, fileUploader) => acc + fileUploader.getFileSize(), 0)
);
const currentUploadSizeRef = useRef<number>(0);
@@ -83,15 +73,13 @@ function DicomUploadProgress({
let intervalStartTime = Date.now();
const setUploadRateRef = () => {
const uploadSizeFromStartOfInterval =
currentUploadSizeRef.current - intervalStartUploadSize;
const uploadSizeFromStartOfInterval = currentUploadSizeRef.current - intervalStartUploadSize;
const now = Date.now();
const timeSinceStartOfInterval = now - intervalStartTime;
// Calculate and set the upload rate (ref)
uploadRateRef.current =
uploadSizeFromStartOfInterval / timeSinceStartOfInterval;
uploadRateRef.current = uploadSizeFromStartOfInterval / timeSinceStartOfInterval;
// Reset the interval starting values.
intervalStartUploadSize = currentUploadSizeRef.current;
@@ -134,28 +122,19 @@ function DicomUploadProgress({
const updateProgress = (percentComplete: number) => {
const previousFileUploadSize = currentFileUploadSize;
currentFileUploadSize = Math.round(
(percentComplete / 100) * fileUploader.getFileSize()
);
currentFileUploadSize = Math.round((percentComplete / 100) * fileUploader.getFileSize());
currentUploadSizeRef.current = Math.min(
totalUploadSize,
currentUploadSizeRef.current -
previousFileUploadSize +
currentFileUploadSize
currentUploadSizeRef.current - previousFileUploadSize + currentFileUploadSize
);
setPercentComplete(
(currentUploadSizeRef.current / totalUploadSize) * 100
);
setPercentComplete((currentUploadSizeRef.current / totalUploadSize) * 100);
if (uploadRateRef.current !== 0) {
const uploadSizeRemaining =
totalUploadSize - currentUploadSizeRef.current;
const uploadSizeRemaining = totalUploadSize - currentUploadSizeRef.current;
const timeRemaining = Math.round(
uploadSizeRemaining / uploadRateRef.current
);
const timeRemaining = Math.round(uploadSizeRemaining / uploadRateRef.current);
if (currentTimeRemaining === null) {
currentTimeRemaining = timeRemaining;
@@ -168,9 +147,7 @@ function DicomUploadProgress({
// due to rounding, inaccuracies in the estimate and slight variations
// in upload rates over time.
if (timeRemaining < ONE_MINUTE) {
const currentSecondsRemaining = Math.ceil(
currentTimeRemaining / ONE_SECOND
);
const currentSecondsRemaining = Math.ceil(currentTimeRemaining / ONE_SECOND);
const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND);
const delta = secondsRemaining - currentSecondsRemaining;
if (delta < 0 || delta > 2) {
@@ -181,9 +158,7 @@ function DicomUploadProgress({
}
if (timeRemaining < ONE_HOUR) {
const currentMinutesRemaining = Math.ceil(
currentTimeRemaining / ONE_MINUTE
);
const currentMinutesRemaining = Math.ceil(currentTimeRemaining / ONE_MINUTE);
const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE);
const delta = minutesRemaining - currentMinutesRemaining;
if (delta < 0 || delta > 2) {
@@ -199,9 +174,7 @@ function DicomUploadProgress({
}
};
const progressCallback = (
progressEvent: DicomFileUploaderProgressEvent
) => {
const progressCallback = (progressEvent: DicomFileUploaderProgressEvent) => {
updateProgress(progressEvent.percentComplete);
};
@@ -249,16 +222,12 @@ function DicomUploadProgress({
if (timeRemaining < ONE_MINUTE) {
const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND);
return `${secondsRemaining} ${
secondsRemaining === 1 ? 'second' : 'seconds'
}`;
return `${secondsRemaining} ${secondsRemaining === 1 ? 'second' : 'seconds'}`;
}
if (timeRemaining < ONE_HOUR) {
const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE);
return `${minutesRemaining} ${
minutesRemaining === 1 ? 'minute' : 'minutes'
}`;
return `${minutesRemaining} ${minutesRemaining === 1 ? 'minute' : 'minutes'}`;
}
const hoursRemaining = Math.ceil(timeRemaining / ONE_HOUR);
@@ -278,9 +247,7 @@ function DicomUploadProgress({
const showInfiniteProgressBar = useCallback((): boolean => {
return (
getPercentCompleteRounded() < 1 &&
(progressBarContainerRef?.current?.offsetWidth ?? 0) *
(percentComplete / 100) <
1
(progressBarContainerRef?.current?.offsetWidth ?? 0) * (percentComplete / 100) < 1
);
}, [getPercentCompleteRounded, percentComplete]);
@@ -300,17 +267,13 @@ function DicomUploadProgress({
const getNumCompletedAndTimeRemainingComponent = (): ReactElement => {
return (
<div className="text-lg px-1 pb-4 h-14 flex bg-primary-dark items-center">
<div className="bg-primary-dark flex h-14 items-center px-1 pb-4 text-lg">
{numFilesCompleted === dicomFileUploaderArr.length ? (
<>
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{`${
dicomFileUploaderArr.length
} ${
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{`${dicomFileUploaderArr.length} ${
dicomFileUploaderArr.length > 1 ? 'files' : 'file'
} completed.`}</span>
<Button
variant="contained"
color="primary"
disabled={false}
className="ml-auto"
onClick={onComplete}
@@ -326,18 +289,14 @@ function DicomUploadProgress({
>
{`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}&nbsp;
</span>
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{' files completed.'}&nbsp;</span>
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
{' files completed.'}&nbsp;
</span>
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
{timeRemaining
? `Less than ${getFormattedTimeRemaining()} remaining. `
: ''}
{timeRemaining ? `Less than ${getFormattedTimeRemaining()} remaining. ` : ''}
</span>
<span
className={classNames(
NO_WRAP_ELLIPSIS_CLASS_NAMES,
'cursor-pointer text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto'
'text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto cursor-pointer'
)}
onClick={cancelAllUploads}
>
@@ -351,14 +310,13 @@ function DicomUploadProgress({
const getShowFailedOnlyIconComponent = (): ReactElement => {
return (
<div className="ml-auto flex justify-center w-6">
<div className="ml-auto flex w-6 justify-center">
{numFails > 0 && (
<div
onClick={() =>
setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly)
}
>
<Icon className="cursor-pointer" name="icon-status-alert"></Icon>
<div onClick={() => setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly)}>
<Icon
className="cursor-pointer"
name="icon-status-alert"
></Icon>
</div>
)}
</div>
@@ -367,31 +325,28 @@ function DicomUploadProgress({
const getPercentCompleteComponent = (): ReactElement => {
return (
<div className="overflow-y-scroll ohif-scrollbar px-2 border-b border-secondary-light">
<div className="flex w-full p-2.5 items-center min-h-14">
<div className="ohif-scrollbar border-secondary-light overflow-y-scroll border-b px-2">
<div className="min-h-14 flex w-full items-center p-2.5">
{numFilesCompleted === dicomFileUploaderArr.length ? (
<>
<div className="text-xl text-primary-light">
<div className="text-primary-light text-xl">
{numFails > 0
? `Completed with ${numFails} ${
numFails > 1 ? 'errors' : 'error'
}!`
? `Completed with ${numFails} ${numFails > 1 ? 'errors' : 'error'}!`
: 'Completed!'}
</div>
{getShowFailedOnlyIconComponent()}
</>
) : (
<>
<div ref={progressBarContainerRef} className="flex-grow">
<div
ref={progressBarContainerRef}
className="flex-grow"
>
<ProgressLoadingBar
progress={
showInfiniteProgressBar()
? undefined
: Math.min(100, percentComplete)
}
progress={showInfiniteProgressBar() ? undefined : Math.min(100, percentComplete)}
></ProgressLoadingBar>
</div>
<div className="w-24 ml-1 flex items-center">
<div className="ml-1 flex w-24 items-center">
<div className="w-10 text-right">{`${getPercentCompleteRounded()}%`}</div>
{getShowFailedOnlyIconComponent()}
</div>
@@ -403,16 +358,15 @@ function DicomUploadProgress({
};
return (
<div className="flex flex-col grow">
<div className="flex grow flex-col">
{getNumCompletedAndTimeRemainingComponent()}
<div className="flex flex-col bg-black text-lg overflow-hidden grow">
<div className="flex grow flex-col overflow-hidden bg-black text-lg">
{getPercentCompleteComponent()}
<div className="overflow-y-scroll ohif-scrollbar px-2 grow h-1">
<div className="ohif-scrollbar h-1 grow overflow-y-scroll px-2">
{dicomFileUploaderArr
.filter(
dicomFileUploader =>
!showFailedOnly ||
dicomFileUploader.getStatus() === UploadStatus.Failed
!showFailedOnly || dicomFileUploader.getStatus() === UploadStatus.Failed
)
.map(dicomFileUploader => (
<DicomUploadProgressItem
@@ -427,9 +381,7 @@ function DicomUploadProgress({
}
DicomUploadProgress.propTypes = {
dicomFileUploaderArr: PropTypes.arrayOf(
PropTypes.instanceOf(DicomFileUploader)
).isRequired,
dicomFileUploaderArr: PropTypes.arrayOf(PropTypes.instanceOf(DicomFileUploader)).isRequired,
onComplete: PropTypes.func.isRequired,
};
@@ -1,10 +1,4 @@
import React, {
ReactElement,
memo,
useCallback,
useEffect,
useState,
} from 'react';
import React, { ReactElement, memo, useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import DicomFileUploader, {
DicomFileUploaderProgressEvent,
@@ -21,13 +15,10 @@ type DicomUploadProgressItemProps = {
// eslint-disable-next-line react/display-name
const DicomUploadProgressItem = memo(
({ dicomFileUploader }: DicomUploadProgressItemProps): ReactElement => {
const [percentComplete, setPercentComplete] = useState(
dicomFileUploader.getPercentComplete()
);
const [percentComplete, setPercentComplete] = useState(dicomFileUploader.getPercentComplete());
const [failedReason, setFailedReason] = useState('');
const [status, setStatus] = useState(dicomFileUploader.getStatus());
console.info(`${dicomFileUploader.getFileId()}`);
const isComplete = useCallback(() => {
return (
status === UploadStatus.Failed ||
@@ -63,7 +54,10 @@ const DicomUploadProgressItem = memo(
switch (dicomFileUploader.getStatus()) {
case UploadStatus.Success:
return (
<Icon name="status-tracked" className="text-primary-light"></Icon>
<Icon
name="status-tracked"
className="text-primary-light"
></Icon>
);
case UploadStatus.InProgress:
return <Icon name="icon-transferring"></Icon>;
@@ -77,27 +71,25 @@ const DicomUploadProgressItem = memo(
};
return (
<div className="flex w-full p-2.5 text-lg min-h-14 items-center border-b border-secondary-light overflow-hidden">
<div className="flex flex-col gap-1 self-top w-0 grow shrink">
<div className="min-h-14 border-secondary-light flex w-full items-center overflow-hidden border-b p-2.5 text-lg">
<div className="self-top flex w-0 shrink grow flex-col gap-1">
<div className="flex gap-4">
<div className="flex w-6 justify-center items-center shrink-0">
{getStatusIcon()}
</div>
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
<div className="flex w-6 shrink-0 items-center justify-center">{getStatusIcon()}</div>
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
{dicomFileUploader.getFileName()}
</div>
</div>
{failedReason && <div className="pl-10">{failedReason}</div>}
</div>
<div className="w-24 flex items-center">
<div className="flex w-24 items-center">
{!isComplete() && (
<>
{dicomFileUploader.getStatus() === UploadStatus.InProgress && (
<div className="w-10 text-right">{percentComplete}%</div>
)}
<div className="flex cursor-pointer ml-auto">
<div className="ml-auto flex cursor-pointer">
<Icon
className="self-center text-primary-active"
className="text-primary-active self-center"
name="close"
onClick={cancelUpload}
/>
@@ -4,9 +4,8 @@ const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'Multi-Planar Reconstruction',
locked: true,
hasUpdatedPriorsInformation: false,
createdDate: '2021-02-23',
modifiedDate: '2023-04-03',
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
// Unknown number of priors referenced - so just match any study
@@ -89,6 +88,7 @@ const mpr: Types.HangingProtocol.Protocol = {
viewports: [
{
viewportOptions: {
viewportId: 'mpr-axial',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
@@ -112,6 +112,7 @@ const mpr: Types.HangingProtocol.Protocol = {
},
{
viewportOptions: {
viewportId: 'mpr-sagittal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
@@ -135,6 +136,7 @@ const mpr: Types.HangingProtocol.Protocol = {
},
{
viewportOptions: {
viewportId: 'mpr-coronal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
@@ -164,7 +166,6 @@ const mpr: Types.HangingProtocol.Protocol = {
const mprAnd3DVolumeViewport = {
id: 'mprAnd3DVolumeViewport',
locked: true,
hasUpdatedPriorsInformation: false,
name: 'mpr',
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
+8 -9
View File
@@ -24,7 +24,6 @@ import { toolNames } from './initCornerstoneTools';
import { getEnabledElement, reset as enabledElementReset } from './state';
import dicomLoaderService from './utils/dicomLoaderService';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
import { id } from './id';
import * as csWADOImageLoader from './initWADOImageLoader.js';
@@ -32,9 +31,7 @@ import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
const Component = React.lazy(() => {
return import(
/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport'
);
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
});
const OHIFCornerstoneViewport = props => {
@@ -62,7 +59,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
imageRetrievalPoolManager.clearRequestStack(type);
});
csWADOImageLoader.destroy();
enabledElementReset();
},
@@ -71,9 +67,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
*
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
*/
preRegistration: function (
props: Types.Extensions.ExtensionParams
): Promise<void> {
preRegistration: function (props: Types.Extensions.ExtensionParams): Promise<void> {
const { servicesManager } = props;
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
servicesManager.registerService(ToolGroupService.REGISTRATION);
@@ -141,5 +135,10 @@ const cornerstoneExtension: Types.Extensions.Extension = {
};
export type { PublicViewportOptions };
export { measurementMappingUtils, CornerstoneExtensionTypes, toolNames , getActiveViewportEnabledElement};
export {
measurementMappingUtils,
CornerstoneExtensionTypes,
toolNames,
getActiveViewportEnabledElement,
};
export default cornerstoneExtension;
+39 -92
View File
@@ -26,6 +26,7 @@ import nthLoader from './utils/nthLoader';
import interleaveTopToBottom from './utils/interleaveTopToBottom';
import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick';
import { CornerstoneServices } from './types';
// TODO: Cypress tests are currently grabbing this from the window?
window.cornerstone = cornerstone;
@@ -48,31 +49,23 @@ export default async function init({
...cornerstone.getConfiguration(),
rendering: {
...cornerstone.getConfiguration().rendering,
strictZSpacingForVolumeViewport:
appConfig.strictZSpacingForVolumeViewport,
strictZSpacingForVolumeViewport: appConfig.strictZSpacingForVolumeViewport,
},
});
// For debugging large datasets
const MAX_CACHE_SIZE_1GB = 1073741824;
const maxCacheSize = appConfig.maxCacheSize;
cornerstone.cache.setMaxCacheSize(
maxCacheSize ? maxCacheSize : MAX_CACHE_SIZE_1GB
);
// For debugging large datasets, otherwise prefer the defaults
const { maxCacheSize } = appConfig;
if (maxCacheSize) {
cornerstone.cache.setMaxCacheSize(maxCacheSize);
}
initCornerstoneTools();
Settings.getRuntimeSettings().set(
'useCursors',
Boolean(appConfig.useCursors)
);
Settings.getRuntimeSettings().set('useCursors', Boolean(appConfig.useCursors));
const {
userAuthenticationService,
measurementService,
customizationService,
displaySetService,
uiDialogService,
uiModalService,
uiNotificationService,
cineService,
@@ -81,28 +74,21 @@ export default async function init({
toolGroupService,
viewportGridService,
stateSyncService,
} = servicesManager.services;
} = servicesManager.services as CornerstoneServices;
window.services = servicesManager.services;
window.extensionManager = extensionManager;
window.commandsManager = commandsManager;
if (
appConfig.showWarningMessageForCrossOrigin &&
!window.crossOriginIsolated
) {
if (appConfig.showWarningMessageForCrossOrigin && !window.crossOriginIsolated) {
uiNotificationService.show({
title: 'Cross Origin Isolation',
message:
'Cross Origin Isolation is not enabled, volume rendering will not work (e.g., MPR)',
message: 'Cross Origin Isolation is not enabled, volume rendering will not work (e.g., MPR)',
type: 'warning',
});
}
if (
appConfig.showCPUFallbackMessage &&
cornerstone.getShouldUseCPURendering()
) {
if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) {
_showCPURenderingModal(uiModalService, hangingProtocolService);
}
@@ -122,18 +108,14 @@ export default async function init({
clearOnModeExit: true,
});
const labelmapRepresentation =
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(
labelmapRepresentation,
{
fillAlpha: 0.3,
fillAlphaInactive: 0.2,
outlineOpacity: 1,
outlineOpacityInactive: 0.65,
}
);
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(labelmapRepresentation, {
fillAlpha: 0.3,
fillAlphaInactive: 0.2,
outlineOpacity: 1,
outlineOpacityInactive: 0.65,
});
const metadataProvider = OHIF.classes.MetadataProvider;
@@ -142,14 +124,8 @@ export default async function init({
cornerstoneStreamingImageVolumeLoader
);
hangingProtocolService.registerImageLoadStrategy(
'interleaveCenter',
interleaveCenterLoader
);
hangingProtocolService.registerImageLoadStrategy(
'interleaveTopToBottom',
interleaveTopToBottom
);
hangingProtocolService.registerImageLoadStrategy('interleaveCenter', interleaveCenterLoader);
hangingProtocolService.registerImageLoadStrategy('interleaveTopToBottom', interleaveTopToBottom);
hangingProtocolService.registerImageLoadStrategy('nth', nthLoader);
// add metadata providers
@@ -166,12 +142,10 @@ export default async function init({
prefetch: appConfig?.maxNumRequests?.prefetch || 10,
};
initWADOImageLoader(userAuthenticationService, appConfig);
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
/* Measurement Service */
this.measurementServiceSource = connectToolsToMeasurementService(
servicesManager
);
this.measurementServiceSource = connectToolsToMeasurementService(servicesManager);
initCineService(cineService);
@@ -181,31 +155,18 @@ export default async function init({
volumeInputArrayMap => {
for (const entry of volumeInputArrayMap.entries()) {
const [viewportId, volumeInputArray] = entry;
const viewport = cornerstoneViewportService.getCornerstoneViewport(
viewportId
);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const ohifViewport = cornerstoneViewportService.getViewportInfo(
viewportId
);
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const { presentationIds } = ohifViewport.getViewportOptions();
const presentations = {
positionPresentation:
positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation:
lutPresentationStore[presentationIds?.lutPresentationId],
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
};
cornerstoneViewportService.setVolumesForViewport(
viewport,
volumeInputArray,
presentations
);
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
}
}
);
@@ -228,9 +189,7 @@ export default async function init({
const resetCrosshairs = evt => {
const { element } = evt.detail;
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(
element
);
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(element);
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(
viewportId,
@@ -256,10 +215,7 @@ export default async function init({
const { element } = evt.detail;
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
eventTarget.addEventListener(
EVENTS.STACK_VIEWPORT_NEW_STACK,
newStackCallback
);
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, newStackCallback);
}
function elementDisabledHandler(evt) {
@@ -274,20 +230,13 @@ export default async function init({
// );
}
eventTarget.addEventListener(
EVENTS.ELEMENT_ENABLED,
elementEnabledHandler.bind(null)
);
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
eventTarget.addEventListener(
EVENTS.ELEMENT_DISABLED,
elementDisabledHandler.bind(null)
);
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
({ viewportIndex, viewportId }) => {
viewportId = viewportId || `viewport-${viewportIndex}`;
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
({ viewportId }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
@@ -296,8 +245,7 @@ export default async function init({
// check if reference lines are active
const referenceLinesEnabled =
toolGroup._toolInstances['ReferenceLines'].mode ===
Enums.ToolModes.Enabled;
toolGroup._toolInstances['ReferenceLines'].mode === Enums.ToolModes.Enabled;
if (!referenceLinesEnabled) {
return;
@@ -322,10 +270,9 @@ function CPUModal() {
return (
<div>
<p>
Your computer does not have enough GPU power to support the default GPU
rendering mode. OHIF has switched to CPU rendering mode. Please note
that CPU rendering does not support all features such as Volume
Rendering, Multiplanar Reconstruction, and Segmentation Overlays.
Your computer does not have enough GPU power to support the default GPU rendering mode. OHIF
has switched to CPU rendering mode. Please note that CPU rendering does not support all
features such as Volume Rendering, Multiplanar Reconstruction, and Segmentation Overlays.
</p>
</div>
);
+18 -25
View File
@@ -35,9 +35,15 @@ const DEFAULT_CONTEXT_MENU_CLICKS = {
function getEventName(evt) {
const button = evt.detail.event.which;
const nameArr = [];
if (evt.detail.event.altKey) nameArr.push('alt');
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
if (evt.detail.event.shiftKey) nameArr.push('shift');
if (evt.detail.event.altKey) {
nameArr.push('alt');
}
if (evt.detail.event.ctrlKey) {
nameArr.push('ctrl');
}
if (evt.detail.event.shiftKey) {
nameArr.push('shift');
}
nameArr.push('button');
nameArr.push(button);
return nameArr.join('');
@@ -54,10 +60,8 @@ function initContextMenu({
*/
const cornerstoneViewportHandleEvent = (name, evt) => {
const customizations =
customizationService.get('cornerstoneViewportClickCommands') ||
DEFAULT_CONTEXT_MENU_CLICKS;
customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_CONTEXT_MENU_CLICKS;
const toRun = customizations[name];
console.log('initContextMenu::cornerstoneViewportHandleEvent', name, toRun);
const options = {
nearbyToolData: findNearbyToolData(commandsManager, evt),
event: evt,
@@ -73,35 +77,24 @@ function initContextMenu({
function elementEnabledHandler(evt) {
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (!viewportInfo) return;
const viewportIndex = viewportInfo.getViewportIndex();
if (!viewportInfo) {
return;
}
// TODO check update upstream
setEnabledElement(viewportIndex, element);
setEnabledElement(viewportId, element);
element.addEventListener(
cs3DToolsEvents.MOUSE_CLICK,
cornerstoneViewportHandleClick
);
element.addEventListener(cs3DToolsEvents.MOUSE_CLICK, cornerstoneViewportHandleClick);
}
function elementDisabledHandler(evt) {
const { element } = evt.detail;
element.removeEventListener(
cs3DToolsEvents.MOUSE_CLICK,
cornerstoneViewportHandleClick
);
element.removeEventListener(cs3DToolsEvents.MOUSE_CLICK, cornerstoneViewportHandleClick);
}
eventTarget.addEventListener(
EVENTS.ELEMENT_ENABLED,
elementEnabledHandler.bind(null)
);
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
eventTarget.addEventListener(
EVENTS.ELEMENT_DISABLED,
elementDisabledHandler.bind(null)
);
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
}
export default initContextMenu;
@@ -28,6 +28,7 @@ import {
} from '@cornerstonejs/tools';
import CalibrationLineTool from './tools/CalibrationLineTool';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
export default function initCornerstoneTools(configuration = {}) {
CrosshairsTool.isAnnotation = false;
@@ -58,6 +59,7 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(ReferenceLinesTool);
addTool(CalibrationLineTool);
addTool(TrackballRotateTool);
addTool(ImageOverlayViewerTool);
// Modify annotation tools to use dashed lines on SR
const annotationStyle = {
@@ -99,6 +101,7 @@ const toolNames = {
ReferenceLines: ReferenceLinesTool.toolName,
CalibrationLine: CalibrationLineTool.toolName,
TrackballRotateTool: TrackballRotateTool.toolName,
ImageOverlayViewer: ImageOverlayViewerTool.toolName,
};
export { toolNames };
+13 -17
View File
@@ -21,9 +21,15 @@ const DEFAULT_DOUBLE_CLICK = {
*/
function getDoubleClickEventName(evt: CustomEvent) {
const nameArr = [];
if (evt.detail.event.altKey) nameArr.push('alt');
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
if (evt.detail.event.shiftKey) nameArr.push('shift');
if (evt.detail.event.altKey) {
nameArr.push('alt');
}
if (evt.detail.event.ctrlKey) {
nameArr.push('ctrl');
}
if (evt.detail.event.shiftKey) {
nameArr.push('shift');
}
nameArr.push('doubleClick');
return nameArr.join('');
}
@@ -33,10 +39,7 @@ export type initDoubleClickArgs = {
commandsManager: CommandsManager;
};
function initDoubleClick({
customizationService,
commandsManager,
}: initDoubleClickArgs): void {
function initDoubleClick({ customizationService, commandsManager }: initDoubleClickArgs): void {
const cornerstoneViewportHandleDoubleClick = (evt: CustomEvent) => {
// Do not allow double click on a tool.
const nearbyToolData = findNearbyToolData(commandsManager, evt);
@@ -48,8 +51,7 @@ function initDoubleClick({
// Allows for the customization of the double click on a viewport.
const customizations =
customizationService.get('cornerstoneViewportClickCommands') ||
DEFAULT_DOUBLE_CLICK;
customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_DOUBLE_CLICK;
const toRun = customizations[eventName];
@@ -78,15 +80,9 @@ function initDoubleClick({
);
}
eventTarget.addEventListener(
EVENTS.ELEMENT_ENABLED,
elementEnabledHandler.bind(null)
);
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
eventTarget.addEventListener(
EVENTS.ELEMENT_DISABLED,
elementDisabledHandler.bind(null)
);
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
}
export default initDoubleClick;
@@ -127,11 +127,8 @@ const initMeasurementService = (
};
const connectToolsToMeasurementService = servicesManager => {
const {
measurementService,
displaySetService,
cornerstoneViewportService,
} = servicesManager.services;
const { measurementService, displaySetService, cornerstoneViewportService } =
servicesManager.services;
const csTools3DVer1MeasurementSource = initMeasurementService(
measurementService,
displaySetService,
@@ -153,10 +150,7 @@ const connectToolsToMeasurementService = servicesManager => {
} = annotationAddedEventDetail;
const { toolName } = metadata;
if (
csToolsEvent.type === completedEvt &&
toolName === toolNames.CalibrationLine
) {
if (csToolsEvent.type === completedEvt && toolName === toolNames.CalibrationLine) {
// show modal to input the measurement (mm)
onCompletedCalibrationLine(servicesManager, csToolsEvent)
.then(
@@ -212,10 +206,8 @@ const connectToolsToMeasurementService = servicesManager => {
try {
const annotationSelectionEventDetail = csToolsEvent.detail;
const {
added: addedSelectedAnnotationUIDs,
removed: removedSelectedAnnotationUIDs,
} = annotationSelectionEventDetail;
const { added: addedSelectedAnnotationUIDs, removed: removedSelectedAnnotationUIDs } =
annotationSelectionEventDetail;
if (removedSelectedAnnotationUIDs) {
removedSelectedAnnotationUIDs.forEach(annotationUID =>
@@ -283,12 +275,8 @@ const connectMeasurementServiceToTools = (
cornerstoneViewportService,
measurementSource
) => {
const {
MEASUREMENT_REMOVED,
MEASUREMENTS_CLEARED,
MEASUREMENT_UPDATED,
RAW_MEASUREMENT_ADDED,
} = measurementService.EVENTS;
const { MEASUREMENT_REMOVED, MEASUREMENTS_CLEARED, MEASUREMENT_UPDATED, RAW_MEASUREMENT_ADDED } =
measurementService.EVENTS;
const csTools3DVer1MeasurementSource = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
@@ -351,11 +339,7 @@ const connectMeasurementServiceToTools = (
return;
}
const {
referenceSeriesUID,
referenceStudyUID,
SOPInstanceUID,
} = measurement;
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID } = measurement;
const instance = DicomMetadataStore.getInstance(
referenceStudyUID,
@@ -368,9 +352,7 @@ const connectMeasurementServiceToTools = (
if (measurement?.metadata?.referencedImageId) {
imageId = measurement.metadata.referencedImageId;
frameNumber = getSOPInstanceAttributes(
measurement.metadata.referencedImageId
).frameNumber;
frameNumber = getSOPInstanceAttributes(measurement.metadata.referencedImageId).frameNumber;
} else {
imageId = dataSource.getImageIdsForInstance({ instance });
}
@@ -1,11 +1,9 @@
import * as cornerstone from '@cornerstonejs/core';
import { volumeLoader } from '@cornerstonejs/core';
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
import dicomImageLoader, {
webWorkerManager,
} from '@cornerstonejs/dicom-image-loader';
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
import dicomParser from 'dicom-parser';
import { errorHandler } from '@ohif/core';
import { errorHandler, utils } from '@ohif/core';
const { registerVolumeLoader } = volumeLoader;
@@ -35,15 +33,13 @@ function initWebWorkers(appConfig) {
export default function initWADOImageLoader(
userAuthenticationService,
appConfig
appConfig,
extensionManager
) {
dicomImageLoader.external.cornerstone = cornerstone;
dicomImageLoader.external.dicomParser = dicomParser;
registerVolumeLoader(
'cornerstoneStreamingImageVolume',
cornerstoneStreamingImageVolumeLoader
);
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
dicomImageLoader.configure({
decodeConfig: {
@@ -54,19 +50,18 @@ export default function initWADOImageLoader(
// we should set this flag to false.
convertFloatPixelDataToInt: false,
},
beforeSend: function(xhr) {
beforeSend: function (xhr) {
//TODO should be removed in the future and request emitted by DicomWebDataSource
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
const headers = userAuthenticationService.getAuthorizationHeader();
const acceptHeader = utils.generateAcceptHeader(
sourceConfig.acceptHeader,
sourceConfig.requestTransferSyntaxUID,
sourceConfig.omitQuotationForMultipartRequest
);
// Request:
// JPEG-LS Lossless (1.2.840.10008.1.2.4.80) if available, otherwise accept
// whatever transfer-syntax the origin server provides.
// For now we use image/jls and image/x-jls because some servers still use the old type
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
const xhrRequestHeaders = {
Accept: appConfig.omitQuotationForMultipartRequest
? 'multipart/related; type=application/octet-stream'
: 'multipart/related; type="application/octet-stream"',
// 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*',
Accept: acceptHeader,
};
if (headers) {
@@ -1,16 +1,8 @@
import { ServicesManager, Types } from '@ohif/core';
import {
cache as cs3DCache,
Enums,
volumeLoader,
utilities as utils,
} from '@cornerstonejs/core';
import { cache as cs3DCache, Enums, volumeLoader, utilities as utils } from '@cornerstonejs/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import {
StackViewportData,
VolumeViewportData,
} from '../../types/CornerstoneCacheService';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
@@ -18,9 +10,7 @@ class CornerstoneCacheService {
static REGISTRATION = {
name: 'cornerstoneCacheService',
altName: 'CornerstoneCacheService',
create: ({
servicesManager,
}: Types.Extensions.ExtensionParams): CornerstoneCacheService => {
create: ({ servicesManager }: Types.Extensions.ExtensionParams): CornerstoneCacheService => {
return new CornerstoneCacheService(servicesManager);
},
};
@@ -77,11 +67,7 @@ class CornerstoneCacheService {
cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC ||
cs3DViewportType === Enums.ViewportType.VOLUME_3D
) {
viewportData = await this._getVolumeViewportData(
dataSource,
displaySets,
cs3DViewportType
);
viewportData = await this._getVolumeViewportData(dataSource, displaySets, cs3DViewportType);
}
viewportData.viewportType = cs3DViewportType;
@@ -134,20 +120,14 @@ class CornerstoneCacheService {
// For Stack Viewport we don't have fusion currently
const displaySet = displaySets[0];
let stackImageIds = this.stackImageIds.get(
displaySet.displaySetInstanceUID
);
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
if (!stackImageIds) {
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
const {
displaySetInstanceUID,
StudyInstanceUID,
isCompositeStack,
} = displaySet;
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet;
const StackViewportData: StackViewportData = {
viewportType,
@@ -196,31 +176,22 @@ class CornerstoneCacheService {
continue;
}
const volumeLoaderSchema =
displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
const volumeLoaderSchema = displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
const volumeId = `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
let volumeImageIds = this.volumeImageIds.get(
displaySet.displaySetInstanceUID
);
let volumeImageIds = this.volumeImageIds.get(displaySet.displaySetInstanceUID);
let volume = cs3DCache.getVolume(volumeId);
if (!volumeImageIds || !volume) {
volumeImageIds = this._getCornerstoneVolumeImageIds(
displaySet,
dataSource
);
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds: volumeImageIds,
});
this.volumeImageIds.set(
displaySet.displaySetInstanceUID,
volumeImageIds
);
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
}
volumeData.push({
@@ -268,10 +239,7 @@ class CornerstoneCacheService {
}
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
const stackImageIds = this._getCornerstoneStackImageIds(
displaySet,
dataSource
);
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
return stackImageIds;
}
@@ -7,32 +7,27 @@
* @returns An array of object that includes data, id, segmentIndex, color
* and geometry Id
*/
export function mapROIContoursToRTStructData(
structureSet: unknown,
rtDisplaySetUID: unknown
) {
return structureSet.ROIContours.map(
({ contourPoints, ROINumber, ROIName, colorArray }) => {
const data = contourPoints.map(({ points, ...rest }) => {
const newPoints = points.map(({ x, y, z }) => {
return [x, y, z];
});
return {
...rest,
points: newPoints,
};
export function mapROIContoursToRTStructData(structureSet: unknown, rtDisplaySetUID: unknown) {
return structureSet.ROIContours.map(({ contourPoints, ROINumber, ROIName, colorArray }) => {
const data = contourPoints.map(({ points, ...rest }) => {
const newPoints = points.map(({ x, y, z }) => {
return [x, y, z];
});
const id = ROIName || ROINumber;
return {
data,
id,
segmentIndex: ROINumber,
color: colorArray,
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
...rest,
points: newPoints,
};
}
);
});
const id = ROIName || ROINumber;
return {
data,
id,
segmentIndex: ROINumber,
color: colorArray,
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
};
});
}
File diff suppressed because it is too large. Load diff
@@ -1,8 +1,4 @@
import {
synchronizers,
SynchronizerManager,
Synchronizer,
} from '@cornerstonejs/tools';
import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
import { pubSubServiceInterface, Types, ServicesManager } from '@ohif/core';
@@ -14,10 +10,7 @@ const EVENTS = {
* @params options - are an optional set of options associated with the first
* sync group declared.
*/
export type SyncCreator = (
type: string,
options?: Record<string, unknown>
) => Synchronizer;
export type SyncCreator = (id: string, options?: Record<string, unknown>) => Synchronizer;
export type SyncGroup = {
type: string;
@@ -40,9 +33,7 @@ export default class SyncGroupService {
static REGISTRATION = {
name: 'syncGroupService',
altName: 'SyncGroupService',
create: ({
servicesManager,
}: Types.Extensions.ExtensionParams): SyncGroupService => {
create: ({ servicesManager }: Types.Extensions.ExtensionParams): SyncGroupService => {
return new SyncGroupService(servicesManager);
},
};
@@ -65,11 +56,7 @@ export default class SyncGroupService {
Object.assign(this, pubSubServiceInterface);
}
private _createSynchronizer(
type: string,
id: string,
options
): Synchronizer | undefined {
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
if (syncCreator) {
return syncCreator(id, options);
@@ -83,7 +70,7 @@ export default class SyncGroupService {
* @param type is the type of the synchronizer to create
* @param creator
*/
public setSynchronizer(type: string, creator: SyncCreator): void {
public addSynchronizerType(type: string, creator: SyncCreator): void {
this.synchronizerCreators[type.toLowerCase()] = creator;
}
@@ -109,19 +96,11 @@ export default class SyncGroupService {
return;
}
const syncGroupsArray = Array.isArray(syncGroups)
? syncGroups
: [syncGroups];
const syncGroupsArray = Array.isArray(syncGroups) ? syncGroups : [syncGroups];
syncGroupsArray.forEach(syncGroup => {
const syncGroupObj = asSyncGroup(syncGroup);
const {
type,
target = true,
source = true,
options = {},
id = type,
} = syncGroupObj;
const { type, target = true, source = true, options = {}, id = type } = syncGroupObj;
const synchronizer = this._getOrCreateSynchronizer(type, id, options);
synchronizer.setOptions(viewportId, options);
@@ -24,9 +24,7 @@ export default class ToolGroupService {
public static REGISTRATION = {
name: 'toolGroupService',
altName: 'ToolGroupService',
create: ({
servicesManager,
}: OhifTypes.Extensions.ExtensionParams): ToolGroupService => {
create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams): ToolGroupService => {
return new ToolGroupService(servicesManager);
},
};
@@ -40,10 +38,7 @@ export default class ToolGroupService {
EVENTS: { [key: string]: string };
constructor(serviceManager) {
const {
cornerstoneViewportService,
viewportGridService,
} = serviceManager.services;
const { cornerstoneViewportService, viewportGridService } = serviceManager.services;
this.cornerstoneViewportService = cornerstoneViewportService;
this.viewportGridService = viewportGridService;
this.listeners = {};
@@ -66,19 +61,14 @@ export default class ToolGroupService {
if (!toolGroupIdToUse) {
// Use the active viewport's tool group if no tool group id is provided
const enabledElement = getActiveViewportEnabledElement(
this.viewportGridService
);
const enabledElement = getActiveViewportEnabledElement(this.viewportGridService);
if (!enabledElement) {
return;
}
const { renderingEngineId, viewportId } = enabledElement;
const toolGroup = ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);
const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngineId);
if (!toolGroup) {
console.warn(
@@ -103,10 +93,7 @@ export default class ToolGroupService {
public getToolGroupForViewport(viewportId: string): Types.IToolGroup | void {
const renderingEngine = this.cornerstoneViewportService.getRenderingEngine();
return ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngine.id
);
return ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngine.id);
}
public getActiveToolForViewport(viewportId: string): string {
@@ -133,10 +120,7 @@ export default class ToolGroupService {
renderingEngineId: string,
deleteToolGroupIfEmpty?: boolean
): void {
const toolGroup = ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);
const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngineId);
if (!toolGroup) {
return;
@@ -193,11 +177,7 @@ export default class ToolGroupService {
return toolGroup;
}
public addToolsToToolGroup(
toolGroupId: string,
tools: Array<Tool>,
configs: any = {}
): void {
public addToolsToToolGroup(toolGroupId: string, tools: Array<Tool>, configs: any = {}): void {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
// this.changeConfigurationIfNecessary(toolGroup, volumeId);
this._addTools(toolGroup, tools, configs);
Loaded 100 of 1274 files, more files were not shown because too many files have changed in this diff. Show more