Compare commits

..
Author SHA1 Message Date
ohif-bot 6e5892d202 chore(version): Update package versions [skip ci] 2024-04-05 03:10:27 +00:00
ohif-bot 9d8997ad84 chore(version): version.json [skip ci] 2024-04-05 03:10:20 +00:00
Ibrahim cea27d438d feat(advanced-roi-tools): new tools and icon updates and overlay bug fixes (#4014) 2024-04-04 23:00:11 -04:00
ohif-bot 78fd8cce86 chore(version): Update package versions [skip ci] 2024-04-05 02:53:53 +00:00
ohif-bot f7c9a5cb64 chore(version): version.json [skip ci] 2024-04-05 02:53:45 +00:00
nithin-trenser 56b1eae635 feat(measurement): Add support measurement label autocompletion (#3855) 2024-04-04 22:42:48 -04:00
ohif-bot 807afb8ce5 chore(version): Update package versions [skip ci] 2024-04-03 13:04:29 +00:00
ohif-bot 5f494598bb chore(version): version.json [skip ci] 2024-04-03 13:04:21 +00:00
Ibrahim fa38e6a07a fix(presentation-state): Iterate over map properly to restore the presentation state (#4013) 2024-04-03 08:53:29 -04:00
ohif-bot d8f66856cc chore(version): Update package versions [skip ci] 2024-04-03 03:44:14 +00:00
ohif-bot 9c39689451 chore(version): version.json [skip ci] 2024-04-03 03:44:05 +00:00
IbrahimandAlireza 9f3235ff09 feat(segmentation): Enhanced segmentation panel design for TMTV (#3988)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2024-04-02 23:33:04 -04:00
ohif-bot e8e209291c chore(version): Update package versions [skip ci] 2024-04-02 13:46:18 +00:00
ohif-bot bc71861619 chore(version): version.json [skip ci] 2024-04-02 13:46:10 +00:00
IbrahimandJoe Boccanfuso b7f90e3951 feat(ViewportActionMenu): window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering (#3963)
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2024-04-02 09:35:30 -04:00
ohif-bot 07301fc020 chore(version): Update package versions [skip ci] 2024-03-28 20:56:04 +00:00
ohif-bot 1203477dd4 chore(version): version.json [skip ci] 2024-03-28 20:55:57 +00:00
Alireza 158a181670 fix(new layout): address black screen bugs (#4008) 2024-03-28 16:45:11 -04:00
ohif-bot aac3546bed chore(version): Update package versions [skip ci] 2024-03-28 14:52:21 +00:00
ohif-bot 1256a4d565 chore(version): version.json [skip ci] 2024-03-28 14:52:11 +00:00
Ibrahim 617043fe0d feat(layout): new layout selector with 3D volume rendering (#3923) 2024-03-28 10:40:32 -04:00
ohif-bot 6f610b09e1 chore(version): Update package versions [skip ci] 2024-03-27 20:12:57 +00:00
ohif-bot a2e320f9f8 chore(version): version.json [skip ci] 2024-03-27 20:12:47 +00:00
Alireza 566b25a544 feat(toolbar): new Toolbar to enable reactive state synchronization (#3983) 2024-03-27 16:01:32 -04:00
ohif-bot 79d5c36bda chore(version): Update package versions [skip ci] 2024-03-25 15:44:13 +00:00
ohif-bot f47a94317d chore(version): version.json [skip ci] 2024-03-25 15:44:04 +00:00
Ibrahim 45b68e841d feat(worklist): new investigational use text (#3999) 2024-03-25 11:33:24 -04:00
ohif-bot d70aae3233 chore(version): Update package versions [skip ci] 2024-03-19 00:37:56 +00:00
ohif-bot 345b137bec chore(version): version.json [skip ci] 2024-03-19 00:37:48 +00:00
Ibrahim 9bcd1ae6f5 feat(worklist): New worklist buttons and tooltips (#3989) 2024-03-18 20:27:12 -04:00
ohif-bot 6b79dff5c1 chore(version): Update package versions [skip ci] 2024-03-18 18:38:23 +00:00
ohif-bot 88859f75d5 chore(version): version.json [skip ci] 2024-03-18 18:38:16 +00:00
Alireza feed2304c1 fix(SR display): and the token based navigation (#3995) 2024-03-18 14:27:17 -04:00
ohif-bot 08815909c7 chore(version): Update package versions [skip ci] 2024-03-15 13:52:45 +00:00
ohif-bot ed2234a67c chore(version): version.json [skip ci] 2024-03-15 13:52:34 +00:00
Pedro H. Köhler f7fe91c5f6 feat(delete measurement): icon for measurement table (#3775) 2024-03-15 09:35:29 -04:00
ohif-bot 21ec6860f2 chore(version): Update package versions [skip ci] 2024-03-08 13:38:48 +00:00
ohif-bot ac474b7f8f chore(version): version.json [skip ci] 2024-03-08 13:38:41 +00:00
EricB e485d68fd4 fix(cli): mode creation template (#3876) (#3981) 2024-03-08 08:27:37 -05:00
ohif-bot 5089229ff9 chore(version): Update package versions [skip ci] 2024-03-05 16:15:56 +00:00
ohif-bot df09176d63 chore(version): version.json [skip ci] 2024-03-05 16:15:48 +00:00
EricB bb603bd1f8 docs: Typo (#3975) 2024-03-05 11:04:28 -05:00
ohif-bot 82a4a7d377 chore(version): Update package versions [skip ci] 2024-02-28 17:25:27 +00:00
ohif-bot c27fa9c802 chore(version): version.json [skip ci] 2024-02-28 17:25:19 +00:00
EricB 4cdfdae814 fix(docs): Minor typos in hpModule.md (#3962) 2024-02-28 12:13:46 -05:00
ohif-bot 596340803a chore(version): Update package versions [skip ci] 2024-02-22 19:54:20 +00:00
ohif-bot ff4e17f5c2 chore(version): version.json [skip ci] 2024-02-22 19:54:11 +00:00
Bill Wallace 21e8a2bd0b fix(demo): Deploy issue (#3951) 2024-02-22 14:42:46 -05:00
ohif-bot 2a442e30eb chore(version): Update package versions [skip ci] 2024-02-21 18:59:56 +00:00
ohif-bot 8e8a66bd29 chore(version): version.json [skip ci] 2024-02-21 18:59:49 +00:00
Alireza b3a0faf5f5 feat(resize): Optimize resizing process and maintain zoom level (#3889) 2024-02-21 13:49:23 -05:00
ohif-bot a2a0090eeb chore(version): Update package versions [skip ci] 2024-02-14 14:02:25 +00:00
ohif-bot ea552027c5 chore(version): version.json [skip ci] 2024-02-14 14:02:17 +00:00
Ibrahim 0eac386a31 feat(errorboundary): format stack trace properly (#3931) 2024-02-14 08:51:17 -05:00
ohif-bot 40b590d75a chore(version): Update package versions [skip ci] 2024-02-05 19:01:43 +00:00
ohif-bot 01df1763ce chore(version): version.json [skip ci] 2024-02-05 19:01:34 +00:00
Igor Octaviano 5bba98ed84 fix: 🐛 Sort merge results based on default data source (input) (#3903) 2024-02-05 13:46:47 -05:00
ohif-bot 16785936d9 chore(version): Update package versions [skip ci] 2024-01-22 20:52:46 +00:00
ohif-bot 3deb73d642 chore(version): version.json [skip ci] 2024-01-22 20:52:38 +00:00
m00n620 ef46b5a704 fix toggleHpTools to support split primary button (#3900)
Fixes a small bug in toggling buttons with hanging protocols.
2024-01-22 15:42:51 -05:00
ohif-bot ed2bf23cd2 chore(version): Update package versions [skip ci] 2024-01-22 15:07:51 +00:00
ohif-bot 2ff2f3f34d chore(version): version.json [skip ci] 2024-01-22 15:07:43 +00:00
Patrick Wespi a47aeb8bd7 fix: catch errors in getPTImageIdInstanceMetadata (#3897) 2024-01-22 09:58:22 -05:00
ohif-bot 94f40192f5 chore(version): Update package versions [skip ci] 2024-01-22 14:55:27 +00:00
ohif-bot 1c43aa791a chore(version): version.json [skip ci] 2024-01-22 14:55:17 +00:00
Alireza e4a116b074 fix(viewport-sync): remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync (#3849) 2024-01-22 09:44:35 -05:00
ohif-bot 2460d28afc chore(version): Update package versions [skip ci] 2024-01-19 13:30:55 +00:00
ohif-bot 4a958b8989 chore(version): version.json [skip ci] 2024-01-19 13:30:47 +00:00
Pedro H. Köhler 31b837fa90 fix: is same orientaiton (#3905) 2024-01-19 08:21:22 -05:00
ohif-bot 9bb00d71fa chore(version): Update package versions [skip ci] 2024-01-17 16:01:31 +00:00
ohif-bot 40bbe33a9d chore(version): version.json [skip ci] 2024-01-17 16:01:24 +00:00
Igor Octaviano 911d672835 fix: 🐛 Check merge key for merge data source (#3901) 2024-01-17 10:51:49 -05:00
ohif-bot 9c553fded4 chore(version): Update package versions [skip ci] 2024-01-12 16:10:06 +00:00
ohif-bot 144dd39393 chore(version): version.json [skip ci] 2024-01-12 16:09:58 +00:00
AbishekBista9 df2f50748e docs: Update architecture.md (#3891) 2024-01-12 11:00:17 -05:00
ohif-bot 83a1523408 chore(version): Update package versions [skip ci] 2024-01-12 13:58:08 +00:00
ohif-bot 5a62a983e5 chore(version): version.json [skip ci] 2024-01-12 13:57:58 +00:00
Bill Wallace d00a86b022 fix: Update CS3D to fix second render (#3892) 2024-01-12 08:48:15 -05:00
ohif-bot 710a2dca72 chore(version): Update package versions [skip ci] 2024-01-09 19:31:20 +00:00
ohif-bot 9100b1f02e chore(version): version.json [skip ci] 2024-01-09 19:31:10 +00:00
0049f4c030 feat(hp): enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol (#3804)
Co-authored-by: rodrigobasilio2022 <rodrigo.basilio@radicalimaging.com>
Co-authored-by: rodrigobasilio2022 <114958722+rodrigobasilio2022@users.noreply.github.com>
2024-01-09 14:22:00 -05:00
ohif-bot b5cdf88a57 chore(version): Update package versions [skip ci] 2024-01-09 17:45:39 +00:00
ohif-bot c793df727b chore(version): version.json [skip ci] 2024-01-09 17:45:30 +00:00
Alireza 1456a493d6 feat(transferSyntax): prefer server transcoded transfer syntax for all images (#3883) 2024-01-09 12:36:29 -05:00
ohif-bot 7c551f5043 chore(version): Update package versions [skip ci] 2024-01-09 15:24:36 +00:00
ohif-bot 0b2c2a235f chore(version): version.json [skip ci] 2024-01-09 15:24:28 +00:00
Alireza b1efe40aa1 fix(segmentation): upgrade cs3d to fix various segmentation bugs (#3885) 2024-01-09 10:15:56 -05:00
ohif-bot a8a0bdb2df chore(version): Update package versions [skip ci] 2024-01-08 19:34:59 +00:00
ohif-bot 6878c1a264 chore(version): version.json [skip ci] 2024-01-08 19:34:49 +00:00
AbishekBista9 d181eb4fdc docs: Update user-account-control.md (#3877) 2024-01-08 14:24:42 -05:00
ohif-bot fd809e4a0b chore(version): Update package versions [skip ci] 2024-01-08 19:21:13 +00:00
ohif-bot 4c29670d76 chore(version): version.json [skip ci] 2024-01-08 19:21:04 +00:00
Igor Octaviano f58725ce40 feat: Add on mode init hook (#3882) 2024-01-08 14:12:26 -05:00
ohif-bot 5a1cb842f0 chore(version): Update package versions [skip ci] 2024-01-08 19:08:04 +00:00
ohif-bot c5bd79552a chore(version): version.json [skip ci] 2024-01-08 19:07:56 +00:00
Sofien-Sellami 61bf22c6f8 feat(ui): sidePanel expandedWidth (#3728) 2024-01-08 13:59:20 -05:00
ohif-bot 3d98548072 chore(version): Update package versions [skip ci] 2024-01-08 18:53:43 +00:00
ohif-bot bf9770a55c chore(version): version.json [skip ci] 2024-01-08 18:53:35 +00:00
Pedro H. KöhlerandIgor Octaviano 2049c0936c feat: improve disableEditing flag (#3875)
Co-authored-by: Igor Octaviano <igoroctaviano@gmail.com>
2024-01-08 13:45:24 -05:00
ohif-bot 7d5607073d chore(version): Update package versions [skip ci] 2024-01-08 18:24:06 +00:00
ohif-bot 1b6910bf76 chore(version): version.json [skip ci] 2024-01-08 18:23:58 +00:00
Bill WallaceandAlireza 98650302c7 fix: PDF display request in v3 (#3878)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2024-01-08 13:15:54 -05:00
Salim Kanoun bf846c94c3 fix: convert radian to degree value for mip rotation (#3881) 2024-01-08 13:09:22 -05:00
ohif-bot b5dc9047ba chore(version): Update package versions [skip ci] 2024-01-08 17:37:17 +00:00
ohif-bot 636d8b0458 chore(version): version.json [skip ci] 2024-01-08 17:37:08 +00:00
Celian-abd e8858f3eb5 fix: colormap for stack viewports via HangingProtocol (#3866) 2024-01-08 12:28:22 -05:00
ohif-bot eb7c950d7e chore(version): Update package versions [skip ci] 2023-12-15 02:56:03 +00:00
ohif-bot c6214d66b9 chore(version): version.json [skip ci] 2023-12-15 02:55:55 +00:00
Alireza 0eb502aaf0 chore(version): upgrade cornerstone3D versions to fix orientation (#3854) 2023-12-14 21:47:15 -05:00
ohif-bot 44a5a24d4f chore(version): Update package versions [skip ci] 2023-12-14 03:48:22 +00:00
ohif-bot b0bf5dc780 chore(version): version.json [skip ci] 2023-12-14 03:48:15 +00:00
Alireza 5915fada83 chore(version): upgrade cornerstone3D versions (#3853) 2023-12-13 22:39:41 -05:00
ohif-bot 2fa335c6d9 chore(version): Update package versions [skip ci] 2023-12-13 21:37:11 +00:00
ohif-bot de4ad88a09 chore(version): version.json [skip ci] 2023-12-13 21:37:03 +00:00
Sofien-Sellami 6ca13c0a4c fix(icon-style): Ensure consistent icon dimensions (#3727) 2023-12-13 16:28:20 -05:00
Pedro H. Köhler 0177b625ba feat(overlay): add inline binary overlays (#3852) 2023-12-13 16:18:49 -05:00
ohif-bot c4064acfbc chore(version): Update package versions [skip ci] 2023-12-13 20:27:22 +00:00
ohif-bot 7a05d169ef chore(version): version.json [skip ci] 2023-12-13 20:27:14 +00:00
dxlin 13d0c883f8 docs: Update HangingProtocolService.md doc sameAs example to contain constr… (#3765) 2023-12-13 15:18:15 -05:00
ohif-bot c45f60dce2 chore(version): Update package versions [skip ci] 2023-12-13 20:11:48 +00:00
ohif-bot 7bb2713c5c chore(version): version.json [skip ci] 2023-12-13 20:11:39 +00:00
Celian-abdandAlireza 97cb1f94e1 fix(dicom-seg) : Fix the missing color tag when loading Dicom-Seg (#3822)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2023-12-13 15:03:09 -05:00
ohif-bot f99645f3a1 chore(version): Update package versions [skip ci] 2023-12-13 19:55:36 +00:00
ohif-bot ae16c7f972 chore(version): version.json [skip ci] 2023-12-13 19:55:28 +00:00
Salim Kanoun 9c6e1cd305 Fix(HPService): custom image load performed is now reset to false on HP exit (#3809) 2023-12-13 14:47:12 -05:00
ohif-bot 7f0ca857fa chore(version): Update package versions [skip ci] 2023-12-13 19:29:55 +00:00
ohif-bot 920256d3a4 chore(version): version.json [skip ci] 2023-12-13 19:29:48 +00:00
Pedro H. Köhler e1f55e65f2 feat(customizationService): Enable saving and loading of private tags in SRs (#3842) 2023-12-13 14:21:46 -05:00
ohif-bot d1c07d90f0 chore(version): Update package versions [skip ci] 2023-12-13 15:29:16 +00:00
ohif-bot b1a04ecac3 chore(version): version.json [skip ci] 2023-12-13 15:29:09 +00:00
Sofien-Sellami f707b4ebc9 feat(config): Add activateViewportBeforeInteraction parameter for viewport interaction customization (#3847) 2023-12-13 10:17:48 -05:00
Pedro H. Köhler 805c53270f fix: address and improve system vulnerabilities (#3851) 2023-12-13 09:25:41 -05:00
Pavel d14a8f0199 feat(i18n): enhanced i18n support (#3761) 2023-12-11 09:43:58 -05:00
ohif-bot 5a5c4f58af chore(version): Update package versions [skip ci] 2023-12-08 14:35:30 +00:00
ohif-bot 6e5d8b691e chore(version): version.json [skip ci] 2023-12-08 14:35:23 +00:00
Joe Boccanfuso 59576d695d feat(HP): Added new 3D hanging protocols to be used in the new layout selector (#3844) 2023-12-08 09:25:57 -05:00
ohif-bot 78d4bea04d chore(version): Update package versions [skip ci] 2023-12-06 13:37:37 +00:00
ohif-bot 806c2f4b36 chore(version): version.json [skip ci] 2023-12-06 13:37:29 +00:00
Alireza 6651008fbb fix(auth): fix the issue with oauth at a non root path (#3840) 2023-12-06 08:28:02 -05:00
ohif-bot 74c98b6e9f chore(version): Update package versions [skip ci] 2023-11-28 18:18:13 +00:00
ohif-bot ecee25b1d5 chore(version): version.json [skip ci] 2023-11-28 18:18:03 +00:00
Alireza f1a67647ae fix(SM): drag and drop is now fixed for SM (#3813) 2023-11-28 13:04:30 -05:00
ohif-bot 4f9a00f872 chore(version): Update package versions [skip ci] 2023-11-27 13:36:36 +00:00
ohif-bot 7673a887e4 chore(version): version.json [skip ci] 2023-11-27 13:36:29 +00:00
Joe Boccanfuso 924affa7b5 fix(cine): Set cine disabled on mode exit. (#3812) 2023-11-27 08:27:38 -05:00
ohif-bot abac6f583e chore(version): Update package versions [skip ci] 2023-11-24 22:41:51 +00:00
ohif-bot 36ba20d6e8 chore(version): version.json [skip ci] 2023-11-24 22:41:42 +00:00
Bill Wallace 9d1884d7d8 fix: Update the CS3D packages to add the most recent HTJ2K TSUIDS (#3806) 2023-11-24 17:32:19 -05:00
ohif-bot 622f18bd3e chore(version): Update package versions [skip ci] 2023-11-24 13:56:33 +00:00
ohif-bot 8b6256e76f chore(version): version.json [skip ci] 2023-11-24 13:56:24 +00:00
Igor Octaviano c4ff2c2f09 feat: Merge Data Source (#3788)
Add the ability to merge two different series queries to generate a complete study query result.  Provides basic support for other types of merges, but those aren't yet added as full features.
2023-11-24 08:46:54 -05:00
ohif-bot 1be5613946 chore(version): Update package versions [skip ci] 2023-11-21 17:47:19 +00:00
ohif-bot 6edb1c1e4c chore(version): version.json [skip ci] 2023-11-21 17:47:12 +00:00
Pedro H. Köhler 404b0a5d53 feat(events): broadcast series summary metadata (#3798) 2023-11-21 12:38:08 -05:00
ohif-bot 786898fd63 chore(version): Update package versions [skip ci] 2023-11-21 14:15:45 +00:00
ohif-bot dfa851600c chore(version): version.json [skip ci] 2023-11-21 14:15:37 +00:00
Bill Wallace 00e751933a fix(DICOM Overlay): The overlay data wasn't being refreshed on change (#3793) 2023-11-21 09:05:25 -05:00
ohif-bot c4e22c251f chore(version): Update package versions [skip ci] 2023-11-21 02:33:40 +00:00
ohif-bot 3cf4b6dea5 chore(version): version.json [skip ci] 2023-11-21 02:33:32 +00:00
Bill Wallace 8c8924af37 fix(metadata): to handle cornerstone3D update for htj2k (#3783) 2023-11-20 21:23:42 -05:00
ohif-bot 04121e3713 chore(version): Update package versions [skip ci] 2023-11-18 01:18:27 +00:00
ohif-bot d1eef6dbdb chore(version): version.json [skip ci] 2023-11-18 01:18:18 +00:00
Joe Boccanfuso d83beb7c62 feat(docs): Added various training videos to support the OHIF CLI tools (#3794) 2023-11-17 20:08:38 -05:00
ohif-bot 7867b29742 chore(version): Update package versions [skip ci] 2023-11-15 14:46:52 +00:00
ohif-bot 4a6f1bd407 chore(version): version.json [skip ci] 2023-11-15 14:46:44 +00:00
Igor Octaviano b694228dd5 feat(url): Add SeriesInstanceUIDs wado query param (#3746) 2023-11-15 09:37:09 -05:00
ohif-bot 013068bcb0 chore(version): Update package versions [skip ci] 2023-11-13 15:52:59 +00:00
ohif-bot 3fbc23b04c chore(version): version.json [skip ci] 2023-11-13 15:52:50 +00:00
Igor Octaviano 323401418e fix: 🐛 Run error handler for failed image requests (#3773) 2023-11-13 10:42:55 -05:00
ohif-bot fc438e90ae chore(version): Update package versions [skip ci] 2023-11-13 14:00:34 +00:00
ohif-bot 9b7f38467b chore(version): version.json [skip ci] 2023-11-13 14:00:27 +00:00
Bill Wallace fd1251f751 fix(overlay): Overlays aren't shown on undefined origin (#3781) 2023-11-13 08:50:58 -05:00
ohif-bot 14793493a4 chore(version): Update package versions [skip ci] 2023-11-10 22:00:19 +00:00
ohif-bot 73d8848707 chore(version): version.json [skip ci] 2023-11-10 22:00:11 +00:00
Alireza 43b1c17209 feat(dicomJSON): Add Loading Other Display Sets and JSON Metadata Generation script (#3777) 2023-11-10 16:51:23 -05:00
ohif-bot 5b513fd30e chore(version): Update package versions [skip ci] 2023-11-10 14:07:30 +00:00
ohif-bot 26c9567c38 chore(version): version.json [skip ci] 2023-11-10 14:07:23 +00:00
Pedro H. Köhler 8bbcd0e692 fix(path): upgrade docusaurus for security (#3780) 2023-11-10 08:54:40 -05:00
ohif-bot 8c2f495405 chore(version): Update package versions [skip ci] 2023-11-09 04:13:50 +00:00
ohif-bot e1e1e60b7d chore(version): version.json [skip ci] 2023-11-09 04:13:43 +00:00
edwardyangxin 8af1046803 fix(arrow): ArrowAnnotate text key cause validation error (#3771) 2023-11-08 23:04:55 -05:00
ohif-bot 4bbba871f5 chore(version): Update package versions [skip ci] 2023-11-08 21:09:51 +00:00
ohif-bot 73e804c311 chore(version): version.json [skip ci] 2023-11-08 21:09:42 +00:00
Pedro H. Köhler 442f99d5eb feat: add VolumeViewport rotation (#3776) 2023-11-08 16:00:18 -05:00
ohif-bot 9575a6f54c chore(version): Update package versions [skip ci] 2023-11-08 14:28:55 +00:00
ohif-bot dc345d1875 chore(version): version.json [skip ci] 2023-11-08 14:28:48 +00:00
AlirezaandOuwen Huang bf252bcec2 feat(hp callback): Add viewport ready callback (#3772)
Co-authored-by: Ouwen Huang <ouwen.huang@duke.edu>
2023-11-08 09:19:49 -05:00
ohif-bot 1a68c06c3b chore(version): Update package versions [skip ci] 2023-11-03 16:29:50 +00:00
ohif-bot a2361fa615 chore(version): version.json [skip ci] 2023-11-03 16:29:42 +00:00
Alireza ee67bcc0f5 docs(faq): FAQ and Segmentation Mode Fix for some studies (#3762) 2023-11-03 12:19:24 -04:00
ohif-bot f9f16bc69f chore(version): Update package versions [skip ci] 2023-11-02 16:17:30 +00:00
ohif-bot a29f5bc9d3 chore(version): version.json [skip ci] 2023-11-02 16:17:20 +00:00
Joe Boccanfuso b23eeff937 fix(thumbnail): Avoid multiple promise creations for thumbnails (#3756) 2023-11-02 12:07:52 -04:00
ohif-bot 09ff293f5f chore(version): Update package versions [skip ci] 2023-10-31 19:49:41 +00:00
ohif-bot 71a30dc9f2 chore(version): version.json [skip ci] 2023-10-31 19:49:31 +00:00
Pavel 330e11c7ff feat(i18n): enhanced i18n support (#3730) 2023-10-31 15:36:45 -04:00
ohif-bot c73a403cdc chore(version): Update package versions [skip ci] 2023-10-30 15:01:05 +00:00
ohif-bot 5b3b6eed16 chore(version): version.json [skip ci] 2023-10-30 15:00:58 +00:00
Joe Boccanfusoandladeirarodolfo 2a15ef0e44 feat(filters): save worklist query filters to session storage so that they persist between navigation to the viewer and back (#3749)
Co-authored-by: ladeirarodolfo <39910206+ladeirarodolfo@users.noreply.github.com>
2023-10-30 10:47:43 -04:00
Joe Boccanfuso db395852b6 fix(measurement service): Implemented correct check of schema keys in _isValidMeasurment. (#3750) 2023-10-30 10:44:21 -04:00
ohif-bot 423ba7c269 chore(version): Update package versions [skip ci] 2023-10-25 17:17:55 +00:00
ohif-bot cafba997bf chore(version): version.json [skip ci] 2023-10-25 17:17:47 +00:00
Bill WallaceandJoe Boccanfuso dd6d9768bb fix(toolbar): allow customizable toolbar for active viewport and allow active tool to be deactivated via a click (#3608)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2023-10-25 13:08:12 -04:00
ohif-bot 2145b42920 chore(version): Update package versions [skip ci] 2023-10-24 17:20:41 +00:00
ohif-bot a5ad1cc02a chore(version): version.json [skip ci] 2023-10-24 17:20:32 +00:00
Joe Boccanfuso d98439fe7f fix(sr): dcm4chee requires the patient name for an SR to match what is in the original study (#3739) 2023-10-24 13:06:12 -04:00
ohif-bot 3bbbcea0d7 chore(version): Update package versions [skip ci] 2023-10-23 14:03:11 +00:00
ohif-bot 6e993511b7 chore(version): version.json [skip ci] 2023-10-23 14:03:01 +00:00
bluesteel23 eec9c33b76 docs(azure): Azure static Deployment details (#3740) 2023-10-23 09:53:43 -04:00
ohif-bot c31c7f8e79 chore(version): Update package versions [skip ci] 2023-10-23 13:23:07 +00:00
ohif-bot a7125f9385 chore(version): version.json [skip ci] 2023-10-23 13:22:59 +00:00
edwardyangxin 49514aedfe fix(recipes): package.json script orthanc:up docker-compose path (#3741) 2023-10-23 09:13:24 -04:00
ohif-bot 43c882ac87 chore(version): Update package versions [skip ci] 2023-10-19 17:39:23 +00:00
ohif-bot 912b3c7c53 chore(version): version.json [skip ci] 2023-10-19 17:39:15 +00:00
d9258eca70 fix(cine): Use the frame rate specified in DICOM and optionally auto play cine (#3735)
Co-authored-by: rareramos <rareramos1126@gmail.com>
Co-authored-by: Doug Horner <horner@mieweb.com>
Co-authored-by: Rehan <58819707+Rayhan-011@users.noreply.github.com>
2023-10-19 13:29:44 -04:00
ohif-bot fa599d05fb chore(version): Update package versions [skip ci] 2023-10-19 17:10:43 +00:00
ohif-bot 5f8325e338 chore(version): version.json [skip ci] 2023-10-19 17:10:36 +00:00
Joe Boccanfuso 93d798db99 fix(calibration): No calibration popup caused by perhaps an unused code optimization for production builds (#3736) 2023-10-19 13:02:03 -04:00
ohif-bot 33f1259408 chore(version): Update package versions [skip ci] 2023-10-12 21:18:38 +00:00
ohif-bot c54658a320 chore(version): version.json [skip ci] 2023-10-12 21:18:31 +00:00
Alireza 55002fbd71 chore(version): move to the next beta version [BUMP BETA] 2023-10-12 17:09:44 -04:00
ohif-bot 3f9ee1451b chore(version): Update package versions [skip ci] 2023-10-11 16:10:32 +00:00
ohif-bot 70ea13ec1f chore(version): version.json [skip ci] 2023-10-11 16:10:25 +00:00
Alireza 157b88c909 fix(display messages): broken after timings (#3719) 2023-10-11 12:01:32 -04:00
ohif-bot 75c3f94177 chore(version): Update package versions [skip ci] 2023-10-11 14:19:25 +00:00
ohif-bot 6874d5f0b9 chore(version): version.json [skip ci] 2023-10-11 14:19:16 +00:00
Alireza a3f2a1a7b0 fix(export): wrong export for the tmtv RT function (#3715) 2023-10-11 10:10:29 -04:00
ohif-bot 8067f0c62c chore(version): Update package versions [skip ci] 2023-10-10 19:30:50 +00:00
ohif-bot a894b6ffc5 chore(version): version.json [skip ci] 2023-10-10 19:30:41 +00:00
Joe Boccanfuso c3a5847dcd fix(i18n): display set(s) are two words for English messages (#3711) 2023-10-10 15:20:47 -04:00
ohif-bot 132938bbe0 chore(version): Update package versions [skip ci] 2023-10-10 17:32:24 +00:00
ohif-bot 5ba63bd010 chore(version): version.json [skip ci] 2023-10-10 17:32:14 +00:00
Alireza 7c57f67844 fix(modules): add stylus loader as an option to be uncommented (#3710) 2023-10-10 13:21:17 -04:00
ohif-bot dc929d3df5 chore(version): Update package versions [skip ci] 2023-10-10 14:36:12 +00:00
ohif-bot 5a3af83168 chore(version): version.json [skip ci] 2023-10-10 14:36:03 +00:00
Joe Boccanfuso a9a6ad50ea fix(segmentation): Various fixes for segmentation mode and other (#3709) 2023-10-10 10:25:30 -04:00
ohif-bot f8455056cd chore(version): Update package versions [skip ci] 2023-10-10 00:58:12 +00:00
ohif-bot f9c3d510d3 chore(version): version.json [skip ci] 2023-10-10 00:58:05 +00:00
Alireza 52f34c64d0 fix(voi): should publish voi change event on reset (#3707) 2023-10-09 20:49:10 -04:00
ohif-bot 7d87766ac4 chore(version): Update package versions [skip ci] 2023-10-09 17:58:57 +00:00
ohif-bot 820bb83b70 chore(version): version.json [skip ci] 2023-10-09 17:58:49 +00:00
Alireza 0a42d573bb fix(modality unit): fix the modality unit per target via upgrade of cs3d (#3706) 2023-10-09 13:48:31 -04:00
ohif-bot f9f33f74c1 chore(version): Update package versions [skip ci] 2023-10-09 15:13:37 +00:00
ohif-bot 6a5fe59136 chore(version): version.json [skip ci] 2023-10-09 15:13:28 +00:00
Alireza 4911e4796c fix(segmentation): do not use SAB if not specified (#3705) 2023-10-09 11:02:49 -04:00
ohif-bot 349c503f04 chore(version): Update package versions [skip ci] 2023-10-06 19:14:47 +00:00
ohif-bot a6a6aed361 chore(version): version.json [skip ci] 2023-10-06 19:14:40 +00:00
dxlinandAlireza 40673f64b3 feat(Segmentation): download RTSS from Labelmap(#3692)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2023-10-06 15:00:04 -04:00
ohif-bot eab42c1032 chore(version): Update package versions [skip ci] 2023-10-06 03:49:04 +00:00
ohif-bot a790be9f5a chore(version): version.json [skip ci] 2023-10-06 03:48:56 +00:00
Alireza 8bc12a37d0 fix(bugs): fixing lots of bugs regarding release candidate (#3700) 2023-10-05 23:39:56 -04:00
ohif-bot 29aeb070e0 chore(version): Update package versions [skip ci] 2023-10-06 02:22:47 +00:00
ohif-bot cdb66dcdef chore(version): version.json [skip ci] 2023-10-06 02:22:39 +00:00
Joe Boccanfuso 1fd98d9220 fix(segmentation scroll): and hydration bugs (#3701) 2023-10-05 22:13:08 -04:00
ohif-bot 748acaf9c1 chore(version): Update package versions [skip ci] 2023-10-04 18:25:03 +00:00
ohif-bot 3f27c3820f chore(version): version.json [skip ci] 2023-10-04 18:24:55 +00:00
Joe Boccanfuso c1d5ee7e3f fix(measurement and microscopy): various small fixes for measurement and microscopy side panel (#3696) 2023-10-04 14:11:09 -04:00
ohif-bot 646bef1d93 chore(version): Update package versions [skip ci] 2023-10-04 17:22:52 +00:00
ohif-bot 83c001f8e4 chore(version): version.json [skip ci] 2023-10-04 17:22:43 +00:00
Alirezaandpwespi ebe8f71da2 feat(locale): add German translations - community PR (#3697)
Co-authored-by: pwespi <pwespi@pm.me>
2023-10-04 13:11:31 -04:00
ohif-bot 17f528bcde chore(version): Update package versions [skip ci] 2023-10-04 16:34:07 +00:00
ohif-bot ed82511acc chore(version): version.json [skip ci] 2023-10-04 16:33:58 +00:00
AlirezaandAhmet Altay 745050a28e feat(locale): Added Turkish language support (tr-TR) - Community PR (#3695)
Co-authored-by: Ahmet Altay <46381367+ahmetaltay33@users.noreply.github.com>
2023-10-04 12:22:26 -04:00
ohif-bot 953c564d98 chore(version): Update package versions [skip ci] 2023-10-04 16:17:52 +00:00
ohif-bot c418185f0b chore(version): version.json [skip ci] 2023-10-04 16:17:44 +00:00
wangxuan 29748d46a1 fix(translation): Side panel translate fix (#3156) 2023-10-04 12:07:30 -04:00
ohif-bot a755cce7f1 chore(version): Update package versions [skip ci] 2023-10-04 15:37:52 +00:00
ohif-bot 42b7d9983c chore(version): version.json [skip ci] 2023-10-04 15:37:44 +00:00
Joe Boccanfuso 28cec04ff4 fix(cli): Add npm packaged mode not working (#3689) 2023-10-04 11:27:55 -04:00
ohif-bot 01d843f267 chore(version): Update package versions [skip ci] 2023-10-03 21:11:31 +00:00
ohif-bot 15eca78b68 chore(version): version.json [skip ci] 2023-10-03 21:11:23 +00:00
Bill Wallace 108383b9ef feat(debug): Add timing information about time to first image/all images, and query time (#3681) 2023-10-03 17:01:00 -04:00
ohif-bot 1b207f14d2 chore(version): Update package versions [skip ci] 2023-10-03 19:28:23 +00:00
ohif-bot 38cd3d8afc chore(version): version.json [skip ci] 2023-10-03 19:28:14 +00:00
AlirezaandOuwen Huang 5e7fe91617 feat(displayArea): add display area to hanging protocol (#3691)
Co-authored-by: Ouwen Huang <ouwen.oh@gmail.com>
2023-10-03 15:18:15 -04:00
ohif-bot 64f68f58c2 chore(version): Update package versions [skip ci] 2023-10-03 18:36:48 +00:00
ohif-bot 49bc346661 chore(version): version.json [skip ci] 2023-10-03 18:36:39 +00:00
Yaroslav Halchenko 5cc1dc9b9c docs(links): Adding references to two used markdown links (#3650) 2023-10-03 14:26:31 -04:00
ohif-bot 2a086a699a chore(version): Update package versions [skip ci] 2023-10-03 18:12:49 +00:00
ohif-bot 58f214bca5 chore(version): version.json [skip ci] 2023-10-03 18:12:41 +00:00
rodrigobasilio2022 4dc2acdefa fix(editing): regression bug in disable editing (#3687) 2023-10-03 14:02:21 -04:00
ohif-bot 4c88a4a2fd chore(version): Update package versions [skip ci] 2023-10-03 16:35:17 +00:00
ohif-bot 06facd8ea2 chore(version): version.json [skip ci] 2023-10-03 16:35:08 +00:00
Edward Sonandedward65 eb22328fc0 fix(typescript error): Change pubSubServiceInterface file type to typescript (#3546)
Co-authored-by: edward65 <edward@afxmedical.com>
2023-10-03 12:23:40 -04:00
ohif-bot b33ad0c7e9 chore(version): Update package versions [skip ci] 2023-10-03 15:55:03 +00:00
ohif-bot 3415cf909f chore(version): version.json [skip ci] 2023-10-03 15:54:54 +00:00
Joe BoccanfusoandAlireza e36a604331 fix(dicom overlay): Handle special cases of ArrayBuffer for various DICOM overlay attributes. (#3684)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2023-10-03 11:44:56 -04:00
Bill Wallace 8a335bd03d fix(StackSync): Miscellaneous fixes for stack image sync (#3663) 2023-10-03 10:59:09 -04:00
ohif-bot b3429729f1 chore(version): Update package versions [skip ci] 2023-10-03 14:22:22 +00:00
ohif-bot e2e806f188 chore(version): version.json [skip ci] 2023-10-03 14:22:12 +00:00
Alireza 1129c155d2 fix(config): support more values for the useSharedArrayBuffer (#3688) 2023-10-03 10:11:32 -04:00
ohif-bot c9c57b04d3 chore(version): Update package versions [skip ci] 2023-09-29 22:04:50 +00:00
ohif-bot d09938b72a chore(version): version.json [skip ci] 2023-09-29 22:04:41 +00:00
Alireza a67d72de85 fix(no sab): should work when shared array buffer is not required (#3686) 2023-09-29 17:54:16 -04:00
ohif-bot 3dabee2342 chore(version): Update package versions [skip ci] 2023-09-29 14:51:58 +00:00
ohif-bot 66abdace30 chore(version): version.json [skip ci] 2023-09-29 14:51:49 +00:00
Joe BoccanfusoandAlireza dc73b18748 fix(cli): various fixes for adding custom modes and extensions (#3683)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2023-09-29 10:35:30 -04:00
ohif-bot 52da92fed4 chore(version): Update package versions [skip ci] 2023-09-26 20:58:57 +00:00
ohif-bot cfbb3cfded chore(version): version.json [skip ci] 2023-09-26 20:58:49 +00:00
Joe Boccanfuso 86f54d0d07 fix(toggleOneUp): fixed one up for main tmtv layout (#3677) 2023-09-26 16:49:06 -04:00
ohif-bot 485cf037a1 chore(version): Update package versions [skip ci] 2023-09-26 19:38:00 +00:00
ohif-bot 3d38122b84 chore(version): version.json [skip ci] 2023-09-26 19:37:51 +00:00
Alireza 8feb5ee3f9 chore(publish): fix timeout problem without lerna (#3679) 2023-09-26 15:27:24 -04:00
ohif-bot 1f906963e7 chore(version): Update package versions [skip ci] 2023-09-26 15:54:37 +00:00
ohif-bot 9a4aa1a5aa chore(version): version.json [skip ci] 2023-09-26 15:54:28 +00:00
Alireza fcd323fa8c chore(publish): fix timeout problem (#3678) 2023-09-26 11:43:37 -04:00
ohif-bot b6e33224c8 chore(version): Update package versions [skip ci] 2023-09-26 13:03:46 +00:00
ohif-bot b8e519a4a9 chore(version): version.json [skip ci] 2023-09-26 13:03:37 +00:00
Alireza 0bf9949f45 chore(build): try to publish to npm again (#3676) 2023-09-26 08:53:40 -04:00
ohif-bot 3431b103e2 chore(version): Update package versions [skip ci] 2023-09-26 03:23:59 +00:00
ohif-bot 00918d86d3 chore(version): version.json [skip ci] 2023-09-26 03:23:52 +00:00
Alireza c86b129f58 chore(build): try to publish to npm (#3675) 2023-09-25 23:13:43 -04:00
ohif-bot 10ca35d5f4 chore(version): Update package versions [skip ci] 2023-09-22 20:00:44 +00:00
ohif-bot a308f7b4b6 chore(version): version.json [skip ci] 2023-09-22 20:00:36 +00:00
Sofien-Sellami 04ca10d877 fix(react-select): update react select package (#3622) 2023-09-22 15:48:12 -04:00
Joe Boccanfuso 85c899b399 feat(SidePanel): new side panel tab look-and-feel (#3657) 2023-09-22 15:36:44 -04:00
Alireza 48bbd6281a feat(segmentation mode): Add create, and export SEG with Brushes (#3632) 2023-09-22 10:23:44 -04:00
ohif-bot 896866e9d0 chore(version): Update package versions [skip ci] 2023-09-22 13:40:25 +00:00
ohif-bot a02a5ded9a chore(version): version.json [skip ci] 2023-09-22 13:40:16 +00:00
AlirezaandOuwen Huang 2bd3b26a6a perf(memory): add 16 bit texture via configuration - reduces memory by half (#3662)
Co-authored-by: Ouwen Huang <ouwen.huang@duke.edu>
2023-09-22 09:31:45 -04:00
ohif-bot 034a6d7696 chore(version): Update package versions [skip ci] 2023-09-21 20:53:19 +00:00
ohif-bot 2fc28978da chore(version): version.json [skip ci] 2023-09-21 20:53:11 +00:00
Joe Boccanfuso 221dedde5d fix(mpr): Return the original/raw hanging protocol when fetching and preserving the current active protocol. (#3670) 2023-09-21 16:45:15 -04:00
ohif-bot 807bd663b3 chore(version): Update package versions [skip ci] 2023-09-21 11:38:01 +00:00
ohif-bot 0318504597 chore(version): version.json [skip ci] 2023-09-21 11:37:54 +00:00
Alireza d609ae1edb docs(url): fix incorrect url param in the doc (#3667) 2023-09-21 07:30:01 -04:00
ohif-bot 2132f0777e chore(version): Update package versions [skip ci] 2023-09-19 14:19:42 +00:00
ohif-bot 10ecd4997b chore(version): version.json [skip ci] 2023-09-19 14:19:34 +00:00
AlirezaandJoe Boccanfuso 2d7721cb58 fix(keyCloak): fix openresty keycloak deployment recipe (#3655)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2023-09-19 10:11:32 -04:00
ohif-bot c49b833274 chore(version): Update package versions [skip ci] 2023-09-18 15:10:25 +00:00
ohif-bot 3279df543f chore(version): version.json [skip ci] 2023-09-18 15:10:17 +00:00
Joe Boccanfuso 2737903386 fix(DicomJson): retrieve.series.metadata method should be async (#3659) 2023-09-18 11:02:27 -04:00
ohif-bot 9ccdaef812 chore(version): Update package versions [skip ci] 2023-09-15 14:58:43 +00:00
ohif-bot 8c6764a936 chore(version): version.json [skip ci] 2023-09-15 14:58:36 +00:00
Bill Wallace 38af3112ec fix(measurements): Update the calibration tool to match changes in CS3D (#3505)
This change makes the OHIF side consistent with the CS3D user calibration settings, and will correctly display px and rounding values consistent with the CS3D display.
2023-09-15 10:50:23 -04:00
ohif-bot bd7f9592ea chore(version): Update package versions [skip ci] 2023-09-12 14:38:15 +00:00
ohif-bot fd74e46280 chore(version): version.json [skip ci] 2023-09-12 14:38:08 +00:00
Ibrahim 74e62a1763 fix(health imaging): studies not loading from healthimaging if imagepositionpatient is missing (#3646) 2023-09-12 10:30:02 -04:00
ohif-bot 7eda0c5f46 chore(version): Update package versions [skip ci] 2023-09-12 11:49:05 +00:00
ohif-bot 15e1af2526 chore(version): version.json [skip ci] 2023-09-12 11:48:58 +00:00
Yaroslav Halchenko 11ca5b6eae docs(spelling): Add codespell config + github action, run and fix a good number of typos (#3645) 2023-09-12 07:40:38 -04:00
ohif-bot 64079e0720 chore(version): Update package versions [skip ci] 2023-09-12 02:02:13 +00:00
ohif-bot 7f683a336b chore(version): version.json [skip ci] 2023-09-12 02:02:04 +00:00
Joe Boccanfuso 0d10f46b88 fix(suv): import calculate-suv library version that prevents SUV calculation for a zero PatientWeight (#3638) 2023-09-11 21:50:32 -04:00
ohif-bot ca19baeafd chore(version): Update package versions [skip ci] 2023-09-12 01:39:48 +00:00
ohif-bot 3e73ddf4c1 chore(version): version.json [skip ci] 2023-09-12 01:39:41 +00:00
lokeshnano c0fb2b4892 chore(gitignore): add vercel ignore (#3640) 2023-09-11 21:32:22 -04:00
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
1306 changed files with 82629 additions and 35589 deletions

No files matched your search

+212 -170
View File
@@ -13,22 +13,24 @@ version: 2.1
##
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@1.26.0
cypress: cypress-io/cypress@3.3.1
executors:
# Custom executor to override Cypress config
deploy-to-prod-executor:
cypress-custom:
description: |
Single Docker container used to run Cypress Tests
docker:
- image: cimg/node:16.14
environment:
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/
chrome-and-pacs:
docker:
# Primary container image where all steps run.
- image: 'cypress/browsers:node16.14.2-slim-chrome103-ff102'
- image: cimg/node:<< parameters.node-version >>-browsers
parameters:
node-version:
default: '18.16.1'
description: |
The version of Node to run your tests with.
type: string
defaults: &defaults
docker:
- image: cimg/node:16.14-browsers
- image: cimg/node:18.18
environment:
TERM: xterm # Enable colors in term
QUICK_BUILD: true
@@ -43,6 +45,7 @@ jobs:
steps:
# Update yarn
- run: yarn -v
- run: node --version
# Checkout code and ALL Git Tags
- checkout
- restore_cache:
@@ -90,43 +93,6 @@ jobs:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
###
# Workflow: PR_OPTIONAL_DOCKER_PUBLISH
###
DOCKER_PR_PUBLISH:
<<: *defaults
steps:
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- checkout
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build and push Docker image
command: |
# Remove npm config
rm -f ./.npmrc
# Set our version number using vars
echo $CIRCLE_BUILD_NUM
# Build our image, auth, and push
docker build --tag ohif/app:PR_BUILD-$CIRCLE_BUILD_NUM .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:PR_BUILD-$CIRCLE_BUILD_NUM
###
# Workflow: DEPLOY
###
@@ -177,62 +143,52 @@ jobs:
- commit.txt
- version.json
# DEPLOY_TO_DEV:
# docker:
# - image: circleci/node:16.14.0
# environment:
# TERM: xterm
# NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77
# working_directory: ~/repo
# steps:
# - attach_workspace:
# at: ~/repo
# - run: cd .netlify && npm install
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
# - run: cd .netlify && npm run deploy
# DEPLOY_TO_STAGING:
# docker:
# - image: circleci/node:16.14.0
# environment:
# TERM: xterm
# NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969
# working_directory: ~/repo
# steps:
# - attach_workspace:
# at: ~/repo
# - run: cd .netlify && npm install
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
# - run: cd .netlify && npm run deploy
# DEPLOY_TO_PRODUCTION:
# docker:
# - image: circleci/node:16.14.0
# environment:
# TERM: xterm
# NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0
# working_directory: ~/repo
# steps:
# - attach_workspace:
# at: ~/repo
# - run: cd .netlify && npm install
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
# - run: cd .netlify && npm run deploy
# DEPLOY_TO_RELEASE_DEV:
# docker:
# - image: circleci/node:16.14.0
# environment:
# TERM: xterm
# NETLIFY_SITE_ID: 3270878-22
# working_directory: ~/repo
# steps:
# - attach_workspace:
# at: ~/repo
# - run: cd .netlify && npm install
# - run:
# cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
# - run: cd .netlify && npm run deploy
# just to make sure later on we can publish them
BUILD_PACKAGES_QUICK:
<<: *defaults
steps:
- run: yarn -v
# Checkout code and ALL Git Tags
- checkout
- attach_workspace:
at: ~/repo
# Use increasingly general patterns to restore cache
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache/yarn
key: yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Avoid hosts unknown for github
command: |
rm -rf ~/.ssh
mkdir ~/.ssh/
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
git config --global user.name "ohif-bot"
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
yarn run build:package-all
- run:
name: build the other half of the packages
command: |
yarn run build:package-all-1
###
# Workflow: RELEASE
@@ -269,8 +225,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: |
@@ -283,14 +238,21 @@ jobs:
name: build the other half of the packages
command: |
yarn run build:package-all-1
- run:
name: increase min time out
command: |
npm config set fetch-retry-mintimeout 20000
- run:
name: increase max time out
command: |
npm config set fetch-retry-maxtimeout 120000
- run:
name: publish package versions
command: |
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 +299,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)
@@ -365,71 +326,152 @@ jobs:
docker push ohif/app:$IMAGE_VERSION_FULL
fi
workflows:
version: 2
# This is copied from the Cypress orb since the default for cypress/run is node 16 and
# we migrated to 18
CYPRESS_CUSTOM_RUN:
description: |
A single, complete job to run Cypress end-to-end tests in your application.
executor: cypress-custom
parallelism: << parameters.parallelism >>
parameters:
cypress-cache-key:
default: cypress-cache-{{ arch }}-{{ checksum "package.json" }}
description: Cache key used to cache the Cypress binary.
type: string
cypress-cache-path:
default: ~/.cache/Cypress
description: |
By default, this will cache the '~/.cache/Cypress' directory so that the Cypress binary is cached. You can override this by providing your own cache path.
type: string
cypress-command:
default: npx cypress run
description: Command used to run your Cypress tests
type: string
include-branch-in-node-cache-key:
default: false
description: |
If true, this cache will only apply to runs within the same branch. (Adds -{{ .Branch }}- to the node cache key)
type: boolean
install-browsers:
default: false
description: |
Cypress runs by default in the Electron browser. Use this flag to install additional browsers to run your tests in.
This is only needed if you are passing the `--browser` flag in your `cypress-command`.
This parameter leverages the `circleci/browser-tools` orb and includes Chrome and FireFox.
If you need additional browser support you can set this to false and use an executor with a docker image
that includes the browsers of your choosing. See https://hub.docker.com/r/cypress/browsers/tags
type: boolean
install-command:
default: ''
description: Overrides the default NPM command (npm ci)
type: string
node-cache-version:
default: v1
description:
Change the default node cache version if you need to clear the cache for any reason.
type: string
package-manager:
default: npm
description: Select the default node package manager to use. NPM v5+ Required.
enum:
- npm
- yarn
- yarn-berry
type: enum
parallelism:
default: 1
description: |
Number of Circle machines to use for load balancing, min 1
(requires `parallel` and `record` flags in your `cypress-command`)
type: integer
post-install:
default: ''
description: |
Additional commands to run after running install but before verifying Cypress and saving cache.
type: string
start-command:
default: ''
description: Command used to start your local dev server for Cypress to tests against
type: string
working-directory:
default: ''
description: Directory containing package.json
type: string
steps:
- cypress/install:
cypress-cache-key: << parameters.cypress-cache-key >>
cypress-cache-path: << parameters.cypress-cache-path >>
include-branch-in-node-cache-key: << parameters.include-branch-in-node-cache-key >>
install-browsers: << parameters.install-browsers >>
install-command: << parameters.install-command >>
node-cache-version: << parameters.node-cache-version >>
package-manager: << parameters.package-manager >>
post-install: << parameters.post-install >>
working-directory: << parameters.working-directory >>
- cypress/run-tests:
cypress-command: << parameters.cypress-command >>
start-command: << parameters.start-command >>
working-directory: << parameters.working-directory >>
workflows:
PR_CHECKS:
jobs:
- UNIT_TESTS
# E2E: PWA
- cypress/run:
name: 'E2E: PWA'
executor: chrome-and-pacs
browser: chrome
pre-steps:
- run: |
# Clear yarn cache; use yarn from image (update image to update yarn)
rm -rf ~/.yarn
yarn -v
yarn: true
record: true
store_artifacts: true
working_directory: platform/app
build: yarn test:data
start: yarn run test:e2e:serve
spec: 'cypress/integration/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/app/cypress/screenshots
- store_artifacts:
path: platform/app/cypress/videos
- store_test_results:
path: platform/app/cypress/results
- BUILD_PACKAGES_QUICK:
filters:
branches:
ignore: master
- UNIT_TESTS:
requires:
- UNIT_TESTS
PR_OPTIONAL_VISUAL_TESTS:
jobs:
- AWAIT_APPROVAL:
type: approval
# Update hub.docker.org
- cypress/run:
name: 'Generate Percy Snapshots'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps:
- run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on'
yarn: true
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
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/app/cypress/screenshots
- store_artifacts:
path: platform/app/cypress/videos
- BUILD_PACKAGES_QUICK
- CYPRESS_CUSTOM_RUN:
name: 'Cypress Tests'
context: cypress
matrix:
parameters:
start-command:
- yarn run test:data && yarn run test:e2e:serve
install-browsers:
- true
cypress-command:
- 'npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run
--record --browser chrome --parallel'
package-manager:
- 'yarn'
cypress-cache-key:
- 'yarn-packages-{{ checksum "yarn.lock" }}'
cypress-cache-path:
- '~/.cache/Cypress'
requires:
- AWAIT_APPROVAL
- BUILD_PACKAGES_QUICK
# PR_OPTIONAL_VISUAL_TESTS:
# jobs:
# - AWAIT_APPROVAL:
# type: approval
# # Update hub.docker.org
# - cypress/run:
# name: 'Generate Percy Snapshots'
# executor: cypress/browsers-chrome76
# browser: chrome
# pre-steps:
# - run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on'
# yarn: true
# 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
# # start server --> verify running --> percy + chrome + cypress
# command: yarn run test:e2e:dist
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
# no-workspace: true # Don't persist workspace
# post-steps:
# - store_artifacts:
# path: platform/app/cypress/screenshots
# - store_artifacts:
# path: platform/app/cypress/videos
# requires:
# - AWAIT_APPROVAL
# Our master branch deploys to viewer-dev.ohif.org, the viewer.ohif.org is
# deployed from the release branch which is more stable and less frequently updated.
+6
View File
@@ -0,0 +1,6 @@
[codespell]
skip = .git,*.pdf,*.svg,yarn.lock,*.min.js,locales
# ignore words ending with … and some camelcased variables and names
ignore-regex = \b\S+…\S*|\b(doubleClick|afterAll|PostgresSQL)\b|\bWee, L\.|.*te.*Telugu.*
# some odd variables
ignore-words-list = datea,ser,childrens
-48
View File
@@ -1,48 +0,0 @@
worker_processes 1;
events { worker_connections 1024; }
http {
upstream orthanc-server {
server orthanc:8042;
}
server {
listen [::]:80 default_server;
listen 80;
# CORS Magic
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://orthanc:8042;
proxy_redirect off;
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-Host $server_name;
# CORS Magic
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
}
}
}
-89
View File
@@ -1,89 +0,0 @@
{
"Name": "Orthanc inside Docker",
"StorageDirectory": "/var/lib/orthanc/db",
"IndexDirectory": "/var/lib/orthanc/db",
"StorageCompression": false,
"MaximumStorageSize": 0,
"MaximumPatientCount": 0,
"LuaScripts": [],
"Plugins": ["/usr/share/orthanc/plugins", "/usr/local/share/orthanc/plugins"],
"ConcurrentJobs": 2,
"HttpServerEnabled": true,
"HttpPort": 8042,
"HttpDescribeErrors": true,
"HttpCompressionEnabled": true,
"DicomServerEnabled": true,
"DicomAet": "ORTHANC",
"DicomCheckCalledAet": false,
"DicomPort": 4242,
"DefaultEncoding": "Latin1",
"DeflatedTransferSyntaxAccepted": true,
"JpegTransferSyntaxAccepted": true,
"Jpeg2000TransferSyntaxAccepted": true,
"JpegLosslessTransferSyntaxAccepted": true,
"JpipTransferSyntaxAccepted": true,
"Mpeg2TransferSyntaxAccepted": true,
"RleTransferSyntaxAccepted": true,
"UnknownSopClassAccepted": false,
"DicomScpTimeout": 30,
"RemoteAccessAllowed": true,
"SslEnabled": false,
"SslCertificate": "certificate.pem",
"AuthenticationEnabled": false,
"RegisteredUsers": {
"test": "test"
},
"DicomModalities": {},
"DicomModalitiesInDatabase": false,
"DicomAlwaysAllowEcho": true,
"DicomAlwaysAllowStore": true,
"DicomCheckModalityHost": false,
"DicomScuTimeout": 10,
"OrthancPeers": {},
"OrthancPeersInDatabase": false,
"HttpProxy": "",
"HttpVerbose": true,
"HttpTimeout": 10,
"HttpsVerifyPeers": true,
"HttpsCACertificates": "",
"UserMetadata": {},
"UserContentType": {},
"StableAge": 60,
"StrictAetComparison": false,
"StoreMD5ForAttachments": true,
"LimitFindResults": 0,
"LimitFindInstances": 0,
"LimitJobs": 10,
"LogExportedResources": false,
"KeepAlive": true,
"TcpNoDelay": true,
"HttpThreadsCount": 50,
"StoreDicom": true,
"DicomAssociationCloseDelay": 5,
"QueryRetrieveSize": 10,
"CaseSensitivePN": false,
"LoadPrivateDictionary": true,
"Dictionary": {},
"SynchronousCMove": true,
"JobsHistorySize": 10,
"SaveJobs": true,
"OverwriteInstances": false,
"MediaArchiveSize": 1,
"StorageAccessOnFind": "Always",
"MetricsEnabled": true,
"DicomWeb": {
"Enable": true,
"Root": "/dicom-web/",
"EnableWado": true,
"WadoRoot": "/wado",
"Host": "127.0.0.1",
"Ssl": false,
"StowMaxInstances": 10,
"StowMaxSize": 10,
"QidoCaseSensitive": false
}
}
-23
View File
@@ -1,23 +0,0 @@
version: '3.5'
services:
orthanc:
image: jodogne/orthanc-plugins:1.11.0
hostname: orthanc
volumes:
# Config
- ./config/orthanc.json:/etc/orthanc/orthanc.json:ro
# Persist data
- ./volumes/orthanc-db/:/var/lib/orthanc/db/
ports:
- '4242:4242' # DICOM
- '8042:8042' # Web
restart: unless-stopped
nginx:
image: nginx:latest
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf
ports:
- '80:80' #ngnix proxy
depends_on:
- orthanc
@@ -1,2 +0,0 @@
*
!.gitignore
+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/
+1 -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",
+6 -8
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
@@ -66,7 +64,7 @@ body:
attributes:
label: 'Node version'
description: 'Your Node.js version.'
placeholder: 'e.g., 16.14.0'
placeholder: 'e.g., 18.16.1'
validations:
required: true
- type: input
@@ -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
+1 -1
View File
@@ -83,7 +83,7 @@ after the commits are squashed.
#### Tested Environment
- [] OS: <!--[e.g. Windows 10, macOS 10.15.4]-->
- [] Node version: <!--[e.g. 16.14.0]-->
- [] Node version: <!--[e.g. 18.16.1]-->
- [] Browser:
<!--[e.g. Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1]-->
+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
+3
View File
@@ -42,3 +42,6 @@ platform/app/src/pluginImports.js
/Viewers.iml
platform/app/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
platform/app/.recipes/OpenResty-Orthanc/logs/*
.vercel
.vs
+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>
+1 -1
View File
@@ -1 +1 @@
16.14.0
18.16.1
+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"
}
+265
View File
@@ -0,0 +1,265 @@
/*
* This script uses nodejs to generate a JSON file from a DICOM study folder.
* You need to have dcmjs installed in your project.
* The JSON file can be used to load the study into the OHIF Viewer. You can get more detail
* in the DICOM JSON Data source on docs.ohif.org
*
* Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>
*
* params:
* - studyFolder: path to the study folder
* - urlPrefix: prefix to the url that will be used to load the study into the viewer. For instance
* we use https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data as the urlPrefix for the
* example since the data is hosted on S3 and each study is in a folder. So the url in the generated
* json file for the first instance of the first series of the first study will be
* dicomweb:https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data/Series1/Instance1
* - outputJSONPath: path to the output JSON file
*/
const dcmjs = require('dcmjs');
const path = require('path');
const fs = require('fs').promises;
const args = process.argv.slice(2);
const [studyDirectory, urlPrefix, outputPath] = args;
if (args.length !== 3) {
console.error('Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>');
process.exit(1);
}
const model = {
studies: [],
};
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath) {
try {
const files = await recursiveReadDir(studyDirectory);
console.debug('Processing...');
for (const file of files) {
if (!file.includes('.DS_Store') && !file.includes('.xml')) {
const arrayBuffer = await fs.readFile(file);
const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer);
const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict);
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory);
processInstance(instance);
}
}
console.log('Successfully loaded data');
model.studies.forEach(study => {
study.NumInstances = findInstancesNumber(study);
study.Modalities = findModalities(study).join('/');
});
await fs.writeFile(outputPath, JSON.stringify(model, null, 2));
console.log('JSON saved');
} catch (error) {
console.error(error);
}
}
async function recursiveReadDir(dir) {
let results = [];
const list = await fs.readdir(dir);
for (const file of list) {
const filePath = path.resolve(dir, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
const res = await recursiveReadDir(filePath);
results = results.concat(res);
} else {
results.push(filePath);
}
}
return results;
}
function createImageId(fileLocation, urlPrefix, studyDirectory) {
const relativePath = path.relative(studyDirectory, fileLocation);
const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/');
return `dicomweb:${urlPrefix}${normalizedPath}`;
}
function processInstance(instance) {
const { StudyInstanceUID, SeriesInstanceUID } = instance;
let study = getStudy(StudyInstanceUID);
if (!study) {
study = createStudyMetadata(StudyInstanceUID, instance);
model.studies.push(study);
}
let series = getSeries(StudyInstanceUID, SeriesInstanceUID);
if (!series) {
series = createSeriesMetadata(instance);
study.series.push(series);
}
const instanceMetaData =
instance.NumberOfFrames > 1
? createInstanceMetaDataMultiFrame(instance)
: createInstanceMetaData(instance);
series.instances.push(...[].concat(instanceMetaData));
}
function getStudy(StudyInstanceUID) {
return model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID);
}
function getSeries(StudyInstanceUID, SeriesInstanceUID) {
const study = getStudy(StudyInstanceUID);
return study
? study.series.find(series => series.SeriesInstanceUID === SeriesInstanceUID)
: undefined;
}
const findInstancesNumber = study => {
let numInstances = 0;
study.series.forEach(aSeries => {
numInstances = numInstances + aSeries.instances.length;
});
return numInstances;
};
const findModalities = study => {
let modalities = new Set();
study.series.forEach(aSeries => {
modalities.add(aSeries.Modality);
});
return Array.from(modalities);
};
function createStudyMetadata(StudyInstanceUID, instance) {
return {
StudyInstanceUID,
StudyDescription: instance.StudyDescription,
StudyDate: instance.StudyDate,
StudyTime: instance.StudyTime,
PatientName: instance.PatientName,
PatientID: instance.PatientID || '1234', // this is critical to have
AccessionNumber: instance.AccessionNumber,
PatientAge: instance.PatientAge,
PatientSex: instance.PatientSex,
PatientWeight: instance.PatientWeight,
series: [],
};
}
function createSeriesMetadata(instance) {
return {
SeriesInstanceUID: instance.SeriesInstanceUID,
SeriesDescription: instance.SeriesDescription,
SeriesNumber: instance.SeriesNumber,
SeriesTime: instance.SeriesTime,
Modality: instance.Modality,
SliceThickness: instance.SliceThickness,
instances: [],
};
}
function commonMetaData(instance) {
return {
Columns: instance.Columns,
Rows: instance.Rows,
InstanceNumber: instance.InstanceNumber,
SOPClassUID: instance.SOPClassUID,
AcquisitionNumber: instance.AcquisitionNumber,
PhotometricInterpretation: instance.PhotometricInterpretation,
BitsAllocated: instance.BitsAllocated,
BitsStored: instance.BitsStored,
PixelRepresentation: instance.PixelRepresentation,
SamplesPerPixel: instance.SamplesPerPixel,
PixelSpacing: instance.PixelSpacing,
HighBit: instance.HighBit,
ImageOrientationPatient: instance.ImageOrientationPatient,
ImagePositionPatient: instance.ImagePositionPatient,
FrameOfReferenceUID: instance.FrameOfReferenceUID,
ImageType: instance.ImageType,
Modality: instance.Modality,
SOPInstanceUID: instance.SOPInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
StudyInstanceUID: instance.StudyInstanceUID,
WindowCenter: instance.WindowCenter,
WindowWidth: instance.WindowWidth,
RescaleIntercept: instance.RescaleIntercept,
RescaleSlope: instance.RescaleSlope,
};
}
function conditionalMetaData(instance) {
return {
...(instance.ConceptNameCodeSequence && {
ConceptNameCodeSequence: instance.ConceptNameCodeSequence,
}),
...(instance.SeriesDate && { SeriesDate: instance.SeriesDate }),
...(instance.ReferencedSeriesSequence && {
ReferencedSeriesSequence: instance.ReferencedSeriesSequence,
}),
...(instance.SharedFunctionalGroupsSequence && {
SharedFunctionalGroupsSequence: instance.SharedFunctionalGroupsSequence,
}),
...(instance.PerFrameFunctionalGroupsSequence && {
PerFrameFunctionalGroupsSequence: instance.PerFrameFunctionalGroupsSequence,
}),
...(instance.ContentSequence && { ContentSequence: instance.ContentSequence }),
...(instance.ContentTemplateSequence && {
ContentTemplateSequence: instance.ContentTemplateSequence,
}),
...(instance.CurrentRequestedProcedureEvidenceSequence && {
CurrentRequestedProcedureEvidenceSequence: instance.CurrentRequestedProcedureEvidenceSequence,
}),
...(instance.CodingSchemeIdentificationSequence && {
CodingSchemeIdentificationSequence: instance.CodingSchemeIdentificationSequence,
}),
...(instance.RadiopharmaceuticalInformationSequence && {
RadiopharmaceuticalInformationSequence: instance.RadiopharmaceuticalInformationSequence,
}),
...(instance.ROIContourSequence && {
ROIContourSequence: instance.ROIContourSequence,
}),
...(instance.StructureSetROISequence && {
StructureSetROISequence: instance.StructureSetROISequence,
}),
...(instance.ReferencedFrameOfReferenceSequence && {
ReferencedFrameOfReferenceSequence: instance.ReferencedFrameOfReferenceSequence,
}),
...(instance.CorrectedImage && { CorrectedImage: instance.CorrectedImage }),
...(instance.Units && { Units: instance.Units }),
...(instance.DecayCorrection && { DecayCorrection: instance.DecayCorrection }),
...(instance.AcquisitionDate && { AcquisitionDate: instance.AcquisitionDate }),
...(instance.AcquisitionTime && { AcquisitionTime: instance.AcquisitionTime }),
...(instance.PatientWeight && { PatientWeight: instance.PatientWeight }),
...(instance.NumberOfFrames && { NumberOfFrames: instance.NumberOfFrames }),
...(instance.FrameTime && { FrameTime: instance.FrameTime }),
...(instance.EncapsulatedDocument && { EncapsulatedDocument: instance.EncapsulatedDocument }),
...(instance.SequenceOfUltrasoundRegions && {
SequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions,
}),
};
}
function createInstanceMetaData(instance) {
const metadata = {
...commonMetaData(instance),
...conditionalMetaData(instance),
};
return { metadata, url: instance.fileLocation };
}
function createInstanceMetaDataMultiFrame(instance) {
const instances = [];
const commonData = commonMetaData(instance);
const conditionalData = conditionalMetaData(instance);
for (let i = 1; i <= instance.NumberOfFrames; i++) {
const metadata = { ...commonData, ...conditionalData };
const result = { metadata, url: instance.fileLocation + `?frame=${i}` };
instances.push(result);
}
return instances;
}
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath);
+1 -1
View File
@@ -31,6 +31,6 @@
"prettier.endOfLine": "lf",
"workbench.colorCustomizations": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
}
+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';
+10
View File
@@ -0,0 +1,10 @@
const stylusToJavaScript = {
test: /\.styl$/,
use: [
{ loader: 'style-loader' }, // 3. Style nodes from JS Strings
{ loader: 'css-loader' }, // 2. CSS to CommonJS
{ loader: 'stylus-loader' }, // 1. Stylus to CSS
],
};
module.exports = stylusToJavaScript;
+49 -31
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
@@ -19,6 +18,7 @@ const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.js');
const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
@@ -26,15 +26,34 @@ 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();
const defineValues = {
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.NODE_DEBUG': JSON.stringify(process.env.NODE_DEBUG),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
'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 || ''),
};
// Only redefine updated values. This avoids warning messages in the logs
if (!process.env.APP_CONFIG) {
defineValues['process.env.APP_CONFIG'] = '';
}
module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
if (!process.env.NODE_ENV) {
throw new Error('process.env.NODE_ENV not set');
@@ -94,10 +113,24 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
},
},
cssToJavaScript,
// Note: Only uncomment the following if you are using the old style of stylus in v2
// Also you need to uncomment this platform/app/.webpack/rules/extractStyleChunks.js
// stylusToJavaScript,
{
test: /\.wasm/,
type: 'asset/resource',
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/images/[name].[ext]',
},
},
],
},
], //.concat(vtkRules),
},
resolve: {
@@ -105,10 +138,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'),
@@ -131,29 +161,17 @@ 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({
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
'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 || ''
),
new webpack.DefinePlugin(defineValues),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
+1338
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -21,7 +21,7 @@
# Stage 1: Build the application
# docker build -t ohif/viewer:latest .
FROM node:16.15.0-slim as json-copier
FROM node:18.16.1-slim as json-copier
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
@@ -37,7 +37,7 @@ COPY platform /usr/src/app/platform
#RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
# Copy Files
FROM node:16.15.0-slim as builder
FROM node:18.16.1-slim as builder
RUN apt-get update && apt-get install -y build-essential python3
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
+4
View File
@@ -17,6 +17,10 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
<div align="center">
📰 <a href="https://ohif.org/news/"><strong>Join OHIF Newsletter</strong></a> 📰
</div>
<div align="center">
📰 <a href="https://ohif.org/news/"><strong>Join OHIF Newsletter</strong></a> 📰
</div>
<hr />
+3 -5
View File
@@ -3,11 +3,7 @@ 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',
@@ -30,6 +26,7 @@ module.exports = {
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
'@babel/plugin-transform-class-static-block',
],
env: {
test: {
@@ -49,6 +46,7 @@ module.exports = {
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/transform-destructuring',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
],
+1 -1
View File
@@ -1 +1 @@
b117a54aa10ee09e72e7675c6fb83cd8f49c26f7
cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1
-138
View File
@@ -1,138 +0,0 @@
import ImageSet from '@ohif/core/src/classes/ImageSet';
import { IWebApiDataSource } from '@ohif/core';
/**
*
*/
export default {
id: '@ohif/extension-*',
/**
* LIFECYCLE HOOKS
*/
preRegistration() {},
beforeExtInit() {},
beforeExtDestroy() {},
/**
* MODULES
*/
getCommandsModule,
getContextModule,
getDataSourcesModule,
getLayoutTemplateModule,
getPanelModule,
getSopClassHandlerModule,
getToolbarModule() {},
getViewportModule,
};
// appConfig,
// extensionConfig,
// dataSources,
// servicesManager,
// extensionManager,
// commandsManager,
/**
*
*/
const getCommandsModule = () => ({
definitions: {
exampleActionDef: {
commandFn: ({ param1 }) => {
console.log(`param1's value is: ${param1}`);
},
// storeContexts: ['viewports'],
options: { param1: 'hello world' },
context: 'VIEWER', // optional
},
},
defaultContext: 'ACTIVE_VIEWPORT::DICOMSR',
});
const ExampleContext = React.createContext();
function ExampleContextProvider({ children }) {
return (
<ExampleContext.Provider value={{ example: 'value' }}>
{children}
</ExampleContext.Provider>
);
}
const getContextModule = () => [
{
name: 'ExampleContext',
context: ExampleContext,
provider: ExampleContextProvider,
},
];
const getDataSourcesModule = () => [
{
name: 'exampleDataSource',
type: 'webApi', // 'webApi' | 'local' | 'other'
createDataSource: dataSourceConfig => {
return IWebApiDataSource.create(/* */);
},
},
];
const getLayoutTemplateModule = (/* ... */) => [
{
id: 'exampleLayout',
name: 'exampleLayout',
component: ExampleLayoutComponent,
},
];
const getPanelModule = () => {
return [
{
name: 'exampleSidePanel',
iconName: 'info-circle-o',
iconLabel: 'Example',
label: 'Hello World',
isDisabled: studies => {}, // optional
component: ExamplePanelContentComponent,
},
];
};
const getSopClassHandlerModule = (/* ... */) => {
const BASIC_TEXT_SR = '1.2.840.10008.5.1.4.1.1.88.11';
return [
{
name: 'ExampleSopClassHandle',
sopClassUids: [BASIC_TEXT_SR],
getDisplaySetsFromSeries: instances => {
const imageSet = new ImageSet(instances);
imageSet.setAttributes(/** */);
imageSet.sortBy((a, b) => 0);
return imageSet;
},
},
];
};
const getToolbarModule = () => {};
// displaySet, viewportIndex, dataSource
const getViewportModule = () => {
const wrappedViewport = props => {
return (
<ExampleViewport
{...props}
onEvent={data => {
commandsManager.runCommand('commandName', data);
}}
/>
);
};
return [{ name: 'example', component: wrappedViewport }];
};
@@ -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,
File diff suppressed because it is too large. Load diff
@@ -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__'],
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.7.0-beta.56",
"version": "3.8.0-beta.71",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -24,6 +24,8 @@
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-seg": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@@ -31,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.56",
"@ohif/extension-cornerstone": "3.7.0-beta.56",
"@ohif/extension-default": "3.7.0-beta.56",
"@ohif/i18n": "3.7.0-beta.56",
"@ohif/core": "3.8.0-beta.71",
"@ohif/extension-cornerstone": "3.8.0-beta.71",
"@ohif/extension-default": "3.8.0-beta.71",
"@ohif/i18n": "3.8.0-beta.71",
"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,12 +2,9 @@ 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 => {
@@ -62,4 +59,3 @@ 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,12 +149,8 @@ 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];
let isSupported = false;
@@ -229,9 +200,7 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry
? imageIdSopInstanceUidPairsEntry.imageId
: null;
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
@@ -252,8 +221,7 @@ function _setROIContourMetadata(
isSupported
) {
const StructureSetROI = StructureSetROISequence.find(
structureSetROI =>
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
structureSetROI => structureSetROI.ROINumber === ROIContour.ReferencedROINumber
);
const ROIContourData = {
@@ -293,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,70 +0,0 @@
async function _hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
servicesManager,
}) {
const {
segmentationService,
hangingProtocolService,
viewportGridService,
} = servicesManager.services;
const displaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
let segmentationId = null;
// We need the hydration to notify panels about the new segmentation added
const suppressEvents = false;
segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
rtDisplaySet,
segmentationId,
suppressEvents
);
segmentationService.hydrateSegmentation(rtDisplaySet.displaySetInstanceUID);
const { viewports } = viewportGridService.getState();
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex,
displaySetInstanceUID
);
viewportGridService.setDisplaySetsForViewports(updatedViewports);
// Todo: fix this after we have a better way for stack viewport segmentations
// check every viewport in the viewports to see if the displaySetInstanceUID
// 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) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
rtDisplaySet.displaySetInstanceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportIndex: index,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
initialImageOptions: {
preset: 'middle',
},
},
});
}
});
// Do the entire update at once
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
export default _hydrateRTDisplaySet;
@@ -1,12 +1,7 @@
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, {});
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
export default createRTToolGroupAndAddTools;
@@ -1,5 +1,4 @@
import { ButtonEnums } from '@ohif/ui';
import hydrateRTDisplaySet from './_hydrateRT';
const RESPONSE = {
NO_NEVER: -1,
@@ -10,17 +9,15 @@ const RESPONSE = {
function promptHydrateRT({
servicesManager,
rtDisplaySet,
viewportIndex,
viewportId,
toolGroupId = 'default',
preHydrateCallbacks,
hydrateRTDisplaySet,
}) {
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 => {
@@ -29,7 +26,7 @@ function promptHydrateRT({
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
viewportId,
toolGroupId,
servicesManager,
});
@@ -39,8 +36,8 @@ 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 = [
{
@@ -60,7 +57,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
};
uiViewportDialogService.show({
viewportIndex,
viewportId,
type: 'info',
message,
actions,
@@ -1,19 +1,11 @@
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 { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
import _hydrateRTdisplaySet from '../utils/_hydrateRT';
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import _hydrateRTDisplaySet from '../utils/_hydrateRT';
const { formatDate } = utils;
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
function OHIFCornerstoneRTViewport(props) {
@@ -21,8 +13,6 @@ function OHIFCornerstoneRTViewport(props) {
children,
displaySets,
viewportOptions,
viewportIndex,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
@@ -34,9 +24,12 @@ function OHIFCornerstoneRTViewport(props) {
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = 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) {
@@ -67,12 +60,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,
@@ -93,21 +84,26 @@ function OHIFCornerstoneRTViewport(props) {
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportIndex }) => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportIndex,
viewportId,
});
});
}, [viewportGrid]);
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [rtDisplaySet],
viewportId,
});
};
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 (
@@ -124,11 +120,10 @@ function OHIFCornerstoneRTViewport(props) {
onElementDisabled={onElementDisabled}
></Component>
);
}, [viewportIndex, rtDisplaySet, toolGroupId]);
}, [viewportId, rtDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
direction = direction === 'left' ? -1 : 1;
const segmentationId = rtDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
@@ -145,11 +140,7 @@ function OHIFCornerstoneRTViewport(props) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(
segmentationId,
newSelectedSegmentIndex,
toolGroupId
);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
@@ -162,32 +153,31 @@ function OHIFCornerstoneRTViewport(props) {
promptHydrateRT({
servicesManager,
viewportIndex,
viewportId,
rtDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateRTDisplaySet,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportIndex, rtDisplaySet, rtIsLoading]);
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
useEffect(() => {
// I'm not sure what is this, since in RT we support Overlapping segments
// via contours
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',
});
}
@@ -222,12 +212,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: [],
});
}
@@ -246,19 +234,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);
};
@@ -269,9 +251,7 @@ function OHIFCornerstoneRTViewport(props) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
toolGroupId
);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [rtDisplaySet]);
@@ -292,27 +272,14 @@ function OHIFCornerstoneRTViewport(props) {
return (
child &&
React.cloneElement(child, {
viewportIndex,
viewportId,
key: index,
})
);
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
const onStatusClick = useCallback(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
@@ -320,56 +287,57 @@ function OHIFCornerstoneRTViewport(props) {
// 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({
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
servicesManager,
viewportId,
});
setIsHydrated(isHydrated);
};
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onSegmentChange}
getStatusComponent={() => {
return _getStatusComponent({
isHydrated,
onStatusClick,
});
}}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: `RT Viewport ${SeriesDescription}`,
patientInformation: {
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`
: '',
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..."
@@ -384,7 +352,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,
@@ -6,45 +6,53 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
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" />;
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">
<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"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<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>
)}
</div>
);
{!isHydrated && (
<div
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}
>
{loadStr}
</div>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left">
<Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea />
</Tooltip>
)}
@@ -40,21 +40,15 @@ 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,
}),
// new MiniCssExtractPlugin({
// filename: `./dist/${outputName}.css`,
// chunkFilename: `./dist/${outputName}.css`,
// }),
new MiniCssExtractPlugin({
filename: `./dist/${outputName}.css`,
chunkFilename: `./dist/${outputName}.css`,
}),
],
});
};
File diff suppressed because it is too large. Load diff
@@ -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__'],
+10 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.7.0-beta.56",
"version": "3.8.0-beta.71",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -24,6 +24,8 @@
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-seg": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@@ -31,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.56",
"@ohif/extension-cornerstone": "3.7.0-beta.56",
"@ohif/extension-default": "3.7.0-beta.56",
"@ohif/i18n": "3.7.0-beta.56",
"@ohif/core": "3.8.0-beta.71",
"@ohif/extension-cornerstone": "3.8.0-beta.71",
"@ohif/extension-default": "3.8.0-beta.71",
"@ohif/i18n": "3.8.0-beta.71",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -44,6 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@kitware/vtk.js": "29.7.0",
"react-color": "^2.19.3"
}
}
@@ -0,0 +1,506 @@
import dcmjs from 'dcmjs';
import { createReportDialogPrompt } from '@ohif/extension-default';
import { ServicesManager, Types } from '@ohif/core';
import { cache, metaData } from '@cornerstonejs/core';
import {
segmentation as cornerstoneToolsSegmentation,
Enums as cornerstoneToolsEnums,
utilities,
} from '@cornerstonejs/tools';
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
import { classes, DicomMetadataStore } from '@ohif/core';
import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchingSquares';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
} from './utils/hydrationUtils';
const { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data;
const {
Cornerstone3D: {
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
},
} = adaptersSEG;
const {
Cornerstone3D: {
RTSS: { generateRTSSFromSegmentations },
},
} = adaptersRT;
const { downloadDICOMData } = helpers;
const commandsModule = ({
servicesManager,
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
uiNotificationService,
segmentationService,
uiDialogService,
displaySetService,
viewportGridService,
toolGroupService,
} = (servicesManager as ServicesManager).services;
const actions = {
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the ID of the viewport to be updated.
* @param params.servicesManager - The services manager
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
getUpdatedViewportsForSegmentation,
/**
* Creates an empty segmentation for a specified viewport.
* It first checks if the display set associated with the viewport is reconstructable.
* If not, it raises a notification error. Otherwise, it creates a new segmentation
* for the display set after handling the necessary steps for making the viewport
* a volume viewport first
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the target viewport ID.
*
*/
createEmptySegmentationForViewport: async ({ viewportId }) => {
const viewport = getTargetViewport({ viewportId, viewportGridService });
// Todo: add support for multiple display sets
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!displaySet.isReconstructable) {
uiNotificationService.show({
title: 'Segmentation',
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
type: 'error',
});
return;
}
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
loadFn: async () => {
const currentSegmentations = segmentationService.getSegmentations();
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
displaySetInstanceUID,
{ label: `Segmentation ${currentSegmentations.length + 1}` }
);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
// Add only one segment for now
segmentationService.addSegment(segmentationId, {
toolGroupId,
segmentIndex: 1,
properties: {
label: 'Segment 1',
},
});
return segmentationId;
},
});
},
/**
* Loads segmentations for a specified viewport.
* The function prepares the viewport for rendering, then loads the segmentation details.
* Additionally, if the segmentation has scalar data, it is set for the corresponding label map volume.
*
* @param {Object} params - Parameters for the function.
* @param params.segmentations - Array of segmentations to be loaded.
* @param params.viewportId - the target viewport ID.
*
*/
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
loadFn: async () => {
// Todo: handle adding more than one segmentation
const viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const segmentation = segmentations[0];
const segmentationId = segmentation.id;
const label = segmentation.label;
const segments = segmentation.segments;
delete segmentation.segments;
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
segmentationId,
label,
});
if (segmentation.scalarData) {
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
labelmapVolume.scalarData.set(segmentation.scalarData);
}
segmentationService.addOrUpdateSegmentation(segmentation);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
segments.forEach(segment => {
if (segment === null) {
return;
}
segmentationService.addSegment(segmentationId, {
segmentIndex: segment.segmentIndex,
toolGroupId,
properties: {
color: segment.color,
label: segment.label,
opacity: segment.opacity,
isLocked: segment.isLocked,
visibility: segment.isVisible,
active: segmentation.activeSegmentIndex === segment.segmentIndex,
},
});
});
if (segmentation.centroidsIJK) {
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
}
return segmentationId;
},
});
},
/**
* Loads segmentation display sets for a specified viewport.
* Depending on the modality of the display set (SEG or RTSTRUCT),
* it chooses the appropriate service function to create
* the segmentation for the display set.
* The function then prepares the viewport for rendering segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
* @param params.displaySets - Array of display sets to be loaded for segmentation.
*
*/
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
// Todo: handle adding more than one segmentation
const displaySet = displaySets[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
referencedDisplaySetInstanceUID: displaySet.referencedDisplaySetInstanceUID,
loadFn: async () => {
const segDisplaySet = displaySet;
const suppressEvents = false;
const serviceFunction =
segDisplaySet.Modality === 'SEG'
? 'createSegmentationForSEGDisplaySet'
: 'createSegmentationForRTDisplaySet';
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
const segmentation = segmentationService.getSegmentation(segmentationId);
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
return segmentationId;
},
});
},
/**
* Generates a segmentation from a given segmentation ID.
* This function retrieves the associated segmentation and
* its referenced volume, extracts label maps from the
* segmentation volume, and produces segmentation data
* alongside associated metadata.
*
* @param {Object} params - Parameters for the function.
* @param params.segmentationId - ID of the segmentation to be generated.
* @param params.options - Optional configuration for the generation process.
*
* @returns Returns the generated segmentation data.
*/
generateSegmentation: ({ segmentationId, options = {} }) => {
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
const segmentationVolume = cache.getVolume(segmentationId);
const referencedVolume = cache.getVolume(referencedVolumeId);
const referencedImages = referencedVolume.getCornerstoneImages();
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
// Generate fake metadata as an example
labelmapObj.metadata = [];
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
labelmapObj.segmentsOnLabelmap.forEach(segmentIndex => {
// segmentation service already has a color for each segment
const segment = segmentationInOHIF?.segments[segmentIndex];
const { label, color } = segment;
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
color.slice(0, 3).map(value => value / 255)
).map(value => Math.round(value));
const segmentMetadata = {
SegmentNumber: segmentIndex.toString(),
SegmentLabel: label,
SegmentAlgorithmType: 'MANUAL',
SegmentAlgorithmName: 'OHIF Brush',
RecommendedDisplayCIELabValue,
SegmentedPropertyCategoryCodeSequence: {
CodeValue: 'T-D0050',
CodingSchemeDesignator: 'SRT',
CodeMeaning: 'Tissue',
},
SegmentedPropertyTypeCodeSequence: {
CodeValue: 'T-D0050',
CodingSchemeDesignator: 'SRT',
CodeMeaning: 'Tissue',
},
};
labelmapObj.metadata[segmentIndex] = segmentMetadata;
});
const generatedSegmentation = generateSegmentation(
referencedImages,
labelmapObj,
metaData,
options
);
return generatedSegmentation;
},
/**
* Downloads a segmentation based on the provided segmentation ID.
* This function retrieves the associated segmentation and
* uses it to generate the corresponding DICOM dataset, which
* is then downloaded with an appropriate filename.
*
* @param {Object} params - Parameters for the function.
* @param params.segmentationId - ID of the segmentation to be downloaded.
*
*/
downloadSegmentation: ({ segmentationId }) => {
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
const generatedSegmentation = actions.generateSegmentation({
segmentationId,
});
downloadDICOMData(generatedSegmentation.dataset, `${segmentationInOHIF.label}`);
},
/**
* Stores a segmentation based on the provided segmentationId into a specified data source.
* The SeriesDescription is derived from user input or defaults to the segmentation label,
* and in its absence, defaults to 'Research Derived Series'.
*
* @param {Object} params - Parameters for the function.
* @param params.segmentationId - ID of the segmentation to be stored.
* @param params.dataSource - Data source where the generated segmentation will be stored.
*
* @returns {Object|void} Returns the naturalized report if successfully stored,
* otherwise throws an error.
*/
storeSegmentation: async ({ segmentationId, dataSource }) => {
const promptResult = await createReportDialogPrompt(uiDialogService, {
extensionManager,
});
if (promptResult.action !== 1 && promptResult.value) {
return;
}
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
throw new Error('No segmentation found');
}
const { label } = segmentation;
const SeriesDescription = promptResult.value || label || 'Research Derived Series';
const generatedData = actions.generateSegmentation({
segmentationId,
options: {
SeriesDescription,
},
});
if (!generatedData || !generatedData.dataset) {
throw new Error('Error during segmentation generation');
}
const { dataset: naturalizedReport } = generatedData;
await dataSource.store.dicom(naturalizedReport);
// The "Mode" route listens for DicomMetadataStore changes
// When a new instance is added, it listens and
// automatically calls makeDisplaySets
// add the information for where we stored it to the instance as well
naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot;
DicomMetadataStore.addInstances([naturalizedReport], true);
return naturalizedReport;
},
/**
* Converts segmentations into RTSS for download.
* This sample function retrieves all segentations and passes to
* cornerstone tool adapter to convert to DICOM RTSS format. It then
* converts dataset to downloadable blob.
*
*/
downloadRTSS: ({ segmentationId }) => {
const segmentations = segmentationService.getSegmentation(segmentationId);
const vtkUtils = {
vtkImageMarchingSquares,
vtkDataArray,
vtkImageData,
};
const RTSS = generateRTSSFromSegmentations(
segmentations,
classes.MetadataProvider,
DicomMetadataStore,
cache,
cornerstoneToolsEnums,
vtkUtils
);
try {
const reportBlob = datasetToBlob(RTSS);
//Create a URL for the binary.
const objectUrl = URL.createObjectURL(reportBlob);
window.location.assign(objectUrl);
} catch (e) {
console.warn(e);
}
},
setBrushSize: ({ value, toolNames }) => {
const brushSize = Number(value);
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
if (toolNames?.length === 0) {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
} else {
toolNames?.forEach(toolName => {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
});
}
});
},
setThresholdRange: ({
value,
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
}) => {
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
toolNames?.forEach(toolName => {
toolGroup.setToolConfiguration(toolName, {
strategySpecificConfiguration: {
THRESHOLD: {
threshold: value,
},
},
});
});
});
},
toggleThresholdRangeAndDynamic() {
const toolGroupIds = toolGroupService.getToolGroupIds();
if (!toolGroupIds) {
return;
}
toolGroupIds.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
const brushInstances = segmentationUtils.getBrushToolInstances(toolGroup.id);
brushInstances.forEach(({ configuration }) => {
const { activeStrategy, strategySpecificConfiguration } = configuration;
if (activeStrategy.startsWith('THRESHOLD')) {
const thresholdConfig = strategySpecificConfiguration.THRESHOLD;
if (thresholdConfig) {
thresholdConfig.isDynamic = !thresholdConfig.isDynamic;
}
}
});
});
},
};
const definitions = {
getUpdatedViewportsForSegmentation: {
commandFn: actions.getUpdatedViewportsForSegmentation,
},
loadSegmentationDisplaySetsForViewport: {
commandFn: actions.loadSegmentationDisplaySetsForViewport,
},
loadSegmentationsForViewport: {
commandFn: actions.loadSegmentationsForViewport,
},
createEmptySegmentationForViewport: {
commandFn: actions.createEmptySegmentationForViewport,
},
generateSegmentation: {
commandFn: actions.generateSegmentation,
},
downloadSegmentation: {
commandFn: actions.downloadSegmentation,
},
storeSegmentation: {
commandFn: actions.storeSegmentation,
},
downloadRTSS: {
commandFn: actions.downloadRTSS,
},
setBrushSize: {
commandFn: actions.setBrushSize,
},
setThresholdRange: {
commandFn: actions.setThresholdRange,
},
toggleThresholdRangeAndDynamic: {
commandFn: actions.toggleThresholdRangeAndDynamic,
},
};
return {
actions,
definitions,
defaultContext: 'SEGMENTATION',
};
};
export default commandsModule;
@@ -0,0 +1,88 @@
import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui';
import PanelSegmentation from './panels/PanelSegmentation';
import { SegmentationPanelMode } from './types/segmentation';
const getPanelModule = ({
commandsManager,
servicesManager,
extensionManager,
configuration,
title,
}) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = configuration => {
const [appConfig] = useAppConfig();
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
const segmentationPanelMode =
customizationService.get('segmentation.segmentationPanelMode')?.value ||
SegmentationPanelMode.Dropdown;
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
segmentationPanelMode: segmentationPanelMode,
}}
/>
);
};
const wrappedPanelSegmentationWithTools = configuration => {
const [appConfig] = useAppConfig();
const segmentationPanelMode =
customizationService.get('segmentation.segmentationPanelMode')?.value ||
SegmentationPanelMode.Dropdown;
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="segmentationToolbox"
title="Segmentation Tools"
configuration={{
...configuration,
}}
/>
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
segmentationPanelMode: segmentationPanelMode,
}}
/>
</>
);
};
return [
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
{
name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationWithTools,
},
];
};
export default getPanelModule;
@@ -1,10 +1,6 @@
import { utils } from '@ohif/core';
import {
metaData,
cache,
triggerEvent,
eventTarget,
} from '@cornerstonejs/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
@@ -14,11 +10,7 @@ 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 {
@@ -66,13 +58,13 @@ function _getDisplaySetsFromSeries(
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
if (!referencedSeriesSequence) {
throw new Error('ReferencedSeriesSequence is missing for the SEG');
console.error('ReferencedSeriesSequence is missing for the SEG');
return;
}
const referencedSeries = referencedSeriesSequence[0];
const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
displaySet.referencedImages =
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => {
@@ -87,8 +79,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;
@@ -121,10 +112,7 @@ 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
) {
if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
await _loadSegments({
extensionManager,
servicesManager,
@@ -149,28 +137,17 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
return loadPromises[SOPInstanceUID];
}
async function _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
}) {
async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers }) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
const { segmentationService } = servicesManager.services;
const { segmentationService, uiNotificationService } = servicesManager.services;
const { dicomLoaderService } = utilityModule.exports;
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(
segDisplaySet,
null,
headers
);
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
const cachedReferencedVolume = cache.getVolume(
segDisplaySet.referencedVolumeId
);
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
if (!cachedReferencedVolume) {
throw new Error(
@@ -186,12 +163,9 @@ async function _loadSegments({
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
{
percentComplete,
}
);
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete,
});
});
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
@@ -201,29 +175,51 @@ async function _loadSegments({
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
let usedRecommendedDisplayCIELabValue = true;
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
data.rgba = data.RecommendedDisplayCIELabValue;
if (data.rgba) {
data.rgba = dicomlabToRGB(data.rgba);
} else {
usedRecommendedDisplayCIELabValue = false;
data.rgba = CONSTANTS.COLOR_LUT[i % CONSTANTS.COLOR_LUT.length];
}
}
});
if (results.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message:
'Unsupported overlapping segments detected, segmentation rendering results may be incorrect.',
type: 'warning',
});
}
if (!usedRecommendedDisplayCIELabValue) {
// Display a notification about the non-utilization of RecommendedDisplayCIELabValue
uiNotificationService.show({
title: 'DICOM SEG import',
message:
'RecommendedDisplayCIELabValue not found for one or more segments. The default color was used instead.',
type: 'warning',
duration: 5000,
});
}
Object.assign(segDisplaySet, results);
}
function _segmentationExists(segDisplaySet, segmentationService) {
// This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation(
segDisplaySet.displaySetInstanceUID
);
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
);
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
@@ -0,0 +1,67 @@
export function getToolbarModule({ commandsManager, servicesManager }) {
const { segmentationService, toolGroupService } = servicesManager.services;
return [
{
name: 'evaluate.cornerstone.segmentation',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
// Todo: we need to pass in the button section Id since we are kind of
// forcing the button to have black background since initially
// it is designed for the toolbox not the toolbar on top
// we should then branch the buttonSectionId to have different styles
const segmentations = segmentationService.getSegmentations();
if (!segmentations?.length) {
return {
disabled: true,
className: '!text-common-bright !bg-black opacity-50',
disabledText: disabledText ?? 'No segmentations available',
};
}
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isPrimaryActive = toolNames
? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool())
: toolGroup.getActivePrimaryMouseButtonTool() === toolName;
return {
disabled: false,
className: isPrimaryActive
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
isActive: isPrimaryActive,
};
},
},
];
}
function getToolNameForButton(button) {
const { props } = button;
const commands = props?.commands || button.commands;
if (commands && commands.length) {
const command = commands[0];
const { commandOptions } = command;
const { toolName } = commandOptions || { toolName: props?.id ?? button.id };
return toolName;
}
return null;
}
+11 -39
View File
@@ -1,17 +1,15 @@
import { id } from './id';
import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import PanelSegmentation from './panels/PanelSegmentation';
import getHangingProtocolModule from './getHangingProtocolModule';
import hydrateSEGDisplaySet from './utils/_hydrateSEG';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule';
import { getToolbarModule } from './getToolbarModule';
import preRegistration from './init';
const Component = React.lazy(() => {
return import(
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport'
);
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
});
const OHIFCornerstoneSEGViewport = props => {
@@ -31,40 +29,17 @@ const extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
preRegistration,
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* 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[] => {
const wrappedPanelSegmentation = () => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
/>
);
};
return [
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
];
},
getViewportModule({ servicesManager, extensionManager }) {
getPanelModule,
getCommandsModule,
getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
const ExtendedOHIFCornerstoneSEGViewport = props => {
return (
<OHIFCornerstoneSEGViewport
@@ -76,9 +51,7 @@ const extension = {
);
};
return [
{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport },
];
return [{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport }];
},
/**
* SopClassHandlerModule should provide a list of sop class handlers that will be
@@ -91,4 +64,3 @@ const extension = {
};
export default extension;
export { hydrateSEGDisplaySet };
@@ -0,0 +1,5 @@
import { addTool, BrushTool } from '@cornerstonejs/tools';
export default function init({ servicesManager }): void {
addTool(BrushTool);
}
@@ -1,50 +1,33 @@
import { createReportAsync } from '@ohif/extension-default';
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable } from '@ohif/ui';
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
import { SegmentationPanelMode } from '../types/segmentation';
import callInputDialog from './callInputDialog';
import { useAppConfig } from '@state';
import callColorPickerDialog from './colorPickerDialog';
import { useTranslation } from 'react-i18next';
const components = {
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
};
export default function PanelSegmentation({
servicesManager,
commandsManager,
extensionManager,
configuration,
}) {
const { segmentationService, uiDialogService } = servicesManager.services;
const [appConfig] = useAppConfig();
const disableEditing = appConfig?.disableEditing;
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
segmentationService.getConfiguration()
);
const [segmentations, setSegmentations] = useState(() =>
segmentationService.getSegmentations()
);
const [isMinimized, setIsMinimized] = useState({});
const onToggleMinimizeSegmentation = useCallback(
id => {
setIsMinimized(prevState => ({
...prevState,
[id]: !prevState[id],
}));
},
[setIsMinimized]
);
// Only expand the last segmentation added to the list and collapse the rest
useEffect(() => {
const lastSegmentationId = segmentations[segmentations.length - 1]?.id;
if (lastSegmentationId) {
setIsMinimized(prevState => ({
...prevState,
[lastSegmentationId]: false,
}));
}
}, [segmentations, setIsMinimized]);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
useEffect(() => {
// ~~ Subscription
@@ -69,6 +52,16 @@ export default function PanelSegmentation({
};
}, []);
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
return toolGroupIds;
};
const onSegmentationAdd = async () => {
commandsManager.runCommand('createEmptySegmentationForViewport');
};
const onSegmentationClick = (segmentationId: string) => {
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
};
@@ -77,33 +70,19 @@ export default function PanelSegmentation({
segmentationService.remove(segmentationId);
};
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(
segmentationId
);
return toolGroupIds;
const onSegmentAdd = segmentationId => {
segmentationService.addSegment(segmentationId);
};
const onSegmentClick = (segmentationId, segmentIndex) => {
segmentationService.setActiveSegmentForSegmentation(
segmentationId,
segmentIndex
);
segmentationService.setActiveSegment(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);
});
};
@@ -118,11 +97,7 @@ export default function PanelSegmentation({
return;
}
segmentationService.setSegmentLabelForSegmentation(
segmentationId,
segmentIndex,
label
);
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
});
};
@@ -147,16 +122,34 @@ export default function PanelSegmentation({
};
const onSegmentColorClick = (segmentationId, segmentIndex) => {
// Todo: Implement color picker later
return;
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const { color, opacity } = segment;
const rgbaColor = {
r: color[0],
g: color[1],
b: color[2],
a: opacity / 255.0,
};
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
newRgbaColor.r,
newRgbaColor.g,
newRgbaColor.b,
newRgbaColor.a * 255.0,
]);
});
};
const onSegmentDelete = (segmentationId, segmentIndex) => {
// segmentationService.removeSegmentFromSegmentation(
// segmentationId,
// segmentIndex
// );
console.warn('not implemented yet');
segmentationService.removeSegment(segmentationId, segmentIndex);
};
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
@@ -176,8 +169,28 @@ export default function PanelSegmentation({
});
};
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
};
const onToggleSegmentationVisibility = segmentationId => {
segmentationService.toggleSegmentationVisibility(segmentationId);
const segmentation = segmentationService.getSegmentation(segmentationId);
const isVisible = segmentation.isVisible;
const segments = segmentation.segments;
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
segments.forEach((segment, segmentIndex) => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
});
};
const _setSegmentationConfiguration = useCallback(
@@ -190,80 +203,92 @@ export default function PanelSegmentation({
[segmentationService]
);
const onSegmentationDownload = segmentationId => {
commandsManager.runCommand('downloadSegmentation', {
segmentationId,
});
};
const storeSegmentation = async segmentationId => {
const datasources = extensionManager.getActiveDataSource();
const displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand('storeSegmentation', {
segmentationId,
dataSource: datasources[0],
}),
reportType: 'Segmentation',
});
// Show the exported report in the active viewport as read only (similar to SR)
if (displaySetInstanceUIDs) {
// clear the segmentation that we exported, similar to the storeMeasurement
// where we remove the measurements and prompt again the user if they would like
// to re-read the measurements in a SR read only viewport
segmentationService.remove(segmentationId);
viewportGridService.setDisplaySetsForViewport({
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs,
});
}
};
const onSegmentationDownloadRTSS = segmentationId => {
commandsManager.runCommand('downloadRTSS', {
segmentationId,
});
};
const SegmentationGroupTableComponent = components[configuration?.segmentationPanelMode];
return (
<div className="flex flex-col flex-auto min-h-0 justify-between mt-1">
{/* show segmentation table */}
{segmentations?.length ? (
<SegmentationGroupTable
title={t('Segmentations')}
showAddSegmentation={false}
segmentations={segmentations}
isMinimized={isMinimized}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
disableEditing={disableEditing}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'renderOutline',
value
)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'outlineOpacity',
value
)
}
setRenderFill={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'renderFill',
value
)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'renderInactiveSegmentations',
value
)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'outlineWidthActive',
value
)
}
setFillAlpha={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'fillAlpha',
value
)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'fillAlphaInactive',
value
)
}
/>
) : null}
</div>
<SegmentationGroupTableComponent
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAdd}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onSegmentAdd={onSegmentAdd}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
showDeleteSegment={true}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
}
setRenderFill={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
}
setFillAlpha={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
/>
);
}
@@ -271,7 +296,6 @@ PanelSegmentation.propTypes = {
commandsManager: PropTypes.shape({
runCommand: PropTypes.func.isRequired,
}),
appConfig: PropTypes.object.isRequired,
servicesManager: PropTypes.shape({
services: PropTypes.shape({
segmentationService: PropTypes.shape({
@@ -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 => {
@@ -0,0 +1,3 @@
.chrome-picker {
background: #090c29 !important;
}
@@ -0,0 +1,58 @@
import React from 'react';
import { Dialog } from '@ohif/ui';
import { ChromePicker } from 'react-color';
import './colorPickerDialog.css';
function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
const dialogId = 'pick-color';
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.rgbaColor, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
};
if (uiDialogService) {
uiDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: 'Segment Color',
value: { rgbaColor },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
const handleChange = color => {
setValue({ rgbaColor: color.rgb });
};
return (
<ChromePicker
color={value.rgbaColor}
onChange={handleChange}
presetColors={[]}
width={300}
/>
);
},
},
});
}
}
export default callColorPickerDialog;
@@ -0,0 +1,4 @@
export enum SegmentationPanelMode {
Expanded = 'expanded',
Dropdown = 'dropdown',
}
@@ -1,69 +0,0 @@
async function _hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
servicesManager,
}) {
const {
segmentationService,
hangingProtocolService,
viewportGridService,
} = servicesManager.services;
const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
let segmentationId = null;
// We need the hydration to notify panels about the new segmentation added
const suppressEvents = false;
segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
segDisplaySet,
segmentationId,
suppressEvents
);
segmentationService.hydrateSegmentation(segDisplaySet.displaySetInstanceUID);
const { viewports } = viewportGridService.getState();
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex,
displaySetInstanceUID
);
// Todo: fix this after we have a better way for stack viewport segmentations
// check every viewport in the viewports to see if the displaySetInstanceUID
// 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) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
segDisplaySet.displaySetInstanceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportIndex: index,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
initialImageOptions: {
preset: 'middle',
},
},
});
}
});
// Do the entire update at once
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
export default _hydrateSEGDisplaySet;
@@ -6,9 +6,7 @@ import dcmjs from 'dcmjs';
* @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)
);
const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x => Math.round(x * 255));
return rgb;
}
@@ -0,0 +1,191 @@
import { Enums, cache } from '@cornerstonejs/core';
/**
* Updates the viewports in preparation for rendering segmentations.
* Evaluates each viewport to determine which need modifications,
* then for those viewports, changes them to a volume type and ensures
* they are ready for segmentation rendering.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport to be updated.
* @param params.loadFn - Function to load the segmentation data.
* @param params.servicesManager - The services manager.
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns Returns true upon successful update of viewports for segmentation rendering.
*/
async function updateViewportsForSegmentationRendering({
viewportId,
loadFn,
servicesManager,
referencedDisplaySetInstanceUID,
}: {
viewportId: string;
loadFn: () => Promise<string>;
servicesManager: any;
referencedDisplaySetInstanceUID?: string;
}) {
const { cornerstoneViewportService, segmentationService, viewportGridService } =
servicesManager.services;
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
referencedDisplaySetInstanceUID =
referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
const updatedViewports = getUpdatedViewportsForSegmentation({
servicesManager,
viewportId,
referencedDisplaySetInstanceUID,
});
// create Segmentation callback which needs to be waited until
// the volume is created (if coming from stack)
const createSegmentationForVolume = async () => {
const segmentationId = await loadFn();
segmentationService.hydrateSegmentation(segmentationId);
};
// the reference volume that is used to draw the segmentation. so check if the
// volume exists in the cache (the target Viewport is already a volume viewport)
const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId =>
volumeId.includes(referencedDisplaySetInstanceUID)
);
updatedViewports.forEach(async viewport => {
viewport.viewportOptions = {
...viewport.viewportOptions,
viewportType: 'volume',
needsRerendering: true,
};
const viewportId = viewport.viewportId;
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const prevCamera = csViewport.getCamera();
// only run the createSegmentationForVolume for the targetViewportId
// since the rest will get handled by cornerstoneViewportService
if (volumeExists && viewportId === targetViewportId) {
await createSegmentationForVolume();
return;
}
const createNewSegmentationWhenVolumeMounts = async evt => {
const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac =>
ac.uid.includes(referencedDisplaySetInstanceUID)
);
// Note: make sure to re-grab the viewport since it might have changed
// during the time it took for the volume to be mounted, for instance
// the stack viewport has been changed to a volume viewport
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
volumeViewport.setCamera(prevCamera);
volumeViewport.element.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
if (!isTheActiveViewportVolumeMounted) {
// it means it is one of those other updated viewports so just update the camera
return;
}
if (viewportId === targetViewportId) {
await createSegmentationForVolume();
}
};
csViewport.element.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
});
// Set the displaySets for the viewports that require to be updated
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the ID of the viewport to be updated.
* @param params.servicesManager - The services manager
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
function getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
referencedDisplaySetInstanceUID,
}) {
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
servicesManager.services;
const { viewports } = viewportGridService.getState();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
const referenceDisplaySetInstanceUID =
referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
targetViewportId,
referenceDisplaySetInstanceUID
);
viewports.forEach((viewport, viewportId) => {
if (
targetViewportId === viewportId ||
updatedViewports.find(v => v.viewportId === viewportId)
) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
segmentationFrameOfReferenceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
viewportType: 'volume',
needsRerendering: true,
},
});
}
});
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
}
export {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
};
@@ -1,12 +1,7 @@
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, {});
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
export default createSEGToolGroupAndAddTools;
@@ -1,5 +1,4 @@
import { ButtonEnums } from '@ohif/ui';
import hydrateSEGDisplaySet from './_hydrateSEG';
const RESPONSE = {
NO_NEVER: -1,
@@ -10,16 +9,14 @@ const RESPONSE = {
function promptHydrateSEG({
servicesManager,
segDisplaySet,
viewportIndex,
viewportId,
preHydrateCallbacks,
hydrateSEGDisplaySet,
}) {
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 => {
@@ -28,8 +25,7 @@ function promptHydrateSEG({
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
servicesManager,
viewportId,
});
resolve(isHydrated);
@@ -37,8 +33,8 @@ 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 = [
{
@@ -58,7 +54,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
};
uiViewportDialogService.show({
viewportIndex,
viewportId,
type: 'info',
message,
actions,
@@ -1,18 +1,11 @@
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, ViewportActionArrows } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import hydrateSEGDisplaySet from '../utils/_hydrateSEG';
import _getStatusComponent from './_getStatusComponent';
const { formatDate } = utils;
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
function OHIFCornerstoneSEGViewport(props) {
@@ -20,14 +13,13 @@ function OHIFCornerstoneSEGViewport(props) {
children,
displaySets,
viewportOptions,
viewportIndex,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { t } = useTranslation('SEGViewport');
const viewportId = viewportOptions.viewportId;
const {
displaySetService,
@@ -35,9 +27,10 @@ function OHIFCornerstoneSEGViewport(props) {
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = 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) {
@@ -67,7 +60,7 @@ function OHIFCornerstoneSEGViewport(props) {
// refs
const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportIndex } = viewportGrid;
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
@@ -94,9 +87,9 @@ function OHIFCornerstoneSEGViewport(props) {
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportIndex }) => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportIndex,
viewportId,
});
});
}, [viewportGrid]);
@@ -106,9 +99,7 @@ function OHIFCornerstoneSEGViewport(props) {
'@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 (
@@ -123,14 +114,12 @@ function OHIFCornerstoneSEGViewport(props) {
}}
onElementEnabled={onElementEnabled}
onElementDisabled={onElementDisabled}
// initialImageIndex={initialImageIndex}
></Component>
);
}, [viewportIndex, segDisplaySet, toolGroupId]);
}, [viewportId, segDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
direction = direction === 'left' ? -1 : 1;
const segmentationId = segDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
@@ -148,11 +137,7 @@ function OHIFCornerstoneSEGViewport(props) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(
segmentationId,
newSelectedSegmentIndex,
toolGroupId
);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
@@ -165,35 +150,24 @@ function OHIFCornerstoneSEGViewport(props) {
promptHydrateSEG({
servicesManager,
viewportIndex,
viewportId,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateSEGDisplaySet,
}).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',
type: 'warning',
});
}
}
);
@@ -225,12 +199,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: [],
});
}
@@ -251,17 +223,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
);
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);
@@ -273,13 +239,74 @@ function OHIFCornerstoneSEGViewport(props) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
toolGroupId
);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [segDisplaySet]);
const hydrateSEGDisplaySet = useCallback(
({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
},
[commandsManager]
);
const onStatusClick = useCallback(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,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -296,7 +323,7 @@ function OHIFCornerstoneSEGViewport(props) {
return (
child &&
React.cloneElement(child, {
viewportIndex,
viewportId,
key: index,
})
);
@@ -315,65 +342,12 @@ function OHIFCornerstoneSEGViewport(props) {
SpacingBetweenSlices,
} = 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,
servicesManager,
});
setIsHydrated(isHydrated);
};
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onSegmentChange}
getStatusComponent={() => {
return _getStatusComponent({
isHydrated,
onStatusClick,
});
}}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
seriesDescription: `SEG Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness
? utils.roundNumber(SliceThickness, 2)
: '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing:
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..."
@@ -388,7 +362,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,
@@ -399,12 +373,18 @@ OHIFCornerstoneSEGViewport.defaultProps = {
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const {
SharedFunctionalGroupsSequence: [SharedFunctionalGroup],
} = segDisplaySet.instance;
const {
PixelMeasuresSequence: [PixelMeasures],
} = SharedFunctionalGroup;
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
? SharedFunctionalGroupsSequence[0]
: SharedFunctionalGroupsSequence;
const { PixelMeasuresSequence } = SharedFunctionalGroup;
const PixelMeasures = Array.isArray(PixelMeasuresSequence)
? PixelMeasuresSequence[0]
: PixelMeasuresSequence;
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
const image0 = referencedDisplaySet.images[0];
@@ -6,45 +6,53 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
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" />;
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">
<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"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<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>
)}
</div>
);
{!isHydrated && (
<div
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}
>
{loadStr}
</div>
)}
</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,
File diff suppressed because it is too large. Load diff
+11 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.7.0-beta.56",
"version": "3.8.0-beta.71",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -23,6 +23,8 @@
"ohif-extension"
],
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@@ -32,11 +34,11 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.56",
"@ohif/extension-cornerstone": "3.7.0-beta.56",
"@ohif/extension-measurement-tracking": "3.7.0-beta.56",
"@ohif/ui": "3.7.0-beta.56",
"dcmjs": "^0.29.5",
"@ohif/core": "3.8.0-beta.71",
"@ohif/extension-cornerstone": "3.8.0-beta.71",
"@ohif/extension-measurement-tracking": "3.8.0-beta.71",
"@ohif/ui": "3.8.0-beta.71",
"dcmjs": "^0.29.12",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -44,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.10.3",
"@cornerstonejs/core": "^1.10.3",
"@cornerstonejs/tools": "^1.10.3",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/tools": "^1.67.0",
"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
@@ -44,7 +40,9 @@ const _generateReport = (
return dataset;
};
const commandsModule = ({}) => {
const commandsModule = props => {
const { servicesManager } = props;
const { customizationService } = servicesManager.services;
const actions = {
/**
*
@@ -54,16 +52,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,32 +81,31 @@ 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');
}
await dataSource.store.dicom(naturalizedReport);
const onBeforeDicomStore =
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
let dicomDict;
if (typeof onBeforeDicomStore === 'function') {
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
}
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
if (StudyInstanceUID) {
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
@@ -130,12 +119,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.');
}
},
};
@@ -143,13 +128,9 @@ const commandsModule = ({}) => {
const definitions = {
downloadReport: {
commandFn: actions.downloadReport,
storeContexts: [],
options: {},
},
storeMeasurements: {
commandFn: actions.storeMeasurements,
storeContexts: [],
options: {},
},
};
@@ -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',
@@ -187,33 +174,34 @@ function _load(displaySet, servicesManager, extensionManager) {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
activeDisplaySet,
dataSource
dataSource,
servicesManager
);
});
// 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,
servicesManager
);
});
});
}
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
dataSource
dataSource,
servicesManager
) {
const { customizationService } = servicesManager.services;
let unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
@@ -228,7 +216,11 @@ function _checkIfCanAddMeasurementsToDisplaySet(
return;
}
const { sopClassUids, images } = newDisplaySet;
if (newDisplaySet.unsupported) {
return;
}
const { sopClassUids } = newDisplaySet;
// Check if any have the newDisplaySet is the correct SOPClass.
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
@@ -248,8 +240,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 +248,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
});
});
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(
newDisplaySet
);
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
for (const imageId of imageIdsForDisplaySet) {
if (!unloadedMeasurements.length) {
@@ -267,25 +256,26 @@ 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(
let measurement = unloadedMeasurements[j];
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
'onBeforeSRAddMeasurement'
)?.value;
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
SOPInstanceUID,
frameNumber
)
) {
addMeasurement(
measurement,
imageId,
newDisplaySet.displaySetInstanceUID
);
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
unloadedMeasurements.splice(j, 1);
}
@@ -294,11 +284,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
@@ -324,11 +310,7 @@ function _measurementReferencesSOPInstanceUID(
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
);
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
@@ -343,30 +325,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);
@@ -379,15 +353,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 =>
@@ -396,22 +366,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.
@@ -420,9 +384,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
item.ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
) {
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
].push(item);
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier].push(item);
}
});
}
@@ -447,18 +409,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) {
@@ -468,9 +424,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,
@@ -485,10 +439,7 @@ function _processTID1410Measurement(mergedContentSequence) {
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
)
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
);
}
});
@@ -497,32 +448,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 = {
@@ -538,8 +479,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,
@@ -554,8 +494,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
);
if (cornerstoneFreeTextFindingSite) {
@@ -567,18 +506,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;
}
@@ -591,10 +524,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
)
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
);
}
});
@@ -634,17 +564,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,
@@ -654,17 +579,11 @@ 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 = [];
@@ -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 => {
@@ -58,7 +54,7 @@ const dicomSRExtension = {
},
getCommandsModule,
getSopClassHandlerModule,
// Include dynmically computed values such as toolNames not known till instantiation
// Include dynamically computed values such as toolNames not known till instantiation
getUtilityModule({ servicesManager }) {
return [
{
+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,9 +282,7 @@ 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 = utilities.math.ellipse.getCanvasEllipseCorners([
@@ -366,23 +337,16 @@ 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
);
if (!annotation.data?.handles?.textBox?.worldPosition) {
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)
);
});
@@ -61,7 +52,7 @@ export default function addMeasurement(
data: {
label: measurement.labels,
handles: {
textBox: {},
textBox: measurement.textBox ?? {},
},
cachedStats: {
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
@@ -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,13 +3,8 @@ 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
);
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } = measurement;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!referencedDisplaySet.images) {
return;
}
@@ -20,10 +15,7 @@ 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) {
@@ -58,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,
@@ -14,8 +14,7 @@ const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
}
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;
@@ -50,7 +44,7 @@ const isStructuredReportSeries = series => {
};
/**
* Checkes if series1 is newer than series2
* Checks if series1 is newer than series2
*
* @param {Object} series1 - Series Metadata 1
* @param {Object} series2 - Series Metadata 2
@@ -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;
@@ -49,19 +49,10 @@ export default function hydrateStructuredReport(
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(
@@ -99,7 +90,7 @@ export default function hydrateStructuredReport(
const datasetToUse = _mapLegacyDataSet(instance);
// Use dcmjs to generate toolState.
const storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse,
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
// for the imageToWorld transformation. The following assumes that the order
@@ -110,6 +101,16 @@ export default function hydrateStructuredReport(
metaData
);
const onBeforeSRHydration =
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
if (typeof onBeforeSRHydration === 'function') {
storedMeasurementByAnnotationType = onBeforeSRHydration({
storedMeasurementByAnnotationType,
displaySet,
});
}
// Filter what is found by DICOM SR to measurements we support.
const mappingDefinitions = mappings.map(m => m.annotationType);
const hydratableMeasurementsInSR = {};
@@ -125,16 +126,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];
@@ -150,10 +149,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);
@@ -162,23 +158,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];
@@ -208,19 +200,11 @@ 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);
const newAnnotationUID = measurementService.addRawMeasurement(
source,
@@ -231,9 +215,7 @@ export default function hydrateStructuredReport(
);
if (disableEditing) {
const addedAnnotation = annotationManager.getAnnotation(
newAnnotationUID
);
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
locking.setAnnotationLocked(addedAnnotation, true);
}
@@ -262,15 +244,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 = [];
@@ -280,13 +261,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;
@@ -304,7 +282,7 @@ function _mapLegacyDataSet(dataset) {
return dataset;
}
const toArray = function(x) {
const toArray = function (x) {
return Array.isArray(x) ? x : [x];
};
@@ -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');
@@ -1,32 +1,21 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
import { ServicesManager, ExtensionManager } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } 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';
function OHIFCornerstoneSRViewport(props) {
const {
children,
dataSource,
displaySets,
viewportIndex,
viewportLabel,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const [appConfig] = useAppConfig();
@@ -34,8 +23,11 @@ function OHIFCornerstoneSRViewport(props) {
displaySetService,
cornerstoneViewportService,
measurementService,
viewportActionCornersService,
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
// SR viewport will always have a single display set
if (displaySets.length > 1) {
throw new Error('SR viewport should only have a single display set');
@@ -46,15 +38,12 @@ 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;
const { t } = useTranslation('Common');
// Optional hook into tracking extension, if present.
let trackedMeasurements;
@@ -82,13 +71,11 @@ function OHIFCornerstoneSRViewport(props) {
{ 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],
},
]);
@@ -126,11 +113,7 @@ function OHIFCornerstoneSRViewport(props) {
const updateViewport = useCallback(
newMeasurementSelected => {
const {
StudyInstanceUID,
displaySetInstanceUID,
sopClassUids,
} = srDisplaySet;
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) {
return;
@@ -139,9 +122,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(
@@ -163,19 +144,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);
@@ -183,7 +156,7 @@ function OHIFCornerstoneSRViewport(props) {
}
});
},
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportIndex]
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
);
const getCornerstoneViewport = useCallback(() => {
@@ -231,35 +204,23 @@ function OHIFCornerstoneSRViewport(props) {
isJumpToMeasurementDisabled={true}
></Component>
);
}, [activeImageDisplaySetData, viewportIndex, measurementSelected]);
}, [activeImageDisplaySetData, viewportId, measurementSelected]);
const onMeasurementChange = useCallback(
direction => {
let newMeasurementSelected = measurementSelected;
if (direction === 'right') {
newMeasurementSelected++;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
}
} else {
newMeasurementSelected--;
if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
newMeasurementSelected += direction;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
} else if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
setTrackingIdentifiers(newMeasurementSelected);
updateViewport(newMeasurementSelected);
},
[
measurementSelected,
measurementCount,
updateViewport,
setTrackingIdentifiers,
]
[measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers]
);
/**
@@ -269,12 +230,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.get(activeViewportId);
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex,
viewportId: activeViewportId,
displaySetInstanceUIDs: [],
});
}
@@ -329,6 +288,46 @@ function OHIFCornerstoneSRViewport(props) {
updateViewport(measurementSelected);
}, [dataSource, srDisplaySet]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
srDisplaySet,
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
index: 0,
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onMeasurementChange}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
isLocked,
onMeasurementChange,
sendTrackedMeasurementsEvent,
srDisplaySet,
t,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -341,69 +340,16 @@ function OHIFCornerstoneSRViewport(props) {
return (
child &&
React.cloneElement(child, {
viewportIndex,
viewportId,
key: index,
})
);
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetMetadata;
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onMeasurementChange}
getStatusComponent={() =>
_getStatusComponent({
srDisplaySet,
viewportIndex,
isTracked: false,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
})
}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: SeriesDescription || '',
patientInformation: {
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`
: '',
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>
@@ -413,13 +359,12 @@ 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,
customProps: PropTypes.object,
viewportOptions: PropTypes.object,
viewportLabel: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
@@ -438,9 +383,7 @@ async function _getViewportReferencedDisplaySetData(
const { displaySetInstanceUID } = measurement;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
@@ -462,26 +405,25 @@ async function _getViewportReferencedDisplaySetData(
function _getStatusComponent({
srDisplaySet,
viewportIndex,
viewportId,
isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
}) {
const handleMouseUp = () => {
sendTrackedMeasurementsEvent('HYDRATE_SR', {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportIndex,
viewportId,
});
};
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
// 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;
@@ -511,22 +453,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}
>
@@ -539,7 +484,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,
File diff suppressed because it is too large. Load diff
+13 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.7.0-beta.56",
"version": "3.8.0-beta.71",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -25,6 +25,8 @@
"access": "public"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js",
@@ -36,10 +38,11 @@
"@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.10.3",
"@ohif/core": "3.7.0-beta.56",
"@ohif/ui": "3.7.0-beta.56",
"dcmjs": "^0.29.6",
"@cornerstonejs/dicom-image-loader": "^1.67.0",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.8.0-beta.71",
"@ohif/ui": "3.8.0-beta.71",
"dcmjs": "^0.29.12",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -52,11 +55,11 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.10.3",
"@cornerstonejs/core": "^1.10.3",
"@cornerstonejs/streaming-image-volume-loader": "^1.10.3",
"@cornerstonejs/tools": "^1.10.3",
"@kitware/vtk.js": "27.3.1",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/streaming-image-volume-loader": "^1.67.0",
"@cornerstonejs/tools": "^1.67.0",
"@kitware/vtk.js": "29.7.0",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
"lodash.merge": "^4.6.2",
@@ -10,11 +10,8 @@ import {
utilities as csUtils,
} from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import { Notification, useViewportDialog } from '@ohif/ui';
import {
IStackViewport,
IVolumeViewport,
} from '@cornerstonejs/core/dist/esm/types';
import { Notification, useViewportDialog, Types as UITypes, AllInOneMenu } from '@ohif/ui';
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
import { setEnabledElement } from '../state';
@@ -25,6 +22,12 @@ import CornerstoneServices from '../types/CornerstoneServices';
import CinePlayer from '../components/CinePlayer';
import { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { useAppConfig } from '@state';
import { LutPresentation, PositionPresentation } from '../types/Presentation';
const STACK = 'stack';
/**
@@ -42,24 +45,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;
}
@@ -75,8 +73,7 @@ function areEqual(prevProps, nextProps) {
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID ===
prevDisplaySet.displaySetInstanceUID
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
@@ -91,9 +88,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;
}
}
@@ -107,13 +102,11 @@ 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,
@@ -121,11 +114,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIndex,
onReady,
} = props;
const viewportId = viewportOptions.viewportId;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef();
const [appConfig] = useAppConfig();
const {
measurementService,
@@ -137,12 +133,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
cornerstoneCacheService,
viewportGridService,
stateSyncService,
viewportActionCornersService,
customizationService,
} = servicesManager.services as CornerstoneServices;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
@@ -154,27 +152,19 @@ const OHIFCornerstoneViewport = React.memo(props => {
}
}, [elementRef]);
const cleanUpServices = useCallback(() => {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const cleanUpServices = useCallback(
viewportInfo => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
if (!viewportInfo) {
return;
}
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
const viewportId = viewportInfo.getViewportId();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
syncGroups
);
}, [viewportIndex, viewportOptions.viewportId]);
viewportActionCornersService.clear(viewportId);
},
[viewportId]
);
const elementEnabledHandler = useCallback(
evt => {
@@ -184,73 +174,61 @@ 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);
const synchronizersStore = stateSyncService.getState().synchronizersStore;
if (synchronizersStore?.[viewportId]?.length) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
}
if (onElementEnabled) {
onElementEnabled(evt);
onReady?.(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 () => {
commandsManager.runCommand('storePresentation', {
viewportIndex,
});
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
cleanUpServices();
if (!viewportInfo) {
return;
}
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
cornerstoneViewportService.storePresentation({ viewportId });
cornerstoneViewportService.disableElement(viewportIndex);
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
if (onElementDisabled) {
onElementDisabled(viewportInfo);
}
eventTarget.removeEventListener(
Enums.Events.ELEMENT_ENABLED,
elementEnabledHandler
);
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
}, []);
@@ -273,9 +251,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
return;
}
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
@@ -287,18 +263,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
@@ -317,27 +289,35 @@ 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 state = stateSyncService.getState();
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
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,
@@ -370,7 +350,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
measurementService,
displaySetService,
elementRef,
viewportIndex,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
@@ -380,7 +360,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
measurementService,
displaySetService,
elementRef,
viewportIndex,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
@@ -389,14 +369,40 @@ const OHIFCornerstoneViewport = React.memo(props => {
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportIndex]);
}, [displaySets, elementRef, viewportId]);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
const wlActionMenu = getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
});
viewportActionCornersService.setComponent({
viewportId,
id: 'windowLevelActionMenu',
component: wlActionMenu,
location: viewportActionCornersService.LOCATIONS.topRight,
indexPriority: -100,
});
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
return (
<React.Fragment>
<div className="viewport-wrapper">
<ReactResizeDetector
refreshMode="debounce"
refreshRate={50} // Wait 50 ms after last move to render
onResize={onResize}
targetRef={elementRef.current}
/>
@@ -408,7 +414,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
ref={elementRef}
></div>
<CornerstoneOverlays
viewportIndex={viewportIndex}
viewportId={viewportId}
toolBarService={toolbarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
@@ -416,12 +422,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
/>
<CinePlayer
enabledVPElement={enabledVPElement}
viewportIndex={viewportIndex}
viewportId={viewportId}
servicesManager={servicesManager}
/>
</div>
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
message={viewportDialogState.message}
@@ -432,6 +439,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
}, areEqual);
@@ -440,37 +449,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;
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,
@@ -487,7 +494,7 @@ function _checkForCachedJumpToMeasurementEvents(
measurementService,
displaySetService,
elementRef,
viewportIndex,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
@@ -499,9 +506,7 @@ function _checkForCachedJumpToMeasurementEvents(
cacheJumpToMeasurementEvent = null;
return;
}
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
);
const displaysUIDs = displaySets.map(displaySet => displaySet.displaySetInstanceUID);
if (!displaysUIDs?.length) {
return;
}
@@ -513,7 +518,7 @@ function _checkForCachedJumpToMeasurementEvents(
_jumpToMeasurement(
measurement,
elementRef,
viewportIndex,
viewportId,
measurementService,
displaySetService,
viewportGridService,
@@ -526,7 +531,7 @@ function _checkForCachedJumpToMeasurementEvents(
function _jumpToMeasurement(
measurement,
targetElementRef,
viewportIndex,
viewportId,
measurementService,
displaySetService,
viewportGridService,
@@ -540,26 +545,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;
@@ -567,14 +565,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
@@ -589,10 +582,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;
}
@@ -613,6 +603,58 @@ function _jumpToMeasurement(
}
}
function _rehydrateSynchronizers(
synchronizersStore: { [key: string]: unknown },
viewportId: string,
syncGroupService: any
) {
synchronizersStore[viewportId].forEach(synchronizerObj => {
if (!synchronizerObj.id) {
return;
}
const { id, sourceViewports, targetViewports } = synchronizerObj;
const synchronizer = syncGroupService.getSynchronizer(id);
if (!synchronizer) {
return;
}
const sourceViewportInfo = sourceViewports.find(
sourceViewport => sourceViewport.viewportId === viewportId
);
const targetViewportInfo = targetViewports.find(
targetViewport => targetViewport.viewportId === viewportId
);
const isSourceViewportInSynchronizer = synchronizer
.getSourceViewports()
.find(sourceViewport => sourceViewport.viewportId === viewportId);
const isTargetViewportInSynchronizer = synchronizer
.getTargetViewports()
.find(targetViewport => targetViewport.viewportId === viewportId);
// if the viewport was previously a source viewport, add it again
if (sourceViewportInfo && !isSourceViewportInSynchronizer) {
synchronizer.addSource({
viewportId: sourceViewportInfo.viewportId,
renderingEngineId: sourceViewportInfo.renderingEngineId,
});
}
// if the viewport was previously a target viewport, add it again
if (targetViewportInfo && !isTargetViewportInSynchronizer) {
synchronizer.addTarget({
viewportId: targetViewportInfo.viewportId,
renderingEngineId: targetViewportInfo.renderingEngineId,
});
}
});
}
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
@@ -621,7 +663,6 @@ OHIFCornerstoneViewport.defaultProps = {
};
OHIFCornerstoneViewport.propTypes = {
viewportIndex: PropTypes.number.isRequired,
displaySets: PropTypes.array.isRequired,
dataSource: PropTypes.object.isRequired,
viewportOptions: PropTypes.object,
@@ -632,10 +673,7 @@ OHIFCornerstoneViewport.propTypes = {
// 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,16 +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;
@@ -48,7 +46,7 @@ function CornerstoneOverlays(props) {
return (
<div className="noselect">
<ViewportImageScrollbar
viewportIndex={viewportIndex}
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
@@ -60,7 +58,7 @@ function CornerstoneOverlays(props) {
<CustomizableViewportOverlay
imageSliceData={imageSliceData}
viewportData={viewportData}
viewportIndex={viewportIndex}
viewportId={viewportId}
servicesManager={servicesManager}
element={element}
/>
@@ -75,7 +73,7 @@ function CornerstoneOverlays(props) {
element={element}
viewportData={viewportData}
servicesManager={servicesManager}
viewportIndex={viewportIndex}
viewportId={viewportId}
/>
</div>
);
@@ -2,27 +2,25 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
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 { InstanceMetadata } from 'platform/core/src/types';
import { ServicesManager } from '@ohif/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import { ViewportOverlay } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
import { InstanceMetadata } from '@ohif/core/src/types';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import './CustomizableViewportOverlay.css';
const EPSILON = 1e-4;
type ViewportData = StackViewportData | VolumeViewportData;
interface OverlayItemProps {
element: any;
viewportData: any;
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
viewportIndex: number | null;
servicesManager: ServicesManager;
viewportId: string;
instance: InstanceMetadata;
customization: any;
formatters: {
@@ -41,71 +39,11 @@ interface OverlayItemProps {
scale?: number;
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
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="mr-1 shrink-0">L:</span>
<span className="ml-1 font-light shrink-0">
{windowCenter.toFixed(0)}
</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">I:</span>
<span className="font-light">
{instanceNumber !== undefined && instanceNumber !== null
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}
const OverlayItemComponents = {
'ohif.overlayItem.windowLevel': VOIOverlayItem,
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
/**
* Customizable Viewport Overlay
@@ -114,17 +52,19 @@ function CustomizableViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
viewportId,
servicesManager,
}: {
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
viewportId: string;
servicesManager: ServicesManager;
}) {
const {
toolbarService,
cornerstoneViewportService,
customizationService,
} = servicesManager.services;
const { cornerstoneViewportService, customizationService, toolGroupService } =
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
const { imageIndex } = imageSliceData;
const topLeftCustomization = customizationService.getModeCustomization(
@@ -140,32 +80,21 @@ function CustomizableViewportOverlay({
'cornerstoneOverlayBottomRight'
);
const instance = useMemo(() => {
const instances = useMemo(() => {
if (viewportData != null) {
return _getViewportInstance(viewportData, imageIndex);
return _getViewportInstances(viewportData);
} else {
return null;
}
}, [viewportData, imageIndex]);
const instanceNumber = useMemo(() => {
if (viewportData != null) {
return _getInstanceNumber(
viewportData,
viewportIndex,
imageIndex,
cornerstoneViewportService
);
}
return null;
}, [viewportData, viewportIndex, imageIndex, cornerstoneViewportService]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
const instanceNumber = useMemo(
() =>
viewportData
? getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService)
: null,
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
);
/**
* Updating the VOI when the viewport changes its voi
@@ -179,10 +108,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 +118,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 +131,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 +150,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,53 +160,38 @@ function CustomizableViewportOverlay({
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportIndex, viewportData, cornerstoneViewportService, element]);
/**
* Updating the active tools when the toolbar changes
*/
// Todo: this should act on the toolGroups instead of the toolbar state
useEffect(() => {
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
unsubscribe();
};
}, [toolbarService]);
}, [viewportId, viewportData, cornerstoneViewportService, element]);
const _renderOverlayItem = useCallback(
item => {
const overlayItemProps: OverlayItemProps = {
const overlayItemProps = {
element,
viewportData,
imageSliceData,
viewportIndex,
viewportId,
servicesManager,
customization: item,
formatters: {
formatPN: formatPN,
formatPN,
formatDate: formatDICOMDate,
formatTime: formatDICOMTime,
formatNumberPrecision: formatNumberPrecision,
formatNumberPrecision,
},
instance,
// calculated
instance: instances ? instances[item?.instanceIndex] : null,
voi,
scale,
instanceNumber,
};
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
return <VOIOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
return <ZoomOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
return <InstanceNumberOverlayItem {...overlayItemProps} />;
if (!item) {
return null;
}
const { customizationType } = item;
const OverlayItemComponent = OverlayItemComponents[customizationType];
if (OverlayItemComponent) {
return <OverlayItemComponent {...overlayItemProps} />;
} else {
const renderItem = customizationService.transform(item);
@@ -296,122 +204,158 @@ function CustomizableViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
viewportId,
servicesManager,
customizationService,
instance,
instances,
voi,
scale,
instanceNumber,
]
);
const getTopLeftContent = useCallback(() => {
const items = topLeftCustomization?.items || [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topLeftCustomization, _renderOverlayItem]);
const getContent = useCallback(
(customization, defaultItems, keyPrefix) => {
const items = customization?.items ?? defaultItems;
return (
<>
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{item?.condition
? item.condition({
instance: instances ? instances[item?.instanceIndex] : null,
formatters: { formatDate: formatDICOMDate },
})
? _renderOverlayItem(item)
: null
: _renderOverlayItem(item)}
</div>
))}
</>
);
},
[_renderOverlayItem]
);
const getTopRightContent = useCallback(() => {
const items = topRightCustomization?.items || [
{
id: 'InstanceNmber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topRightCustomization, _renderOverlayItem]);
const studyDateItem = {
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ instance }) => instance && instance.StudyDate,
contentF: ({ instance, formatters: { formatDate } }) => formatDate(instance.StudyDate),
};
const getBottomLeftContent = useCallback(() => {
const items = bottomLeftCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomLeftOverlayItem_${i}`}>
{_renderOverlayItem(item)}
</div>
))}
</>
);
}, [bottomLeftCustomization, _renderOverlayItem]);
const seriesDescriptionItem = {
id: 'SeriesDescription',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Series description',
attribute: 'SeriesDescription',
condition: ({ instance }) => {
return instance && instance.SeriesDescription;
},
};
const getBottomRightContent = useCallback(() => {
const items = bottomRightCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomRightOverlayItem_${i}`}>
{_renderOverlayItem(item)}
</div>
))}
</>
);
}, [bottomRightCustomization, _renderOverlayItem]);
const topLeftItems = instances
? instances
.map((instance, index) => {
return [
{
...studyDateItem,
instanceIndex: index,
},
{
...seriesDescriptionItem,
instanceIndex: index,
},
];
})
.flat()
: [];
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
bottomLeft={getBottomLeftContent()}
bottomRight={getBottomRightContent()}
topLeft={
/**
* Inline default overlay items for a more standard expansion
*/
getContent(topLeftCustomization, [...topLeftItems], 'topLeftOverlayItem')
}
topRight={getContent(topRightCustomization, [], 'topRightOverlayItem')}
bottomLeft={getContent(
bottomLeftCustomization,
[
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: () => {
const activeToolName = toolGroupService.getActiveToolForViewport(viewportId);
return activeToolName === 'Zoom';
},
},
],
'bottomLeftOverlayItem'
)}
bottomRight={getContent(
bottomRightCustomization,
[
{
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
],
'bottomRightOverlayItem'
)}
/>
);
}
function _getViewportInstance(viewportData, imageIndex) {
let imageId = null;
function _getViewportInstances(viewportData) {
const imageIds = [];
if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageId = viewportData.data.imageIds[imageIndex];
imageIds.push(viewportData.data.imageIds[0]);
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.data;
if (volumes && volumes.length == 1) {
const volume = volumes[0];
imageId = volume.imageIds[imageIndex];
}
volumes.forEach(volume => {
if (!volume?.imageIds) {
return;
}
imageIds.push(volume.imageIds[0]);
});
}
return imageId ? metaData.get('instance', imageId) || {} : {};
const instances = [];
imageIds.forEach(imageId => {
const instance = metaData.get('instance', imageId) || {};
instances.push(instance);
});
return instances;
}
function _getInstanceNumber(
viewportData,
viewportIndex,
imageIndex,
cornerstoneViewportService
) {
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber && instanceNumber !== 0) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
cornerstoneViewportService
);
switch (viewportData.viewportType) {
case Enums.ViewportType.STACK:
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
break;
case Enums.ViewportType.ORTHOGRAPHIC:
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
viewportId,
cornerstoneViewportService,
imageIndex
);
break;
}
return instanceNumber;
}
return instanceNumber ?? null;
};
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
@@ -438,23 +382,21 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
function _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
cornerstoneViewportService
viewportId,
cornerstoneViewportService,
imageIndex
) {
const volumes = viewportData.volumes;
const volumes = viewportData.data;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
if (!volumes) {
return;
}
const volume = volumes[0];
// Todo: support fusion of acquisition plane which has instanceNumber
const { volume } = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!cornerstoneViewport) {
return;
@@ -477,16 +419,81 @@ function _getInstanceNumberFromVolume(
return {};
}
const { instanceNumber } =
metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber);
}
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 shrink-0">{windowCenter.toFixed(0)}</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span>{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span>
{instanceNumber !== undefined && instanceNumber !== null ? (
<>
<span className="mr-1 shrink-0">I:</span>
<span>{`${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`}</span>
</>
) : (
`${imageIndex + 1}/${numberOfSlices}`
)}
</span>
</div>
);
}
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,27 +102,21 @@ function ViewportOrientationMarkers({
flipHorizontal
);
const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
if (!ohifViewport) {
console.log('ViewportOrientationMarkers::No viewport');
return null;
}
const backgroundColor = ohifViewport.getViewportOptions().background;
// 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;
return orientationMarkers.map((m, index) => (
<div
className={classNames(
'overlay-text',
`${m}-mid orientation-marker`,
isLight ? 'text-[#726F7E]' : 'text-[#ccc]'
'text-aqua-pale',
'text-[13px]',
'leading-5'
)}
key={`${m}-mid orientation-marker`}
>
@@ -151,7 +133,7 @@ function ViewportOrientationMarkers({
element,
]);
return <div className="ViewportOrientationMarkers noselect">{markers}</div>;
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
}
ViewportOrientationMarkers.propTypes = {
@@ -174,13 +156,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);
@@ -1,294 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
const EPSILON = 1e-4;
function CornerstoneViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
servicesManager,
}) {
const {
cornerstoneViewportService,
toolbarService,
} = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
useEffect(() => {
let isMounted = true;
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
if (!isMounted) {
return;
}
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
isMounted = false;
unsubscribe();
};
}, []);
/**
* Updating the VOI when the viewport changes its voi
*/
useEffect(() => {
const updateVOI = eventDetail => {
const { range } = eventDetail.detail;
if (!range) {
return;
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
lower,
upper
);
setVOI({ windowCenter, windowWidth });
};
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportIndex, viewportData, voi, element]);
/**
* Updating the scale when the viewport changes its zoom
*/
useEffect(() => {
const updateScale = eventDetail => {
const { previousCamera, camera } = eventDetail.detail;
if (
previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale
) {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
if (!viewport) {
return;
}
const imageData = viewport.getImageData();
if (!imageData) {
return;
}
if (camera.scale) {
setScale(camera.scale);
return;
}
const { spacing } = imageData;
// convert parallel scale to scale
const scale =
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale);
}
};
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportIndex, viewportData]);
const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi;
if (activeTools.includes('WindowLevel')) {
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div className="flex flex-row text-base">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
if (activeTools.includes('Zoom')) {
return (
<div className="flex flex-row text-base">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
return null;
}, [voi, scale, activeTools]);
const getTopRightContent = useCallback(() => {
const { imageIndex, numberOfSlices } = imageSliceData;
if (!viewportData) {
return;
}
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
cornerstoneViewportService
);
}
return (
<div className="flex flex-row text-base">
<span className="mr-1">I:</span>
<span className="font-light">
{instanceNumber !== undefined
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}, [imageSliceData, viewportData, viewportIndex]);
if (!viewportData) {
return null;
}
const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (!ohifViewport) {
return null;
}
const backgroundColor = ohifViewport.getViewportOptions().background;
// 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;
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
color={isLight && 'text-[#0944B3]'}
/>
);
}
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
const imageId = imageIds[imageIndex];
if (!imageId) {
return;
}
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
const stackSize = imageIds.length;
if (stackSize <= 1) {
return;
}
return parseInt(instanceNumber);
}
// 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
) {
const volumes = viewportData.volumes;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
return;
}
const volume = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
if (!cornerstoneViewport) {
return;
}
const camera = cornerstoneViewport.getCamera();
const { viewPlaneNormal } = camera;
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
const scanAxisNormal = direction.slice(6, 9);
// check if viewPlaneNormal is parallel to scanAxisNormal
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
if (isAcquisitionPlane) {
const imageId = imageIds[imageIndex];
if (!imageId) {
return {};
}
const { instanceNumber } =
metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber);
}
}
CornerstoneViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportIndex: PropTypes.number,
servicesManager: PropTypes.instanceOf(ServicesManager),
};
export default CornerstoneViewportOverlay;
@@ -75,22 +75,16 @@ export function formatPN(name) {
* Gets compression type
*
* @param {number} imageId
* @returns {string} comrpession type.
* @returns {string} compression type.
*/
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';
}
+371 -218
View File
@@ -3,6 +3,8 @@ import {
StackViewport,
VolumeViewport,
utilities as csUtils,
Types as CoreTypes,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import {
ToolGroupManager,
@@ -11,13 +13,20 @@ import {
ReferenceLinesTool,
} from '@cornerstonejs/tools';
import { Types as OhifTypes } from '@ohif/core';
import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog';
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { CornerstoneServices } from './types';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
voi: toggleVOISliceSync,
};
function commandsModule({
servicesManager,
@@ -27,12 +36,14 @@ function commandsModule({
viewportGridService,
toolGroupService,
cineService,
toolbarService,
stateSyncService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
measurementService,
customizationService,
colorbarService,
hangingProtocolService,
syncGroupService,
} = servicesManager.services as CornerstoneServices;
const { measurementServiceSource } = this;
@@ -57,9 +68,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 {
@@ -89,10 +98,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({
@@ -109,18 +115,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;
},
@@ -139,27 +141,18 @@ function commandsModule({
* on the measurement with a response if not cancelled.
*/
setMeasurementLabel: ({ uid }) => {
const labelConfig = customizationService.get('measurementLabels');
const measurement = measurementService.getMeasurement(uid);
callInputDialog(
uiDialogService,
measurement,
(label, actionId) => {
if (actionId === 'cancel') {
return;
}
const updatedMeasurement = Object.assign({}, measurement, {
label,
});
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(
(val: Map<any, any>) => {
measurementService.update(
updatedMeasurement.uid,
updatedMeasurement,
uid,
{
...val,
},
true
);
},
false
}
);
},
@@ -168,7 +161,7 @@ function commandsModule({
* @param props - containing the updates to apply
* @param props.measurementKey - chooses the measurement key to apply the
* code to. This will typically be finding or site to apply a
* finind code or a findingSites code.
* finding code or a findingSites code.
* @param props.code - A coding scheme value from DICOM, including:
* * CodeValue - the language independent code, for example '1234'
* * CodingSchemeDesignator - the issue of the code value
@@ -220,62 +213,42 @@ 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);
arrowTextCallback: ({ callback, data, uid }) => {
const labelConfig = customizationService.get('measurementLabels');
callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig);
},
toggleCine: () => {
const { viewports } = viewportGridService.getState();
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 }) {
setViewportWindowLevel({ viewportId, window, level }) {
// convert to numbers
const windowWidthNum = Number(window);
const windowCenterNum = Number(level);
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(
viewportId
);
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
// get actor from the viewport
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: {
@@ -286,78 +259,85 @@ function commandsModule({
viewport.render();
},
// Just call the toolbar service record interaction - allows
// executing a toolbar command as a full toolbar command with side affects
// coming from the ToolbarService itself.
toolbarServiceRecordInteraction: props => {
toolbarService.recordInteraction(props);
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
const hasColorbar = colorbarService.hasColorbar(viewportId);
if (hasColorbar) {
colorbarService.removeColorbar(viewportId);
return;
}
colorbarService.addColorbar(viewportId, displaySetInstanceUIDs, options);
},
setToolActive: ({ toolName, toolGroupId = null }) => {
if (toolName === 'Crosshairs') {
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
setWindowLevel(props) {
const { toolGroupId } = props;
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
uiNotificationService.show({
title: 'Crosshairs',
message:
'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.',
type: 'info',
duration: 3000,
});
throw new Error('Crosshairs tool is not available in this viewport');
}
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
const { viewports } = viewportGridService.getState() || {
viewports: [],
};
actions.setViewportWindowLevel({ ...props, viewportId });
},
setToolEnabled: ({ toolName, toggle }) => {
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
return;
}
const toolGroup = toolGroupService.getToolGroup(null);
if (!toolGroup) {
return;
}
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
// Toggle the tool's state only if the toggle is true
if (toggle) {
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
}
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
const toolName = itemId || value;
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
toolGroupIds.forEach(toolGroupId => {
actions.setToolActive({ toolName, toolGroupId });
});
},
setToolActive: ({ toolName, toolGroupId = null }) => {
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
return;
}
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
// if toolGroup has been destroyed, or its viewports have been removed
if (!toolGroupViewportIds || !toolGroupViewportIds.length) {
if (!toolGroup) {
return;
}
const filteredViewports = viewports.filter(viewport => {
if (!viewport.viewportOptions) {
return false;
}
return toolGroupViewportIds.includes(
viewport.viewportOptions.viewportId
);
});
if (!filteredViewports.length) {
if (!toolGroup.hasTool(toolName)) {
return;
}
if (!toolGroup.getToolInstance(toolName)) {
uiNotificationService.show({
title: `${toolName} tool`,
message: `The ${toolName} tool is not available in this viewport.`,
type: 'info',
duration: 3000,
});
throw new Error(`ToolGroup ${toolGroup.id} does not have this tool.`);
}
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
if (activeToolName) {
// Todo: this is a hack to prevent the crosshairs to stick around
// after another tool is selected. We should find a better way to do this
if (activeToolName === 'Crosshairs') {
toolGroup.setToolDisabled(activeToolName);
} else {
toolGroup.setToolPassive(activeToolName);
}
const activeToolOptions = toolGroup.getToolConfiguration(activeToolName);
activeToolOptions?.disableOnPassive
? toolGroup.setToolDisabled(activeToolName)
: toolGroup.setToolPassive(activeToolName);
}
// Set the new toolName to be active
toolGroup.setToolActive(toolName, {
bindings: [
@@ -368,13 +348,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',
@@ -391,7 +367,7 @@ function commandsModule({
content: CornerstoneViewportDownloadForm,
title: 'Download High Quality Image',
contentProps: {
activeViewportIndex,
activeViewportId,
onClose: uiModalService.hide,
cornerstoneViewportService,
},
@@ -406,8 +382,16 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { rotation: currentRotation } = viewport.getProperties();
if (viewport instanceof BaseVolumeViewport) {
const camera = viewport.getCamera();
const rotAngle = (rotation * Math.PI) / 180;
const rotMat = mat4.identity(new Float32Array(16));
mat4.rotate(rotMat, rotMat, rotAngle, camera.viewPlaneNormal);
const rotatedViewUp = vec3.transformMat4(vec3.create(), camera.viewUp, rotMat);
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
viewport.render();
} else if (viewport.getRotation !== undefined) {
const currentRotation = viewport.getRotation();
const newRotation = (currentRotation + rotation) % 360;
viewport.setProperties({ rotation: newRotation });
viewport.render();
@@ -471,13 +455,8 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
viewport.resetProperties();
viewport.resetCamera();
} else {
// Todo: add reset properties for volume viewport
viewport.resetCamera();
}
viewport.resetProperties?.();
viewport.resetCamera();
viewport.render();
},
@@ -513,9 +492,7 @@ function commandsModule({
}
viewport = enabledElement.viewport;
} else {
viewport = cornerstoneViewportService.getCornerstoneViewport(
gridViewport.id
);
viewport = cornerstoneViewportService.getCornerstoneViewport(gridViewport.id);
}
// Get number of slices
@@ -525,14 +502,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,97 +529,252 @@ function commandsModule({
cstUtils.scroll(viewport, options);
},
setViewportColormap: ({
viewportIndex,
viewportId,
displaySetInstanceUID,
colormap,
opacity = 1,
immediate = false,
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const actorEntries = viewport.getActors();
let hpOpacity;
// Retrieve active protocol's viewport match details
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
// Get display set options for the specified viewport ID
const displaySetsInfo = viewportMatchDetails.get(viewportId)?.displaySetsInfo;
const actorEntry = actorEntries.find(actorEntry => {
return actorEntry.uid.includes(displaySetInstanceUID);
});
if (displaySetsInfo) {
// Find the display set that matches the given UID
const matchingDisplaySet = displaySetsInfo.find(
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID
);
// If a matching display set is found, update the opacity with its value
hpOpacity = matchingDisplaySet?.displaySetOptions?.options?.colormap?.opacity;
}
const { actor: volumeActor, uid: volumeId } = actorEntry;
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
viewport.setProperties({ colormap, volumeActor }, volumeId);
const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
const { actor: volumeActor, uid: volumeId } = actorEntry;
viewport.setProperties({ colormap, volumeActor }, volumeId);
};
if (viewport instanceof StackViewport) {
setViewportProperties(viewport, viewportId);
}
if (viewport instanceof VolumeViewport) {
if (!displaySetInstanceUID) {
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
}
setViewportProperties(viewport, displaySetInstanceUID);
}
if (immediate) {
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,
servicesManager,
toggledState,
});
},
toggleReferenceLines: ({ toggledState }) => {
const { activeViewportIndex } = viewportGridService.getState();
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
activeViewportIndex
);
/**
* If the syncId is given and a synchronizer with that ID already exists, it will
* toggle it on/off for the provided viewports. If not, it will attempt to create
* a new synchronizer using the given syncId and type for the specified viewports.
* If no viewports are provided, you may notice some default behavior.
* - 'voi' type, we will aim to synchronize all viewports with the same modality
* -'imageSlice' type, we will aim to synchronize all viewports with the same orientation.
*
* @param options
* @param options.viewports - The viewports to synchronize
* @param options.syncId - The synchronization group ID
* @param options.type - The type of synchronization to perform
*/
toggleSynchronizer: ({ type, viewports, syncId }) => {
const synchronizer = syncGroupService.getSynchronizer(syncId);
const viewportId = viewportInfo.getViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toggledState) {
toolGroup.setToolDisabled(ReferenceLinesTool.toolName);
if (synchronizer) {
synchronizer.isDisabled() ? synchronizer.setEnabled(true) : synchronizer.setEnabled(false);
return;
}
toolGroup.setToolConfiguration(
const fn = toggleSyncFunctions[type];
if (fn) {
fn({
servicesManager,
viewports,
syncId,
});
}
},
setSourceViewportForReferenceLinesTool: ({ viewportId }) => {
if (!viewportId) {
const { activeViewportId } = viewportGridService.getState();
viewportId = activeViewportId ?? 'default';
}
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
toolGroup?.setToolConfiguration(
ReferenceLinesTool.toolName,
{
sourceViewportId: viewportId,
},
true // overwrite
);
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
storePresentation: ({ viewportIndex }) => {
const presentation = cornerstoneViewportService.getPresentation(
viewportIndex
);
if (!presentation || !presentation.presentationIds) {
storePresentation: ({ viewportId }) => {
cornerstoneViewportService.storePresentation({ viewportId });
},
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const command = protocol.callbacks.onViewportDataInitialized;
const numPanes = protocol.stages?.[stageIndex]?.viewports.length ?? 1;
let numPanesWithData = 0;
const { unsubscribe } = cornerstoneViewportService.subscribe(EVENT, evt => {
numPanesWithData++;
if (numPanesWithData === numPanes) {
commandsManager.run(...command);
// Unsubscribe from the event
unsubscribe(EVENT);
}
});
},
setViewportPreset: ({ viewportId, preset }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { presentationIds } = presentation;
const { lutPresentationId, positionPresentationId } =
presentationIds || {};
const storeState = {};
if (lutPresentationId) {
storeState.lutPresentationStore = {
...lutPresentationStore,
[lutPresentationId]: presentation,
};
viewport.setProperties({
preset,
});
viewport.render();
},
/**
* Sets the volume quality for a given viewport.
* @param {string} viewportId - The ID of the viewport to set the volume quality.
* @param {number} volumeQuality - The desired quality level of the volume rendering.
*/
setVolumeRenderingQulaity: ({ viewportId, volumeQuality }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const mapper = actor.getMapper();
const image = mapper.getInputData();
const dims = image.getDimensions();
const spacing = image.getSpacing();
const spatialDiagonal = vec3.length(
vec3.fromValues(dims[0] * spacing[0], dims[1] * spacing[1], dims[2] * spacing[2])
);
let sampleDistance = spacing.reduce((a, b) => a + b) / 3.0;
sampleDistance /= volumeQuality > 1 ? 0.5 * volumeQuality ** 2 : 1.0;
const samplesPerRay = spatialDiagonal / sampleDistance + 1;
mapper.setMaximumSamplesPerRay(samplesPerRay);
mapper.setSampleDistance(sampleDistance);
viewport.render();
},
/**
* Shifts opacity points for a given viewport id.
* @param {string} viewportId - The ID of the viewport to set the mapping range.
* @param {number} shift - The shift value to shift the points by.
*/
shiftVolumeOpacityPoints: ({ viewportId, shift }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ofun = actor.getProperty().getScalarOpacity(0);
const opacityPointValues = []; // Array to hold values
// Gather Existing Values
const size = ofun.getSize();
for (let pointIdx = 0; pointIdx < size; pointIdx++) {
const opacityPointValue = [0, 0, 0, 0];
ofun.getNodeValue(pointIdx, opacityPointValue);
// opacityPointValue now holds [xLocation, opacity, midpoint, sharpness]
opacityPointValues.push(opacityPointValue);
}
if (positionPresentationId) {
storeState.positionPresentationStore = {
...positionPresentationStore,
[positionPresentationId]: presentation,
};
// Add offset
opacityPointValues.forEach(opacityPointValue => {
opacityPointValue[0] += shift; // Change the location value
});
// Set new values
ofun.removeAllPoints();
opacityPointValues.forEach(opacityPointValue => {
ofun.addPoint(...opacityPointValue);
});
viewport.render();
},
/**
* Sets the volume lighting settings for a given viewport.
* @param {string} viewportId - The ID of the viewport to set the lighting settings.
* @param {Object} options - The lighting settings to be set.
* @param {boolean} options.shade - The shade setting for the lighting.
* @param {number} options.ambient - The ambient setting for the lighting.
* @param {number} options.diffuse - The diffuse setting for the lighting.
* @param {number} options.specular - The specular setting for the lighting.
**/
setVolumeLighting: ({ viewportId, options }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const property = actor.getProperty();
if (options.shade !== undefined) {
property.setShade(options.shade);
}
stateSyncService.store(storeState);
if (options.ambient !== undefined) {
property.setAmbient(options.ambient);
}
if (options.diffuse !== undefined) {
property.setDiffuse(options.diffuse);
}
if (options.specular !== undefined) {
property.setSpecular(options.specular);
}
viewport.render();
},
resetCrosshairs: ({ viewportId }) => {
const crosshairInstances = [];
const getCrosshairInstances = toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
crosshairInstances.push(toolGroup.getToolInstance('Crosshairs'));
};
if (!viewportId) {
const toolGroupIds = toolGroupService.getToolGroupIds();
toolGroupIds.forEach(getCrosshairInstances);
} else {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
getCrosshairInstances(toolGroup.id);
}
crosshairInstances.forEach(ins => {
ins.resetCrosshairs();
});
},
};
@@ -653,7 +783,6 @@ function commandsModule({
// context menu
showCornerstoneContextMenu: {
commandFn: actions.showCornerstoneContextMenu,
storeContexts: [],
options: {
menuCustomizationId: 'measurementsContextMenu',
commands: [
@@ -672,7 +801,9 @@ function commandsModule({
storeContexts: [],
options: {},
},
toggleViewportColorbar: {
commandFn: actions.toggleViewportColorbar,
},
deleteMeasurement: {
commandFn: actions.deleteMeasurement,
},
@@ -682,16 +813,21 @@ function commandsModule({
updateMeasurement: {
commandFn: actions.updateMeasurement,
},
setViewportWindowLevel: {
commandFn: actions.setViewportWindowLevel,
},
setWindowLevel: {
commandFn: actions.setWindowLevel,
},
toolbarServiceRecordInteraction: {
commandFn: actions.toolbarServiceRecordInteraction,
},
setToolActive: {
commandFn: actions.setToolActive,
},
setToolActiveToolbar: {
commandFn: actions.setToolActiveToolbar,
},
setToolEnabled: {
commandFn: actions.setToolEnabled,
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
options: { rotation: 90 },
@@ -701,10 +837,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,
@@ -764,16 +901,32 @@ function commandsModule({
setViewportColormap: {
commandFn: actions.setViewportColormap,
},
toggleStackImageSync: {
commandFn: actions.toggleStackImageSync,
},
toggleReferenceLines: {
commandFn: actions.toggleReferenceLines,
setSourceViewportForReferenceLinesTool: {
commandFn: actions.setSourceViewportForReferenceLinesTool,
},
storePresentation: {
commandFn: actions.storePresentation,
storeContexts: [],
options: {},
},
attachProtocolViewportDataListener: {
commandFn: actions.attachProtocolViewportDataListener,
},
setViewportPreset: {
commandFn: actions.setViewportPreset,
},
setVolumeRenderingQulaity: {
commandFn: actions.setVolumeRenderingQulaity,
},
shiftVolumeOpacityPoints: {
commandFn: actions.shiftVolumeOpacityPoints,
},
setVolumeLighting: {
commandFn: actions.setVolumeLighting,
},
resetCrosshairs: {
commandFn: actions.resetCrosshairs,
},
toggleSynchronizer: {
commandFn: actions.toggleSynchronizer,
},
};
@@ -1,101 +1,112 @@
import React, { useEffect } from 'react';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { useAppConfig } from '@state';
function WrappedCinePlayer({
enabledVPElement,
viewportIndex,
servicesManager,
}) {
const { toolbarService, customizationService } = servicesManager.services;
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportIndex }] = useViewportGrid();
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
const [{ isCineEnabled, cines }, api] = useCine();
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
const [appConfig] = useAppConfig();
const isMountedRef = useRef(null);
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[viewportIndex] || !enabledVPElement) {
if (!cines || !cines[viewportId] || !enabledVPElement) {
return;
}
const cine = cines[viewportIndex];
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, {
api.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
api.stopClip(enabledVPElement);
}
};
const newStackCineHandler = useCallback(() => {
const { viewports } = viewportGridService.getState();
const { displaySetInstanceUIDs } = viewports.get(viewportId);
let frameRate = 24;
let isPlaying = cines[viewportId].isPlaying;
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (displaySet.FrameRate) {
// displaySet.FrameRate corresponds to DICOM tag (0018,1063) which is defined as the the frame time in milliseconds
// So a bit of math to get the actual frame rate.
frameRate = Math.round(1000 / displaySet.FrameRate);
isPlaying ||= !!appConfig.autoPlayCine;
}
});
if (isPlaying) {
api.setIsCineEnabled(isPlaying);
}
api.setCine({ id: viewportId, isPlaying, frameRate });
setNewStackFrameRate(frameRate);
}, [displaySetService, viewportId, viewportGridService, cines]);
useEffect(() => {
eventTarget.addEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
isMountedRef.current = true;
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
return () => {
cineService.setCine({ id: viewportIndex, isPlaying: false });
eventTarget.removeEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
isMountedRef.current = false;
api.stopClip(enabledVPElement);
api.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
};
}, [enabledVPElement]);
}, [enabledVPElement, newStackCineHandler]);
useEffect(() => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
api.stopClip(enabledVPElement);
};
}, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]);
}, [cines, viewportId, enabledVPElement, cineHandler]);
const cine = cines[viewportIndex];
const cine = cines[viewportId];
const isPlaying = (cine && cine.isPlaying) || false;
return (
isCineEnabled && (
<CinePlayerComponent
className="absolute left-1/2 -translate-x-1/2 bottom-3"
className="absolute left-1/2 bottom-3 -translate-x-1/2"
frameRate={newStackFrameRate}
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
id: activeViewportIndex,
onClose={() => {
// also stop the clip
api.setCine({
id: viewportId,
isPlaying: false,
});
api.setIsCineEnabled(false);
}}
onPlayPauseChange={isPlaying => {
api.setCine({
id: viewportId,
isPlaying,
})
}
});
}}
onFrameRateChange={frameRate =>
cineService.setCine({
id: activeViewportIndex,
api.setCine({
id: viewportId,
frameRate,
})
}
@@ -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;
}
@@ -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,20 +34,29 @@ 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 disabled={false} onClick={() => {}}>
<Button
disabled={false}
onClick={() => {}}
>
{'Add files'}
<input {...getInputProps()} />
</Button>
</div>
)}
</Dropzone>
<Dropzone onDrop={onDrop} noDrag>
<Dropzone
onDrop={onDrop}
noDrag
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<Button
@@ -73,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>
@@ -92,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,15 +267,17 @@ 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 disabled={false} className="ml-auto" onClick={onComplete}>
<Button
disabled={false}
className="ml-auto"
onClick={onComplete}
>
{'Close'}
</Button>
</>
@@ -320,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}
>
@@ -345,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>
@@ -361,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>
@@ -397,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
@@ -421,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,9 +15,7 @@ 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());
@@ -62,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>;
@@ -76,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}
/>
@@ -0,0 +1,23 @@
import React from 'react';
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
import { ViewportActionCorners } from '@ohif/ui';
export type OHIFViewportActionCornersProps = {
viewportId: string;
};
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
const [viewportActionCornersState] = useViewportActionCornersContext();
if (!viewportActionCornersState[viewportId]) {
return null;
}
return (
<ViewportActionCorners
cornerComponents={viewportActionCornersState[viewportId]}
></ViewportActionCorners>
);
}
export default OHIFViewportActionCorners;
@@ -0,0 +1,114 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui';
import { StackViewport, VolumeViewport } from '@cornerstonejs/core';
import { ColorbarProps } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core';
export function setViewportColorbar(
viewportId,
displaySets,
commandsManager,
serviceManager,
colorbarOptions
) {
const { cornerstoneViewportService } = serviceManager.services;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
if (isLight) {
colorbarOptions.ticks = {
position: 'left',
style: {
font: '12px Arial',
color: '#000000',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
}
const displaySetInstanceUIDs = [];
if (viewport instanceof StackViewport) {
displaySetInstanceUIDs.push(viewportId);
}
if (viewport instanceof VolumeViewport) {
displaySets.forEach(ds => {
displaySetInstanceUIDs.push(ds.displaySetInstanceUID);
});
}
commandsManager.run({
commandName: 'toggleViewportColorbar',
commandOptions: {
viewportId,
options: colorbarOptions,
displaySetInstanceUIDs,
},
context: 'CORNERSTONE',
});
}
export function Colorbar({
viewportId,
displaySets,
commandsManager,
serviceManager,
colorbarProperties,
}: ColorbarProps): ReactElement {
const { colorbarService } = serviceManager.services;
const {
width: colorbarWidth,
colorbarTickPosition,
colorbarContainerPosition,
colormaps,
colorbarInitialColormap,
} = colorbarProperties;
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
viewportId,
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const updateColorbarState = () => {
setShowColorbar(colorbarService.hasColorbar(viewportId));
};
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
updateColorbarState
);
return () => {
unsubscribe();
};
}, [viewportId]);
return (
<div className="all-in-one-menu-item flex w-full justify-center">
<SwitchButton
label="Display Color bar"
checked={showColorbar}
onChange={() => {
onSetColorbar();
}}
/>
</div>
);
}
Loaded 100 of 1306 files, more files were not shown because too many files have changed in this diff. Show more