Compare commits

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

No files matched your search

+2 -1
View File
@@ -1,5 +1,6 @@
server {
listen ${PORT};
listen ${PORT} default_server;
listen [::]:${PORT} default_server;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
@@ -0,0 +1,22 @@
server {
listen ${SSL_PORT} ssl http2 default_server;
listen [::]:${SSL_PORT} ssl http2 default_server;
ssl_certificate /etc/ssl/certs/ssl-certificate.crt;
ssl_certificate_key /etc/ssl/private/ssl-private-key.key;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
+11 -1
View File
@@ -1,6 +1,16 @@
#!/bin/sh
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
if [ -n "$SSL_PORT" ]
then
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template > /etc/nginx/conf.d/default.conf
else
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
fi
if [ -n "$APP_CONFIG" ]
then
echo "$APP_CONFIG" > /usr/share/nginx/html/app-config.js
fi
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
+4
View File
@@ -21,6 +21,10 @@
"version": "detect"
}
},
"rules": {
// Enforce consistent brace style for all control statements for readability
"curly": "error"
},
"globals": {
"cy": true,
"before": true,
+1
View File
@@ -1,3 +1,4 @@
[submodule "testdata"]
path = testdata
url = https://github.com/OHIF/viewer-testdata-dicomweb.git
branch = main
+5 -1
View File
@@ -33,7 +33,11 @@ function transpileJavaScript(mode) {
rootMode: 'upward',
envName: mode,
cacheCompression: false,
cacheDirectory: true,
// Note: This was causing a lot of issues with yarn link of the cornerstone
// only set this to true if you don't have a yarn link to external libs
// otherwise expect the lib changes not to be reflected in the dev server
// as it will be cached
cacheDirectory: false,
},
};
}
+14 -10
View File
@@ -73,21 +73,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
children: false,
warnings: true,
},
devServer: {
open: true,
port: 3000,
historyApiFallback: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
cache: {
type: 'filesystem',
},
module: {
noParse: [/(codec)/, /(dicomicc)/],
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'source-map-loader',
},
transpileJavaScriptRule(mode),
loadWebWorkersRule,
// loadShadersRule,
@@ -135,7 +131,12 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
// symlinked resources are resolved to their real path, not their symlinked location
symlinks: true,
fallback: { fs: false, path: false, zlib: false },
fallback: {
fs: false,
path: false,
zlib: false,
"buffer": require.resolve("buffer")
},
},
plugins: [
new webpack.DefinePlugin({
@@ -159,6 +160,9 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
process.env.REACT_APP_I18N_DEBUG || ''
),
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
],
+52
View File
@@ -0,0 +1,52 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
### Bug Fixes
* **OpenIdConnectRoutes:** fix handleUnauthenticated ([#3617](https://github.com/OHIF/Viewers/issues/3617)) ([35fc30c](https://github.com/OHIF/Viewers/commit/35fc30c5359d8199cc38ffa670c08687d2672f11))
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
### Bug Fixes
* **PT Metadata:** Allow for PatientWeight to be missing from the metadata ([#3621](https://github.com/OHIF/Viewers/issues/3621)) ([44f101d](https://github.com/OHIF/Viewers/commit/44f101d3f2b3204b67e31f4e4939062e65a246ee))
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
+7 -1
View File
@@ -38,6 +38,7 @@ COPY platform /usr/src/app/platform
# Copy Files
FROM node:16.15.0-slim as builder
RUN apt-get update && apt-get install -y build-essential python3
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
@@ -61,7 +62,7 @@ RUN yarn run build
# Stage 3: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.23.1-alpine as final
FROM nginxinc/nginx-unprivileged:1.25-alpine as final
#RUN apk add --no-cache bash
ENV PORT=80
RUN rm /etc/nginx/conf.d/default.conf
@@ -69,5 +70,10 @@ USER nginx
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
# The nginx user cannot chmod it, so change to root.
USER root
RUN chmod 666 /usr/share/nginx/html/app-config.js
USER nginx
ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+17 -11
View File
@@ -3,7 +3,7 @@
<div align="center">
<h1>OHIF Medical Imaging Viewer</h1>
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p>
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/using/dicomweb/">DICOMweb</a>.</p>
</div>
@@ -34,13 +34,15 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
<!-- [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) -->
<!-- prettier-ignore-end -->
| | |
| :-: | :--- |
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking |
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations |
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols |
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy |
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering |
| | | |
| :-: | :--- | :--- |
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.275741864483510678566144889372061815320) |
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
## About
@@ -161,7 +163,7 @@ yarn install
These commands are available from the root directory. Each project directory
also supports a number of commands that can be found in their respective
`README.md` and `project.json` files.
`README.md` and `package.json` files.
| Yarn Commands | Description |
| ---------------------------- | ------------------------------------------------------------- |
@@ -247,7 +249,7 @@ or, for v1, please cite:
> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334)
**Note:** If you use or find this repository helpful, please take the time to
star this repository on Github. This is an easy way for us to assess adoption
star this repository on GitHub. This is an easy way for us to assess adoption
and it can help us obtain future funding for the project.
This work is supported primarily by the National Institutes of Health, National
@@ -255,7 +257,11 @@ Cancer Institute, Informatics Technology for Cancer Research (ITCR) program,
under a
[grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104).
This project is tested with BrowserStack. Thank you for supporting open source
[NCI Imaging Data Commons (IDC) project](https://imaging.datacommons.cancer.gov/) supported the development of new features and bug fixes marked with ["IDC:priority"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Apriority),
["IDC:candidate"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acandidate) or ["IDC:collaboration"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acollaboration). NCI Imaging Data Commons is supported by contract number 19X037Q from
Leidos Biomedical Research under Task Order HHSN26100071 from NCI. [IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized version of the OHIF Viewer.
This project is tested with BrowserStack. Thank you for supporting open-source!
## License
+8 -4
View File
@@ -13,12 +13,16 @@ module.exports = {
'inline-react-svg',
{
svgo: {
plugins: extendDefaultPlugins([
plugins: [
{
name: 'removeViewBox',
active: false,
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
]),
],
},
},
],
+1 -1
View File
@@ -1 +1 @@
14369414cf0193f00243ae5c9f8415cf7079a753
35fc30c5359d8199cc38ffa670c08687d2672f11
@@ -0,0 +1,43 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.61",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -31,10 +31,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.2",
"@ohif/extension-cornerstone": "3.7.0-beta.2",
"@ohif/extension-default": "3.7.0-beta.2",
"@ohif/i18n": "3.7.0-beta.2",
"@ohif/core": "3.7.0-beta.61",
"@ohif/extension-cornerstone": "3.7.0-beta.61",
"@ohif/extension-default": "3.7.0-beta.61",
"@ohif/i18n": "3.7.0-beta.61",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -2,6 +2,7 @@ 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(
@@ -36,12 +37,14 @@ const extension: Types.Extensions.Extension = {
getViewportModule({
servicesManager,
extensionManager,
commandsManager,
}: Types.Extensions.ExtensionParams) {
const ExtendedOHIFCornerstoneRTViewport = props => {
return (
<OHIFCornerstoneRTViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);
@@ -59,3 +62,4 @@ const extension: Types.Extensions.Extension = {
};
export default extension;
export { hydrateRTDisplaySet };
@@ -181,12 +181,6 @@ export default async function loadRTStruct(
ContourGeometricType,
} = ContourSequenceArray[c];
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
if (!imageId) {
continue;
}
let isSupported = false;
const points = [];
@@ -1,3 +1,4 @@
import { ButtonEnums } from '@ohif/ui';
import hydrateRTDisplaySet from './_hydrateRT';
const RESPONSE = {
@@ -11,6 +12,7 @@ function promptHydrateRT({
rtDisplaySet,
viewportIndex,
toolGroupId = 'default',
preHydrateCallbacks,
}) {
const { uiViewportDialogService } = servicesManager.services;
@@ -21,6 +23,10 @@ function promptHydrateRT({
);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
@@ -38,12 +44,12 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
type: 'secondary',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
type: 'primary',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
},
@@ -25,6 +25,7 @@ function OHIFCornerstoneRTViewport(props) {
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const {
@@ -91,6 +92,14 @@ function OHIFCornerstoneRTViewport(props) {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportIndex }) => {
commandsManager.runCommand('storePresentation', {
viewportIndex,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
@@ -155,6 +164,7 @@ function OHIFCornerstoneRTViewport(props) {
servicesManager,
viewportIndex,
rtDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
@@ -303,6 +313,13 @@ function OHIFCornerstoneRTViewport(props) {
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await _hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
@@ -18,7 +18,9 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
);
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>;
}
@@ -0,0 +1,43 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.61",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -31,10 +31,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.2",
"@ohif/extension-cornerstone": "3.7.0-beta.2",
"@ohif/extension-default": "3.7.0-beta.2",
"@ohif/i18n": "3.7.0-beta.2",
"@ohif/core": "3.7.0-beta.61",
"@ohif/extension-cornerstone": "3.7.0-beta.61",
"@ohif/extension-default": "3.7.0-beta.61",
"@ohif/i18n": "3.7.0-beta.61",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -5,7 +5,6 @@ const segProtocol: Types.HangingProtocol.Protocol = {
// Don't store this hanging protocol as it applies to the currently active
// display set by default
// cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'Segmentations',
// Just apply this one when specifically listed
protocolMatchingRules: [],
@@ -1,11 +1,14 @@
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
import { utils } from '@ohif/core';
import {
metaData,
cache,
triggerEvent,
eventTarget,
} from '@cornerstonejs/core';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
import dcmjs from 'dcmjs';
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
import { dicomlabToRGB } from './utils/dicomlabToRGB';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
@@ -122,13 +125,12 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
!segDisplaySet.segments ||
Object.keys(segDisplaySet.segments).length === 0
) {
const segments = await _loadSegments(
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers
);
segDisplaySet.segments = segments;
headers,
});
}
const suppressEvents = true;
@@ -147,28 +149,65 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
return loadPromises[SOPInstanceUID];
}
async function _loadSegments(extensionManager, segDisplaySet, headers) {
async function _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
}) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
const { segmentationService } = servicesManager.services;
const { dicomLoaderService } = utilityModule.exports;
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(
segDisplaySet,
null,
headers
);
const dicomData = DicomMessage.readFile(segArrayBuffer);
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
const cachedReferencedVolume = cache.getVolume(
segDisplaySet.referencedVolumeId
);
if (!Array.isArray(dataset.SegmentSequence)) {
dataset.SegmentSequence = [dataset.SegmentSequence];
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
}
const segments = _getSegments(dataset);
return segments;
const { imageIds } = cachedReferencedVolume;
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
{
percentComplete,
}
);
});
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
imageIds,
arrayBuffer,
metaData,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
}
});
Object.assign(segDisplaySet, results);
}
function _segmentationExists(segDisplaySet, segmentationService) {
@@ -178,114 +217,6 @@ function _segmentationExists(segDisplaySet, segmentationService) {
);
}
function _getPixelData(dataset, segments) {
let frameSize = Math.ceil((dataset.Rows * dataset.Columns) / 8);
let nextOffset = 0;
Object.keys(segments).forEach(segmentKey => {
const segment = segments[segmentKey];
segment.numberOfFrames = segment.functionalGroups.length;
segment.size = segment.numberOfFrames * frameSize;
segment.offset = nextOffset;
nextOffset = segment.offset + segment.size;
const packedSegment = dataset.PixelData[0].slice(
segment.offset,
nextOffset
);
segment.pixelData = dcmjs.data.BitArray.unpack(packedSegment);
segment.geometry = geometryFromFunctionalGroups(
dataset,
segment.functionalGroups
);
});
return segments;
}
function geometryFromFunctionalGroups(dataset, perFrame) {
let pixelMeasures =
dataset.SharedFunctionalGroupsSequence.PixelMeasuresSequence;
let planeOrientation =
dataset.SharedFunctionalGroupsSequence.PlaneOrientationSequence;
let planePosition = perFrame[0].PlanePositionSequence; // TODO: assume sorted frames!
const geometry = {};
// NB: DICOM PixelSpacing is defined as Row then Column,
// unlike ImageOrientationPatient
let spacingBetweenSlices = pixelMeasures.SpacingBetweenSlices;
if (!spacingBetweenSlices) {
if (pixelMeasures.SliceThickness) {
console.log('Using SliceThickness as SpacingBetweenSlices');
spacingBetweenSlices = pixelMeasures.SliceThickness;
}
}
geometry.spacing = [
pixelMeasures.PixelSpacing[1],
pixelMeasures.PixelSpacing[0],
spacingBetweenSlices,
].map(Number);
geometry.dimensions = [dataset.Columns, dataset.Rows, perFrame.length].map(
Number
);
let orientation = planeOrientation.ImageOrientationPatient.map(Number);
const columnStepToPatient = orientation.slice(0, 3);
const rowStepToPatient = orientation.slice(3, 6);
geometry.planeNormal = [];
vtkMath.cross(columnStepToPatient, rowStepToPatient, geometry.planeNormal);
let firstPosition = perFrame[0].PlanePositionSequence.ImagePositionPatient.map(
Number
);
let lastPosition = perFrame[
perFrame.length - 1
].PlanePositionSequence.ImagePositionPatient.map(Number);
geometry.sliceStep = [];
vtkMath.subtract(lastPosition, firstPosition, geometry.sliceStep);
vtkMath.normalize(geometry.sliceStep);
geometry.direction = columnStepToPatient
.concat(rowStepToPatient)
.concat(geometry.sliceStep);
geometry.origin = planePosition.ImagePositionPatient.map(Number);
return geometry;
}
function _getSegments(dataset) {
const segments = {};
dataset.SegmentSequence.forEach(segment => {
const cielab = segment.RecommendedDisplayCIELabValue;
const rgba = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
Math.round(x * 255)
);
rgba.push(255);
const segmentNumber = segment.SegmentNumber;
segments[segmentNumber] = {
color: rgba,
functionalGroups: [],
offset: null,
size: null,
pixelData: null,
label: segment.SegmentLabel,
};
});
// make a list of functional groups per segment
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
const segmentNumber =
functionalGroup.SegmentIdentificationSequence.ReferencedSegmentNumber;
segments[segmentNumber].functionalGroups.push(functionalGroup);
});
return _getPixelData(dataset, segments);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(
@@ -6,6 +6,7 @@ import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import PanelSegmentation from './panels/PanelSegmentation';
import getHangingProtocolModule from './getHangingProtocolModule';
import hydrateSEGDisplaySet from './utils/_hydrateSEG';
const Component = React.lazy(() => {
return import(
@@ -69,6 +70,7 @@ const extension = {
<OHIFCornerstoneSEGViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);
@@ -88,4 +90,5 @@ const extension = {
getHangingProtocolModule,
};
export default extension;
export default extension;
export { hydrateSEGDisplaySet };
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable } from '@ohif/ui';
import callInputDialog from './callInputDialog';
import { useAppConfig } from '@state';
import { useTranslation } from 'react-i18next';
export default function PanelSegmentation({
@@ -10,6 +10,8 @@ export default function PanelSegmentation({
commandsManager,
}) {
const { segmentationService, uiDialogService } = servicesManager.services;
const [appConfig] = useAppConfig();
const disableEditing = appConfig?.disableEditing;
const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
@@ -203,6 +205,7 @@ export default function PanelSegmentation({
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
disableEditing={disableEditing}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
@@ -268,6 +271,7 @@ PanelSegmentation.propTypes = {
commandsManager: PropTypes.shape({
runCommand: PropTypes.func.isRequired,
}),
appConfig: PropTypes.object.isRequired,
servicesManager: PropTypes.shape({
services: PropTypes.shape({
segmentationService: PropTypes.shape({
@@ -1,5 +1,5 @@
import React from 'react';
import { Input, Dialog } from '@ohif/ui';
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
function callInputDialog(uiDialogService, label, callback) {
const dialogId = 'enter-segment-label';
@@ -29,8 +29,8 @@ function callInputDialog(uiDialogService, label, callback) {
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Confirm', type: 'secondary' },
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
@@ -0,0 +1,16 @@
import dcmjs from 'dcmjs';
/**
* Converts a CIELAB color to an RGB color using the dcmjs library.
* @param cielab - The CIELAB color to convert.
* @returns The RGB color as an array of three integers between 0 and 255.
*/
function dicomlabToRGB(cielab: number[]): number[] {
const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
Math.round(x * 255)
);
return rgb;
}
export { dicomlabToRGB };
@@ -1,3 +1,4 @@
import { ButtonEnums } from '@ohif/ui';
import hydrateSEGDisplaySet from './_hydrateSEG';
const RESPONSE = {
@@ -10,6 +11,7 @@ function promptHydrateSEG({
servicesManager,
segDisplaySet,
viewportIndex,
preHydrateCallbacks,
}) {
const { uiViewportDialogService } = servicesManager.services;
@@ -20,6 +22,10 @@ function promptHydrateSEG({
);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
@@ -36,12 +42,12 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
type: 'secondary',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
type: 'primary',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
},
@@ -24,6 +24,7 @@ function OHIFCornerstoneSEGViewport(props) {
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { t } = useTranslation('SEGViewport');
@@ -48,7 +49,6 @@ function OHIFCornerstoneSEGViewport(props) {
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
const [selectedSegment, setSelectedSegment] = useState(1);
// Hydration means that the SEG is opened and segments are loaded into the
@@ -71,7 +71,8 @@ function OHIFCornerstoneSEGViewport(props) {
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet
referencedDisplaySet,
segDisplaySet
);
referencedDisplaySetRef.current = {
@@ -92,6 +93,14 @@ function OHIFCornerstoneSEGViewport(props) {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportIndex }) => {
commandsManager.runCommand('storePresentation', {
viewportIndex,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
@@ -158,6 +167,7 @@ function OHIFCornerstoneSEGViewport(props) {
servicesManager,
viewportIndex,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
@@ -247,8 +257,6 @@ function OHIFCornerstoneSEGViewport(props) {
toolGroupId
);
setToolGroupCreated(true);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
@@ -308,6 +316,13 @@ function OHIFCornerstoneSEGViewport(props) {
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
@@ -316,7 +331,6 @@ function OHIFCornerstoneSEGViewport(props) {
setIsHydrated(isHydrated);
};
return (
<>
<ViewportActionBar
@@ -343,10 +357,13 @@ function OHIFCornerstoneSEGViewport(props) {
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
thickness: SliceThickness
? utils.roundNumber(SliceThickness, 2)
: '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing:
SpacingBetweenSlices !== undefined
? `${SpacingBetweenSlices.toFixed(2)}mm`
? utils.roundNumber(SpacingBetweenSlices, 2)
: '',
scanner: ManufacturerModelName || '',
},
@@ -381,20 +398,28 @@ OHIFCornerstoneSEGViewport.defaultProps = {
customProps: {},
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet) {
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const {
SharedFunctionalGroupsSequence: [SharedFunctionalGroup],
} = segDisplaySet.instance;
const {
PixelMeasuresSequence: [PixelMeasures],
} = SharedFunctionalGroup;
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
PatientID: image0.PatientID,
PatientName: image0.PatientName,
PatientSex: image0.PatientSex,
PatientAge: image0.PatientAge,
SliceThickness: image0.SliceThickness,
SliceThickness: image0.SliceThickness || SliceThickness,
StudyDate: image0.StudyDate,
SeriesDescription: image0.SeriesDescription,
SeriesInstanceUID: image0.SeriesInstanceUID,
SeriesNumber: image0.SeriesNumber,
ManufacturerModelName: image0.ManufacturerModelName,
SpacingBetweenSlices: image0.SpacingBetweenSlices,
SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
};
return referencedDisplaySetMetadata;
@@ -2,13 +2,12 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Tooltip } from '@ohif/ui';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
const {t} = useTranslation("Common");
const loadStr = t("LOAD");
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
switch (isHydrated) {
case true:
@@ -18,8 +17,10 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
<div>This Segmentation is loaded in the segmentation panel</div>
);
break;
case false:
StatusIcon = () => <Icon name="status-untracked" />;
case false:
StatusIcon = () => (
<Icon className="text-aqua-pale" name="status-untracked" />
);
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
@@ -42,7 +43,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
</div>
);
return (
<>
{ToolTipMessage && (
@@ -0,0 +1,46 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.61",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -32,10 +32,10 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.2",
"@ohif/extension-cornerstone": "3.7.0-beta.2",
"@ohif/extension-measurement-tracking": "3.7.0-beta.2",
"@ohif/ui": "3.7.0-beta.2",
"@ohif/core": "3.7.0-beta.61",
"@ohif/extension-cornerstone": "3.7.0-beta.61",
"@ohif/extension-measurement-tracking": "3.7.0-beta.61",
"@ohif/ui": "3.7.0-beta.61",
"dcmjs": "^0.29.5",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -44,9 +44,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.1.8",
"@cornerstonejs/core": "^1.1.8",
"@cornerstonejs/tools": "^1.1.8",
"@cornerstonejs/adapters": "^1.11.1",
"@cornerstonejs/core": "^1.11.1",
"@cornerstonejs/tools": "^1.11.1",
"classnames": "^2.3.2"
}
}
@@ -5,7 +5,6 @@ const srProtocol: Types.HangingProtocol.Protocol = {
// Don't store this hanging protocol as it applies to the currently active
// display set by default
// cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'SR Key Images',
// Just apply this one when specifically listed
protocolMatchingRules: [],
@@ -308,8 +308,9 @@ function _measurementReferencesSOPInstanceUID(
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
1;
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber))
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
return false;
}
for (let j = 0; j < coords.length; j++) {
const coord = coords[j];
@@ -670,7 +671,9 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
const { ReferencedSOPSequence } = item;
if (!ReferencedSOPSequence) return;
if (!ReferencedSOPSequence) {
return;
}
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
if (ref.ReferencedSOPClassUID) {
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
@@ -687,7 +690,9 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
}
function _getSequenceAsArray(sequence) {
if (!sequence) return [];
if (!sequence) {
return [];
}
return Array.isArray(sequence) ? sequence : [sequence];
}
@@ -314,18 +314,16 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
);
let canvasCorners;
if (rotation == 90 || rotation == 270) {
canvasCorners = <Array<Types.Point2>>(
utilities.math.ellipse.getCanvasEllipseCorners([
canvasCoordinates[2],
canvasCoordinates[3],
canvasCoordinates[0],
canvasCoordinates[1],
])
);
canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners([
canvasCoordinates[2],
canvasCoordinates[3],
canvasCoordinates[0],
canvasCoordinates[1],
]) as Array<Types.Point2>;
} else {
canvasCorners = <Array<Types.Point2>>(
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates)
);
canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners(
canvasCoordinates
) as Array<Types.Point2>;
}
const lineUID = `${index}`;
@@ -3,12 +3,16 @@ import { DisplaySetService, classes } from '@ohif/core';
const ImageSet = classes.ImageSet;
const findInstance = (measurement, displaySetService: DisplaySetService) => {
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } =
measurement;
const {
displaySetInstanceUID,
ReferencedSOPInstanceUID: sopUid,
} = measurement;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
if (!referencedDisplaySet.images) return;
if (!referencedDisplaySet.images) {
return;
}
return referencedDisplaySet.images.find(it => it.SOPInstanceUID === sopUid);
};
@@ -24,8 +28,12 @@ const findReferencedInstances = (
const instanceById = {};
for (const measurement of displaySet.measurements) {
const { imageId } = measurement;
if (!imageId) continue;
if (instanceById[imageId]) continue;
if (!imageId) {
continue;
}
if (instanceById[imageId]) {
continue;
}
const instance = findInstance(measurement, displaySetService);
if (!instance) {
@@ -9,7 +9,9 @@ const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
let instanceFound;
displaySets.find(displaySet => {
if (!displaySet.images) return false;
if (!displaySet.images) {
return false;
}
instanceFound = displaySet.images.find(
instanceMetadata =>
@@ -2,6 +2,8 @@ import { utilities, metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
const { locking } = CsAnnotation;
const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
@@ -12,20 +14,26 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => {
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') return;
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
return;
}
const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`;
const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning };
return ret;
};
const convertSites = (codingValues, sites) => {
if (!sites || !sites.length) return;
if (!sites || !sites.length) {
return;
}
const ret = [];
// Do as a loop to convert away from Proxy instances
for (let i = 0; i < sites.length; i++) {
// Deal with irregular conversion from dcmjs
const site = convertCode(codingValues, sites[i][0] || sites[i]);
if (site) ret.push(site);
if (site) {
ret.push(site);
}
}
return (ret.length && ret) || undefined;
};
@@ -35,9 +43,11 @@ const convertSites = (codingValues, sites) => {
*
*/
export default function hydrateStructuredReport(
{ servicesManager, extensionManager },
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
const disableEditing = appConfig?.disableEditing;
const dataSource = extensionManager.getActiveDataSource()[0];
const {
measurementService,
@@ -212,7 +222,7 @@ export default function hydrateStructuredReport(
m => m.annotationType === annotationType
);
measurementService.addRawMeasurement(
const newAnnotationUID = measurementService.addRawMeasurement(
source,
annotationType,
{ annotation },
@@ -220,6 +230,13 @@ export default function hydrateStructuredReport(
dataSource
);
if (disableEditing) {
const addedAnnotation = annotationManager.getAnnotation(
newAnnotationUID
);
locking.setAnnotationLocked(addedAnnotation, true);
}
if (!imageIds.includes(imageId)) {
imageIds.push(imageId);
}
@@ -287,7 +304,7 @@ function _mapLegacyDataSet(dataset) {
return dataset;
}
const toArray = function (x) {
const toArray = function(x) {
return Array.isArray(x) ? x : [x];
};
@@ -7,6 +7,7 @@ import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSR
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
const { formatDate } = utils;
@@ -27,6 +28,8 @@ function OHIFCornerstoneSRViewport(props) {
extensionManager,
} = props;
const [appConfig] = useAppConfig();
const {
displaySetService,
cornerstoneViewportService,
@@ -76,7 +79,7 @@ function OHIFCornerstoneSRViewport(props) {
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
measurementService.clearMeasurements();
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager },
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(
@@ -225,6 +228,7 @@ function OHIFCornerstoneSRViewport(props) {
}}
onElementEnabled={onElementEnabled}
initialImageIndex={initialImageIndex}
isJumpToMeasurementDisabled={true}
></Component>
);
}, [activeImageDisplaySetData, viewportIndex, measurementSelected]);
@@ -507,7 +511,9 @@ function _getStatusComponent({
);
break;
case 3:
StatusIcon = () => <Icon name="status-untracked" />;
StatusIcon = () => (
<Icon className="text-aqua-pale" name="status-untracked" />
);
ToolTipMessage = () => (
<div>{`Click ${loadStr} to restore measurements.`}</div>
+43
View File
@@ -0,0 +1,43 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.61",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -36,9 +36,9 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.1.8",
"@ohif/core": "3.7.0-beta.2",
"@ohif/ui": "3.7.0-beta.2",
"@cornerstonejs/dicom-image-loader": "^1.11.1",
"@ohif/core": "3.7.0-beta.61",
"@ohif/ui": "3.7.0-beta.61",
"dcmjs": "^0.29.6",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -52,10 +52,10 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.1.8",
"@cornerstonejs/core": "^1.1.8",
"@cornerstonejs/streaming-image-volume-loader": "^1.1.8",
"@cornerstonejs/tools": "^1.1.8",
"@cornerstonejs/adapters": "^1.11.1",
"@cornerstonejs/core": "^1.11.1",
"@cornerstonejs/streaming-image-volume-loader": "^1.11.1",
"@cornerstonejs/tools": "^1.11.1",
"@kitware/vtk.js": "27.3.1",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
@@ -10,13 +10,7 @@ import {
utilities as csUtils,
} from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import {
CinePlayer,
useCine,
useViewportGrid,
Notification,
useViewportDialog,
} from '@ohif/ui';
import { Notification, useViewportDialog } from '@ohif/ui';
import {
IStackViewport,
IVolumeViewport,
@@ -28,6 +22,8 @@ import './OHIFCornerstoneViewport.css';
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import CornerstoneServices from '../types/CornerstoneServices';
import CinePlayer from '../components/CinePlayer';
import { Types } from '@ohif/core';
const STACK = 'stack';
@@ -117,8 +113,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
onElementDisabled,
isJumpToMeasurementDisabled,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
@@ -126,10 +124,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
} = props;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportIndex }] = useViewportGrid();
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef();
const {
@@ -145,74 +140,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
} = servicesManager.services as CornerstoneServices;
const [viewportDialogState] = useViewportDialog();
const cineHandler = () => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
return;
}
const cine = cines[viewportIndex];
const isPlaying = cine.isPlaying || false;
const frameRate = cine.frameRate || 24;
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
}
};
useEffect(() => {
eventTarget.addEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
return () => {
cineService.setCine({ id: viewportIndex, isPlaying: false });
eventTarget.removeEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
};
}, [enabledVPElement]);
useEffect(() => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
};
}, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]);
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
itemId: 'cine',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
context: 'CORNERSTONE',
},
],
});
};
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
@@ -227,33 +154,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
}
}, [elementRef]);
const storePresentation = () => {
const currentPresentation = cornerstoneViewportService.getPresentation(
viewportIndex
);
if (!currentPresentation || !currentPresentation.presentationIds) return;
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { presentationIds } = currentPresentation;
const { lutPresentationId, positionPresentationId } = presentationIds || {};
const storeState = {};
if (lutPresentationId) {
storeState.lutPresentationStore = {
...lutPresentationStore,
[lutPresentationId]: currentPresentation,
};
}
if (positionPresentationId) {
storeState.positionPresentationStore = {
...positionPresentationStore,
[positionPresentationId]: currentPresentation,
};
}
stateSyncService.store(storeState);
};
const cleanUpServices = useCallback(() => {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
@@ -331,7 +231,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
setImageScrollBarHeight();
return () => {
storePresentation();
commandsManager.runCommand('storePresentation', {
viewportIndex,
});
cleanUpServices();
@@ -363,7 +265,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
useEffect(() => {
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async invalidatedDisplaySetInstanceUID => {
async ({
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
invalidateData,
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
return;
}
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
@@ -434,7 +343,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
displaySetOptions,
presentations
);
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
}
@@ -454,6 +362,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
measurementService,
displaySetService,
@@ -483,11 +395,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
<React.Fragment>
<div className="viewport-wrapper">
<ReactResizeDetector
handleWidth
handleHeight
skipOnMount={true} // Todo: make these configurable
refreshMode={'debounce'}
refreshRate={200} // transition amount in side panel
refreshMode="debounce"
refreshRate={50} // Wait 50 ms after last move to render
onResize={onResize}
targetRef={elementRef.current}
/>
@@ -505,25 +414,11 @@ const OHIFCornerstoneViewport = React.memo(props => {
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
{isCineEnabled && (
<CinePlayer
className="absolute left-1/2 -translate-x-1/2 bottom-3"
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
id: activeViewportIndex,
isPlaying,
})
}
onFrameRateChange={frameRate =>
cineService.setCine({
id: activeViewportIndex,
frameRate,
})
}
/>
)}
<CinePlayer
enabledVPElement={enabledVPElement}
viewportIndex={viewportIndex}
servicesManager={servicesManager}
/>
</div>
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
@@ -558,7 +453,9 @@ function _subscribeToJumpToMeasurementEvents(
props => {
cacheJumpToMeasurementEvent = props;
const { viewportIndex: jumpIndex, measurement, isConsumed } = props;
if (!measurement || isConsumed) return;
if (!measurement || isConsumed) {
return;
}
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
// Decide on which viewport should handle this
cacheJumpToMeasurementEvent.cornerstoneViewport = cornerstoneViewportService.getViewportIndexToJump(
@@ -595,7 +492,9 @@ function _checkForCachedJumpToMeasurementEvents(
viewportGridService,
cornerstoneViewportService
) {
if (!cacheJumpToMeasurementEvent) return;
if (!cacheJumpToMeasurementEvent) {
return;
}
if (cacheJumpToMeasurementEvent.isConsumed) {
cacheJumpToMeasurementEvent = null;
return;
@@ -603,7 +502,9 @@ function _checkForCachedJumpToMeasurementEvents(
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
);
if (!displaysUIDs?.length) return;
if (!displaysUIDs?.length) {
return;
}
// Jump to measurement if the measurement exists
const { measurement } = cacheJumpToMeasurementEvent;
@@ -654,7 +555,6 @@ function _jumpToMeasurement(
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (enabledElement) {
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
const viewport = enabledElement.viewport as
@@ -716,6 +616,10 @@ function _jumpToMeasurement(
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
OHIFCornerstoneViewport.defaultProps = {
isJumpToMeasurementDisabled: false,
};
OHIFCornerstoneViewport.propTypes = {
viewportIndex: PropTypes.number.isRequired,
displaySets: PropTypes.array.isRequired,
@@ -724,6 +628,7 @@ OHIFCornerstoneViewport.propTypes = {
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
servicesManager: PropTypes.object.isRequired,
onElementEnabled: PropTypes.func,
isJumpToMeasurementDisabled: PropTypes.bool,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
@@ -36,8 +36,9 @@ function CornerstoneOverlays(props) {
}
if (viewportData) {
const viewportInfo =
cornerstoneViewportService.getViewportInfoByIndex(viewportIndex);
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) {
return null;
@@ -379,7 +379,7 @@ function _getViewportInstance(viewportData, imageIndex) {
if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageId = viewportData.data.imageIds[imageIndex];
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.volumes;
const volumes = viewportData.data;
if (volumes && volumes.length == 1) {
const volume = volumes[0];
imageId = volume.imageIds[imageIndex];
+38 -5
View File
@@ -28,6 +28,7 @@ function commandsModule({
toolGroupService,
cineService,
toolbarService,
stateSyncService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
@@ -457,11 +458,9 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { invert } = viewport.getProperties();
viewport.setProperties({ invert: !invert });
viewport.render();
}
const { invert } = viewport.getProperties();
viewport.setProperties({ invert: !invert });
viewport.render();
},
resetViewport: () => {
const enabledElement = _getActiveViewportEnabledElement();
@@ -618,6 +617,35 @@ function commandsModule({
);
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
},
storePresentation: ({ viewportIndex }) => {
const presentation = cornerstoneViewportService.getPresentation(
viewportIndex
);
if (!presentation || !presentation.presentationIds) {
return;
}
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { presentationIds } = presentation;
const { lutPresentationId, positionPresentationId } =
presentationIds || {};
const storeState = {};
if (lutPresentationId) {
storeState.lutPresentationStore = {
...lutPresentationStore,
[lutPresentationId]: presentation,
};
}
if (positionPresentationId) {
storeState.positionPresentationStore = {
...positionPresentationStore,
[positionPresentationId]: presentation,
};
}
stateSyncService.store(storeState);
},
};
const definitions = {
@@ -742,6 +770,11 @@ function commandsModule({
toggleReferenceLines: {
commandFn: actions.toggleReferenceLines,
},
storePresentation: {
commandFn: actions.storePresentation,
storeContexts: [],
options: {},
},
};
return {
@@ -0,0 +1,107 @@
import React, { useEffect } from 'react';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
import { Enums, eventTarget } from '@cornerstonejs/core';
function WrappedCinePlayer({
enabledVPElement,
viewportIndex,
servicesManager,
}) {
const { toolbarService, customizationService } = servicesManager.services;
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportIndex }] = useViewportGrid();
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
itemId: 'cine',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
context: 'CORNERSTONE',
},
],
});
};
const cineHandler = () => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
return;
}
const cine = cines[viewportIndex];
const isPlaying = cine.isPlaying || false;
const frameRate = cine.frameRate || 24;
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
}
};
useEffect(() => {
eventTarget.addEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
return () => {
cineService.setCine({ id: viewportIndex, isPlaying: false });
eventTarget.removeEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
};
}, [enabledVPElement]);
useEffect(() => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
};
}, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]);
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
return (
isCineEnabled && (
<CinePlayerComponent
className="absolute left-1/2 -translate-x-1/2 bottom-3"
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
id: activeViewportIndex,
isPlaying,
})
}
onFrameRateChange={frameRate =>
cineService.setCine({
id: activeViewportIndex,
frameRate,
})
}
/>
)
);
}
export default WrappedCinePlayer;
@@ -0,0 +1,3 @@
import CinePlayer from './CinePlayer';
export default CinePlayer;
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import DicomFileUploader from '../../utils/DicomFileUploader';
import DicomUploadProgress from './DicomUploadProgress';
import { Button } from '@ohif/ui';
import { Button, ButtonEnums } from '@ohif/ui';
import './DicomUpload.css';
type DicomUploadProps = {
@@ -46,12 +46,7 @@ function DicomUpload({
<Dropzone onDrop={onDrop} noDrag>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<Button
variant="contained"
color="primary"
disabled={false}
onClick={() => {}}
>
<Button disabled={false} onClick={() => {}}>
{'Add files'}
<input {...getInputProps()} />
</Button>
@@ -62,9 +57,7 @@ function DicomUpload({
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<Button
variant="contained"
color="primaryDark"
border="primaryActive"
type={ButtonEnums.type.secondary}
disabled={false}
onClick={() => {}}
>
@@ -308,13 +308,7 @@ function DicomUploadProgress({
} ${
dicomFileUploaderArr.length > 1 ? 'files' : 'file'
} completed.`}</span>
<Button
variant="contained"
color="primary"
disabled={false}
className="ml-auto"
onClick={onComplete}
>
<Button disabled={false} className="ml-auto" onClick={onComplete}>
{'Close'}
</Button>
</>
@@ -27,7 +27,6 @@ const DicomUploadProgressItem = memo(
const [failedReason, setFailedReason] = useState('');
const [status, setStatus] = useState(dicomFileUploader.getStatus());
console.info(`${dicomFileUploader.getFileId()}`);
const isComplete = useCallback(() => {
return (
status === UploadStatus.Failed ||
@@ -4,9 +4,8 @@ const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'Multi-Planar Reconstruction',
locked: true,
hasUpdatedPriorsInformation: false,
createdDate: '2021-02-23',
modifiedDate: '2023-04-03',
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
// Unknown number of priors referenced - so just match any study
@@ -164,7 +163,6 @@ const mpr: Types.HangingProtocol.Protocol = {
const mprAnd3DVolumeViewport = {
id: 'mprAnd3DVolumeViewport',
locked: true,
hasUpdatedPriorsInformation: false,
name: 'mpr',
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
-2
View File
@@ -24,7 +24,6 @@ import { toolNames } from './initCornerstoneTools';
import { getEnabledElement, reset as enabledElementReset } from './state';
import dicomLoaderService from './utils/dicomLoaderService';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
import { id } from './id';
import * as csWADOImageLoader from './initWADOImageLoader.js';
@@ -62,7 +61,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
imageRetrievalPoolManager.clearRequestStack(type);
});
csWADOImageLoader.destroy();
enabledElementReset();
},
+8 -11
View File
@@ -26,6 +26,7 @@ import nthLoader from './utils/nthLoader';
import interleaveTopToBottom from './utils/interleaveTopToBottom';
import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick';
import { CornerstoneServices } from './types';
// TODO: Cypress tests are currently grabbing this from the window?
window.cornerstone = cornerstone;
@@ -53,12 +54,11 @@ export default async function init({
},
});
// For debugging large datasets
const MAX_CACHE_SIZE_1GB = 1073741824;
const maxCacheSize = appConfig.maxCacheSize;
cornerstone.cache.setMaxCacheSize(
maxCacheSize ? maxCacheSize : MAX_CACHE_SIZE_1GB
);
// For debugging large datasets, otherwise prefer the defaults
const { maxCacheSize } = appConfig;
if (maxCacheSize) {
cornerstone.cache.setMaxCacheSize(maxCacheSize);
}
initCornerstoneTools();
@@ -69,10 +69,7 @@ export default async function init({
const {
userAuthenticationService,
measurementService,
customizationService,
displaySetService,
uiDialogService,
uiModalService,
uiNotificationService,
cineService,
@@ -81,7 +78,7 @@ export default async function init({
toolGroupService,
viewportGridService,
stateSyncService,
} = servicesManager.services;
} = servicesManager.services as CornerstoneServices;
window.services = servicesManager.services;
window.extensionManager = extensionManager;
@@ -166,7 +163,7 @@ export default async function init({
prefetch: appConfig?.maxNumRequests?.prefetch || 10,
};
initWADOImageLoader(userAuthenticationService, appConfig);
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
/* Measurement Service */
this.measurementServiceSource = connectToolsToMeasurementService(
+12 -4
View File
@@ -35,9 +35,15 @@ const DEFAULT_CONTEXT_MENU_CLICKS = {
function getEventName(evt) {
const button = evt.detail.event.which;
const nameArr = [];
if (evt.detail.event.altKey) nameArr.push('alt');
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
if (evt.detail.event.shiftKey) nameArr.push('shift');
if (evt.detail.event.altKey) {
nameArr.push('alt');
}
if (evt.detail.event.ctrlKey) {
nameArr.push('ctrl');
}
if (evt.detail.event.shiftKey) {
nameArr.push('shift');
}
nameArr.push('button');
nameArr.push(button);
return nameArr.join('');
@@ -73,7 +79,9 @@ function initContextMenu({
function elementEnabledHandler(evt) {
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (!viewportInfo) return;
if (!viewportInfo) {
return;
}
const viewportIndex = viewportInfo.getViewportIndex();
// TODO check update upstream
setEnabledElement(viewportIndex, element);
@@ -21,9 +21,15 @@ const DEFAULT_DOUBLE_CLICK = {
*/
function getDoubleClickEventName(evt: CustomEvent) {
const nameArr = [];
if (evt.detail.event.altKey) nameArr.push('alt');
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
if (evt.detail.event.shiftKey) nameArr.push('shift');
if (evt.detail.event.altKey) {
nameArr.push('alt');
}
if (evt.detail.event.ctrlKey) {
nameArr.push('ctrl');
}
if (evt.detail.event.shiftKey) {
nameArr.push('shift');
}
nameArr.push('doubleClick');
return nameArr.join('');
}
@@ -5,7 +5,7 @@ import dicomImageLoader, {
webWorkerManager,
} from '@cornerstonejs/dicom-image-loader';
import dicomParser from 'dicom-parser';
import { errorHandler } from '@ohif/core';
import { errorHandler, utils } from '@ohif/core';
const { registerVolumeLoader } = volumeLoader;
@@ -35,7 +35,8 @@ function initWebWorkers(appConfig) {
export default function initWADOImageLoader(
userAuthenticationService,
appConfig
appConfig,
extensionManager
) {
dicomImageLoader.external.cornerstone = cornerstone;
dicomImageLoader.external.dicomParser = dicomParser;
@@ -55,18 +56,18 @@ export default function initWADOImageLoader(
convertFloatPixelDataToInt: false,
},
beforeSend: function(xhr) {
//TODO should be removed in the future and request emitted by DicomWebDataSource
const sourceConfig =
extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
const headers = userAuthenticationService.getAuthorizationHeader();
const acceptHeader = utils.generateAcceptHeader(
sourceConfig.acceptHeader,
sourceConfig.requestTransferSyntaxUID,
sourceConfig.omitQuotationForMultipartRequest
);
// Request:
// JPEG-LS Lossless (1.2.840.10008.1.2.4.80) if available, otherwise accept
// whatever transfer-syntax the origin server provides.
// For now we use image/jls and image/x-jls because some servers still use the old type
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
const xhrRequestHeaders = {
Accept: appConfig.omitQuotationForMultipartRequest
? 'multipart/related; type=application/octet-stream'
: 'multipart/related; type="application/octet-stream"',
// 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*',
Accept: acceptHeader,
};
if (headers) {
@@ -542,6 +542,7 @@ class SegmentationService extends PubSubService {
id: segmentationId,
displaySetInstanceUID: segDisplaySet.displaySetInstanceUID,
type: representationType,
label: segDisplaySet.SeriesDescription,
representationData: {
[LABELMAP]: {
volumeId: segmentationId,
@@ -562,11 +563,11 @@ class SegmentationService extends PubSubService {
);
}
const { segments, referencedVolumeId } = segDisplaySet;
const { labelmapBufferArray, referencedVolumeId } = segDisplaySet;
if (!segments || !referencedVolumeId) {
if (!labelmapBufferArray || !referencedVolumeId) {
throw new Error(
'To create the segmentation from SEG displaySet, the displaySet should be loaded first, you can perform segDisplaySet.load() before calling this method.'
'No labelmapBufferArray or referencedVolumeId found for the SEG displaySet'
);
}
@@ -592,88 +593,27 @@ class SegmentationService extends PubSubService {
},
}
);
const [rows, columns] = derivedVolume.dimensions;
const derivedVolumeScalarData = derivedVolume.getScalarData();
const { imageIds } = referencedVolume;
const sopUIDImageIdIndexMap = imageIds.reduce((acc, imageId, index) => {
const { sopInstanceUid } = metaData.get('generalImageModule', imageId);
acc[sopInstanceUid] = index;
return acc;
}, {} as { [sopUID: string]: number });
const segmentsInfo = segDisplaySet.segMetadata.data;
derivedVolumeScalarData.set(new Uint8Array(labelmapBufferArray[0]));
const numSegments = Object.keys(segments).length;
// Note: ideally we could use the TypedArray set method, but since each
// slice can have multiple segments, we need to loop over each slice and
// set the segment value for each segment.
let overlappingSegments = false;
const _segmentInfoUpdate = (segmentInfo, segmentIndex) => {
const { pixelData: segPixelData } = segmentInfo;
let segmentX = 0;
let segmentY = 0;
let segmentZ = 0;
let count = 0;
for (const [
functionalGroupIndex,
functionalGroup,
] of segmentInfo.functionalGroups.entries()) {
const {
ReferencedSOPInstanceUID,
} = functionalGroup.DerivationImageSequence.SourceImageSequence;
const imageIdIndex = sopUIDImageIdIndexMap[ReferencedSOPInstanceUID];
if (imageIdIndex === -1) {
return;
}
const step = rows * columns;
// we need a faster way to get the pixel data for the current
// functional group, which we use typed array view
const functionGroupPixelData = new Uint8Array(
segPixelData.buffer,
functionalGroupIndex * step,
step
);
const functionalGroupStartIndex = imageIdIndex * step;
const functionalGroupEndIndex = (imageIdIndex + 1) * step;
// Note: this for loop is not optimized, since DICOM SEG stores
// each segment as a separate labelmap so if there is a slice
// that has multiple segments, we will have to loop over each
// segment and we cannot use the TypedArray set method.
for (
let i = functionalGroupStartIndex, j = 0;
i < functionalGroupEndIndex;
i++, j++
) {
if (functionGroupPixelData[j] !== 0) {
if (derivedVolumeScalarData[i] !== 0) {
overlappingSegments = true;
}
derivedVolumeScalarData[i] = segmentIndex;
// centroid calculations
segmentX += i % columns;
segmentY += Math.floor(i / columns) % rows;
segmentZ += Math.floor(i / (columns * rows));
count++;
}
}
segmentation.segments = segmentsInfo.map((segmentInfo, segmentIndex) => {
if (segmentIndex === 0) {
return;
}
// centroid calculations
const x = Math.floor(segmentX / count);
const y = Math.floor(segmentY / count);
const z = Math.floor(segmentZ / count);
const {
SegmentedPropertyCategoryCodeSequence,
SegmentNumber,
SegmentLabel,
SegmentAlgorithmType,
SegmentAlgorithmName,
SegmentedPropertyTypeCodeSequence,
rgba,
} = segmentInfo;
const { x, y, z } = segDisplaySet.centroids.get(segmentIndex);
const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]);
segmentation.cachedStats = {
@@ -690,65 +630,31 @@ class SegmentationService extends PubSubService {
},
};
const numInitialized = Object.keys(segmentation.cachedStats.segmentCenter)
.length;
// Calculate percentage completed
const percentComplete = Math.round((numInitialized / numSegments) * 100);
this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete,
numSegments: numSegments,
});
};
const promiseArray = [];
for (const segmentIndex in segments) {
const segmentInfo = segments[segmentIndex];
// Important: we need a non-blocking way to update the segmentation
// state, otherwise the UI will freeze and the user will not be able
// to interact with the app or progress bars will not be updated.
const promise = new Promise<void>((resolve, reject) => {
setTimeout(() => {
_segmentInfoUpdate(segmentInfo, segmentIndex);
resolve();
}, 0);
});
promiseArray.push(promise);
}
await Promise.all(promiseArray);
segmentation.segmentCount = Object.keys(segments).length;
segmentation.segments = [null]; // segment 0
Object.keys(segments).forEach(segmentIndex => {
const segmentInfo = segments[segmentIndex];
const segIndex = Number(segmentIndex);
segmentation.segments[segIndex] = {
label: segmentInfo.label || `Segment ${segIndex}`,
segmentIndex: Number(segmentIndex),
color: [
segmentInfo.color[0],
segmentInfo.color[1],
segmentInfo.color[2],
],
opacity: segmentInfo.color[3],
return {
label: SegmentLabel || `Segment ${SegmentNumber}`,
segmentIndex: Number(SegmentNumber),
category: SegmentedPropertyCategoryCodeSequence
? SegmentedPropertyCategoryCodeSequence.CodeMeaning
: '',
type: SegmentedPropertyTypeCodeSequence
? SegmentedPropertyTypeCodeSequence.CodeMeaning
: '',
algorithmType: SegmentAlgorithmType,
algorithmName: SegmentAlgorithmName,
color: rgba,
opacity: 255,
isVisible: true,
isLocked: false,
};
});
segmentation.segmentCount = segmentsInfo.length - 1;
segDisplaySet.isLoaded = true;
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
segmentationId,
segDisplaySet,
overlappingSegments,
});
return this.addOrUpdateSegmentation(segmentation, suppressEvents);
@@ -789,6 +695,7 @@ class SegmentationService extends PubSubService {
id: segmentationId,
displaySetInstanceUID: rtDisplaySetUID,
type: representationType,
label: rtDisplaySet.SeriesDescription,
representationData: {
[CONTOUR]: {
geometryIds,
@@ -816,45 +723,54 @@ class SegmentationService extends PubSubService {
const segmentsCachedStats = {};
const initializeContour = async rtStructData => {
const { data, id, color, segmentIndex, geometryId } = rtStructData;
const geometry = await geometryLoader.createAndCacheGeometry(geometryId, {
geometryData: {
data,
id,
color,
frameOfReferenceUID: structureSet.frameOfReferenceUID,
// catch error instead of failing to allow loading to continue
try {
const geometry = await geometryLoader.createAndCacheGeometry(
geometryId,
{
geometryData: {
data,
id,
color,
frameOfReferenceUID: structureSet.frameOfReferenceUID,
segmentIndex,
},
type: csEnums.GeometryType.CONTOUR,
}
);
const contourSet = geometry.data;
const centroid = contourSet.getCentroid();
segmentsCachedStats[segmentIndex] = {
center: { world: centroid },
modifiedTime: rtDisplaySet.SeriesDate, // we use the SeriesDate as the modifiedTime since this is the first time we are creating the segmentation
};
segmentation.segments[segmentIndex] = {
label: id,
segmentIndex,
},
type: csEnums.GeometryType.CONTOUR,
});
color,
...SEGMENT_CONSTANT,
};
const contourSet = geometry.data;
const centroid = contourSet.getCentroid();
const numInitialized = Object.keys(segmentsCachedStats).length;
segmentsCachedStats[segmentIndex] = {
center: { world: centroid },
modifiedTime: rtDisplaySet.SeriesDate, // we use the SeriesDate as the modifiedTime since this is the first time we are creating the segmentation
};
// Calculate percentage completed
const percentComplete = Math.round(
(numInitialized / allRTStructData.length) * 100
);
segmentation.segments[segmentIndex] = {
label: id,
segmentIndex,
color,
...SEGMENT_CONSTANT,
};
const numInitialized = Object.keys(segmentsCachedStats).length;
// Calculate percentage completed
const percentComplete = Math.round(
(numInitialized / allRTStructData.length) * 100
);
this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete,
// Note: this is not the geometryIds length since there might be
// some missing ROINumbers
numSegments: allRTStructData.length,
});
this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete,
// Note: this is not the geometryIds length since there might be
// some missing ROINumbers
numSegments: allRTStructData.length,
});
} catch (e) {
console.warn(e);
}
};
const promiseArray = [];
@@ -1207,6 +1123,8 @@ class SegmentationService extends PubSubService {
);
}
this._setDisplaySetIsHydrated(segmentationId, true);
segmentation.hydrated = true;
if (!suppressEvents) {
@@ -1216,6 +1134,18 @@ class SegmentationService extends PubSubService {
}
};
private _setDisplaySetIsHydrated(
displaySetUID: string,
isHydrated: boolean
): void {
const {
DisplaySetService: displaySetService,
} = this.servicesManager.services;
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
displaySet.isHydrated = isHydrated;
displaySetService.setDisplaySetMetadataInvalidated(displaySetUID, false);
}
private _highlightLabelmap(
segmentIndex: number,
alpha: number,
@@ -1387,6 +1317,8 @@ class SegmentationService extends PubSubService {
}
}
this._setDisplaySetIsHydrated(segmentationId, false);
this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, {
segmentationId,
});
@@ -15,7 +15,7 @@ const EVENTS = {
* sync group declared.
*/
export type SyncCreator = (
type: string,
id: string,
options?: Record<string, unknown>
) => Synchronizer;
@@ -83,7 +83,7 @@ export default class SyncGroupService {
* @param type is the type of the synchronizer to create
* @param creator
*/
public setSynchronizer(type: string, creator: SyncCreator): void {
public addSynchronizerType(type: string, creator: SyncCreator): void {
this.synchronizerCreators[type.toLowerCase()] = creator;
}
@@ -190,18 +190,26 @@ class CornerstoneViewportService extends PubSubService
public setPresentations(viewport, presentations?: Presentations): void {
const properties = presentations?.lutPresentation?.properties;
if (properties) viewport.setProperties(properties);
if (properties) {
viewport.setProperties(properties);
}
const camera = presentations?.positionPresentation?.camera;
if (camera) viewport.setCamera(camera);
if (camera) {
viewport.setCamera(camera);
}
}
public getPresentation(viewportIndex: number): Presentation {
const viewportInfo = this.viewportsInfo.get(viewportIndex);
if (!viewportInfo) return;
if (!viewportInfo) {
return;
}
const { viewportType, presentationIds } = viewportInfo.getViewportOptions();
const csViewport = this.getCornerstoneViewportByIndex(viewportIndex);
if (!csViewport) return;
if (!csViewport) {
return;
}
const properties = csViewport.getProperties();
if (properties.isComputedVOI) {
@@ -408,7 +416,9 @@ class CornerstoneViewportService extends PubSubService
viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties(properties);
const camera = presentations.positionPresentation?.camera;
if (camera) viewport.setCamera(camera);
if (camera) {
viewport.setCamera(camera);
}
});
}
@@ -745,7 +755,10 @@ class CornerstoneViewportService extends PubSubService
const viewport = this.getCornerstoneViewport(viewportId);
const viewportCamera = viewport.getCamera();
if (viewport instanceof VolumeViewport) {
if (
viewport instanceof VolumeViewport ||
viewport instanceof VolumeViewport3D
) {
this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => {
if (keepCamera) {
viewport.setCamera(viewportCamera);
@@ -95,7 +95,9 @@ const dataContains = (
displaySetUID: string,
imageId?: string
): boolean => {
if (data.displaySetInstanceUID === displaySetUID) return true;
if (data.displaySetInstanceUID === displaySetUID) {
return true;
}
if (imageId && data.isCompositeStack && data.imageIds) {
return !!data.imageIds.find(dataId => dataId === imageId);
}
@@ -123,7 +125,9 @@ class ViewportInfo {
* OR if it is a composite stack and contains the given imageId
*/
public contains(displaySetUID: string, imageId: string): boolean {
if (!this.viewportData?.data) return false;
if (!this.viewportData?.data) {
return false;
}
if (this.viewportData.data.length) {
return !!this.viewportData.data.find(data =>
@@ -198,7 +202,9 @@ class ViewportInfo {
// via cornerstoneViewportService
let viewportData = this.getViewportData();
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC ||
viewportData.viewportType === Enums.ViewportType.VOLUME_3D
) {
viewportData = viewportData as VolumeViewportData;
return viewportData.data.some(
({ displaySetInstanceUID: dsUID }) => dsUID === displaySetInstanceUID
@@ -1,9 +1,9 @@
import { Types } from '@ohif/core';
import ToolGroupService from './services/ToolGroupService';
import SyncGroupService from './services/SyncGroupService';
import SegmentationService from './services/SegmentationService';
import CornerstoneCacheService from './services/CornerstoneCacheService';
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
import ToolGroupService from '../services/ToolGroupService';
import SyncGroupService from '../services/SyncGroupService';
import SegmentationService from '../services/SegmentationService';
import CornerstoneCacheService from '../services/CornerstoneCacheService';
import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService';
interface CornerstoneServices extends Types.Services {
cornerstoneViewportService: CornerstoneViewportService;
@@ -212,7 +212,9 @@ export default class DicomFileUploader extends PubSubService {
}
private _checkDicomFile(arrayBuffer: ArrayBuffer) {
if (arrayBuffer.length <= 132) return false;
if (arrayBuffer.length <= 132) {
return false;
}
const arr = new Uint8Array(arrayBuffer.slice(128, 132));
// bytes from 128 to 132 must be "DICM"
return Array.from('DICM').every((char, i) => char.charCodeAt(0) === arr[i]);
@@ -1,5 +1,5 @@
import React from 'react';
import { Input, Dialog } from '@ohif/ui';
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
/**
*
@@ -35,8 +35,9 @@ function callInputDialog(
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
if (typeof validateFunc === 'function' && !validateFunc(value.label))
if (typeof validateFunc === 'function' && !validateFunc(value.label)) {
return;
}
callback(value.label, action.id);
break;
@@ -60,8 +61,8 @@ function callInputDialog(
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
@@ -7,8 +7,12 @@
* @returns [] reordered to be breadth first traversal of lists
*/
export default function interleave(lists) {
if (!lists || !lists.length) return [];
if (lists.length === 1) return lists[0];
if (!lists || !lists.length) {
return [];
}
if (lists.length === 1) {
return lists[0];
}
console.time('interleave');
const useLists = [...lists];
const ret = [];
@@ -3,7 +3,7 @@ import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
const Angle = {
toAnnotation: measurement => { },
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
@@ -198,7 +198,9 @@ function getDisplayText(mappedAnnotations, displaySet) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
if (angle === undefined) return displayText;
if (angle === undefined) {
return displayText;
}
const roundedAngle = utils.roundNumber(angle, 2);
displayText.push(
`${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`
@@ -1,6 +1,5 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getModalityUnit from './utils/getModalityUnit';
import { utils } from '@ohif/core';
const CircleROI = {
@@ -112,8 +111,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
);
const { SeriesNumber } = displaySet;
const { mean, stdDev, max, area, Modality } = targetStats;
const unit = getModalityUnit(Modality);
const { mean, stdDev, max, area, Modality, modalityUnit } = targetStats;
annotations.push({
SeriesInstanceUID,
@@ -121,7 +119,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
SeriesNumber,
frameNumber,
Modality,
unit,
unit: modalityUnit,
mean,
stdDev,
max,
@@ -3,7 +3,7 @@ import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
const CobbAngle = {
toAnnotation: measurement => { },
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
@@ -198,7 +198,9 @@ function getDisplayText(mappedAnnotations, displaySet) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
if (angle === undefined) return displayText;
if (angle === undefined) {
return displayText;
}
const roundedAngle = utils.roundNumber(angle, 2);
displayText.push(
`${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`
@@ -1,6 +1,5 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getModalityUnit from './utils/getModalityUnit';
import { utils } from '@ohif/core';
const EllipticalROI = {
@@ -112,8 +111,7 @@ function getMappedAnnotations(annotation, displaySetService) {
);
const { SeriesNumber } = displaySet;
const { mean, stdDev, max, area, Modality } = targetStats;
const unit = getModalityUnit(Modality);
const { mean, stdDev, max, area, Modality, modalityUnit } = targetStats;
annotations.push({
SeriesInstanceUID,
@@ -121,7 +119,7 @@ function getMappedAnnotations(annotation, displaySetService) {
SeriesNumber,
frameNumber,
Modality,
unit,
unit: modalityUnit,
mean,
stdDev,
max,
@@ -32,12 +32,15 @@ const Length = {
throw new Error('Tool not supported');
}
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
getSOPInstanceAttributes(
referencedImageId,
cornerstoneViewportService,
viewportId
);
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
cornerstoneViewportService,
viewportId
);
let displaySet;
@@ -101,8 +104,11 @@ function getMappedAnnotations(annotation, displaySetService) {
);
}
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
getSOPInstanceAttributes(referencedImageId);
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
@@ -173,8 +179,12 @@ function getDisplayText(mappedAnnotations, displaySet) {
const displayText = [];
// Area is the same for all series
const { length, SeriesNumber, SOPInstanceUID, frameNumber } =
mappedAnnotations[0];
const {
length,
SeriesNumber,
SOPInstanceUID,
frameNumber,
} = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
@@ -188,7 +198,9 @@ function getDisplayText(mappedAnnotations, displaySet) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
if (length === null || length === undefined) return displayText;
if (length === null || length === undefined) {
return displayText;
}
const roundedLength = utils.roundNumber(length, 2);
displayText.push(
`${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
@@ -2,7 +2,7 @@ import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const PlanarFreehandROI = {
toAnnotation: measurement => { },
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
@@ -1,6 +1,5 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getModalityUnit from './utils/getModalityUnit';
import { utils } from '@ohif/core';
const RectangleROI = {
@@ -26,12 +25,15 @@ const RectangleROI = {
throw new Error('Tool not supported');
}
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
@@ -96,8 +98,11 @@ function getMappedAnnotations(annotation, DisplaySetService) {
);
}
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
getSOPInstanceAttributes(referencedImageId);
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
@@ -106,8 +111,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
);
const { SeriesNumber } = displaySet;
const { mean, stdDev, max, area, Modality } = targetStats;
const unit = getModalityUnit(Modality);
const { mean, stdDev, max, area, Modality, modalityUnit } = targetStats;
annotations.push({
SeriesInstanceUID,
@@ -115,7 +119,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
SeriesNumber,
frameNumber,
Modality,
unit,
unit: modalityUnit,
mean,
stdDev,
max,
@@ -1,11 +0,0 @@
function getModalityUnit(modality) {
if (modality === 'CT') {
return 'HU';
} else if (modality === 'PT') {
return 'SUV';
} else {
return '';
}
}
export default getModalityUnit;
@@ -4,11 +4,9 @@ import {
setAnnotationSelected,
getFirstAnnotationSelected,
} from './selection';
import getModalityUnit from './getModalityUnit';
import getSOPInstanceAttributes from './getSOPInstanceAttributes';
export {
getModalityUnit,
getHandlesFromPoints,
getSOPInstanceAttributes,
isAnnotationSelected,
+46
View File
@@ -0,0 +1,46 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-default
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
### Bug Fixes
* **PT Metadata:** Allow for PatientWeight to be missing from the metadata ([#3621](https://github.com/OHIF/Viewers/issues/3621)) ([44f101d](https://github.com/OHIF/Viewers/commit/44f101d3f2b3204b67e31f4e4939062e65a246ee))
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-default
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
**Note:** Version bump only for package @ohif/extension-default
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-default",
"version": "3.7.0-beta.2",
"version": "3.7.0-beta.61",
"description": "Common/default features and functionality for basic image viewing",
"author": "OHIF Core Team",
"license": "MIT",
@@ -30,8 +30,8 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.2",
"@ohif/i18n": "3.7.0-beta.2",
"@ohif/core": "3.7.0-beta.61",
"@ohif/i18n": "3.7.0-beta.61",
"dcmjs": "^0.29.5",
"dicomweb-client": "^0.10.2",
"prop-types": "^15.6.2",
@@ -0,0 +1,109 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, useModal } from '@ohif/ui';
import { ExtensionManager, ServicesManager, Types } from '@ohif/core';
import DataSourceConfigurationModalComponent from './DataSourceConfigurationModalComponent';
type DataSourceConfigurationComponentProps = {
servicesManager: ServicesManager;
extensionManager: ExtensionManager;
};
function DataSourceConfigurationComponent({
servicesManager,
extensionManager,
}: DataSourceConfigurationComponentProps): ReactElement {
const { t } = useTranslation('DataSourceConfiguration');
const { show, hide } = useModal();
const { customizationService } = servicesManager.services;
const [configurationAPI, setConfigurationAPI] = useState<
Types.BaseDataSourceConfigurationAPI
>();
const [configuredItems, setConfiguredItems] = useState<
Array<Types.BaseDataSourceConfigurationAPIItem>
>();
useEffect(() => {
let shouldUpdate = true;
const dataSourceChangedCallback = async () => {
const activeDataSourceDef = extensionManager.getActiveDataSourceDefinition();
if (!activeDataSourceDef.configuration.configurationAPI) {
return;
}
const { factory: configurationAPIFactory } =
customizationService.get(
activeDataSourceDef.configuration.configurationAPI
) ?? {};
if (!configurationAPIFactory) {
return;
}
const configAPI = configurationAPIFactory(activeDataSourceDef.sourceName);
setConfigurationAPI(configAPI);
configAPI.getConfiguredItems().then(list => {
if (shouldUpdate) {
setConfiguredItems(list);
}
});
};
const sub = extensionManager.subscribe(
extensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED,
dataSourceChangedCallback
);
dataSourceChangedCallback();
return () => {
shouldUpdate = false;
sub.unsubscribe();
};
}, []);
return configuredItems ? (
<div className="flex text-aqua-pale overflow-hidden items-center">
<Icon
name="settings"
className="cursor-pointer shrink-0 w-3.5 h-3.5 mr-2.5"
onClick={() =>
show({
content: DataSourceConfigurationModalComponent,
title: t('Configure Data Source'),
contentProps: {
configurationAPI,
configuredItems,
onHide: hide,
},
})
}
></Icon>
{configuredItems.map((item, itemIndex) => {
return (
<div key={itemIndex} className="flex overflow-hidden">
<div
key={itemIndex}
className="text-ellipsis whitespace-nowrap overflow-hidden"
>
{item.name}
</div>
{itemIndex !== configuredItems.length - 1 && (
<div className="px-2.5">|</div>
)}
</div>
);
})}
</div>
) : (
<></>
);
}
export default DataSourceConfigurationComponent;
@@ -0,0 +1,209 @@
import classNames from 'classnames';
import React, { ReactElement, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon } from '@ohif/ui';
import { Types } from '@ohif/core';
import ItemListComponent from './ItemListComponent';
const NO_WRAP_ELLIPSIS_CLASS_NAMES =
'text-ellipsis whitespace-nowrap overflow-hidden';
type DataSourceConfigurationModalComponentProps = {
configurationAPI: Types.BaseDataSourceConfigurationAPI;
configuredItems: Array<Types.BaseDataSourceConfigurationAPIItem>;
onHide: () => void;
};
function DataSourceConfigurationModalComponent({
configurationAPI,
configuredItems,
onHide,
}: DataSourceConfigurationModalComponentProps) {
const { t } = useTranslation('DataSourceConfiguration');
const [itemList, setItemList] = useState<
Array<Types.BaseDataSourceConfigurationAPIItem>
>();
const [selectedItems, setSelectedItems] = useState(configuredItems);
// Determines whether to show the full configuration for the data source.
// This typically occurs when the configuration component is first displayed.
const [showFullConfig, setShowFullConfig] = useState(true);
const [errorMessage, setErrorMessage] = useState<string>();
const [itemLabels] = useState(configurationAPI.getItemLabels());
/**
* The index of the selected item that is considered current and for which
* its sub-items should be displayed in the items list component. When the
* full/existing configuration for a data source is to be shown, the current
* selected item is the second to last in the `selectedItems` list.
*/
const currentSelectedItemIndex = showFullConfig
? selectedItems.length - 2
: selectedItems.length - 1;
useEffect(() => {
let shouldUpdate = true;
setErrorMessage(null);
// Clear out the former/old list while we fetch the next sub item list.
setItemList(null);
if (selectedItems.length === 0) {
configurationAPI
.initialize()
.then(items => {
if (shouldUpdate) {
setItemList(items);
}
})
.catch(error => setErrorMessage(error.message));
} else if (!showFullConfig && selectedItems.length === itemLabels.length) {
// The last item to configure the data source (path) has been selected.
configurationAPI.setCurrentItem(selectedItems[selectedItems.length - 1]);
// We can hide the modal dialog now.
onHide();
} else {
configurationAPI
.setCurrentItem(selectedItems[currentSelectedItemIndex])
.then(items => {
if (shouldUpdate) {
setItemList(items);
}
})
.catch(error => setErrorMessage(error.message));
}
return () => {
shouldUpdate = false;
};
}, [
selectedItems,
configurationAPI,
onHide,
itemLabels,
showFullConfig,
currentSelectedItemIndex,
]);
const getSelectedItemCursorClasses = itemIndex =>
itemIndex !== itemLabels.length - 1 && itemIndex < selectedItems.length
? 'cursor-pointer'
: 'cursor-auto';
const getSelectedItemBackgroundClasses = itemIndex =>
itemIndex < selectedItems.length
? classNames(
'bg-black/[.4]',
itemIndex !== itemLabels.length - 1
? 'hover:bg-transparent active:bg-secondary-dark'
: ''
)
: 'bg-transparent';
const getSelectedItemBorderClasses = itemIndex =>
itemIndex === currentSelectedItemIndex + 1
? classNames('border-2', 'border-solid', 'border-primary-light')
: itemIndex < selectedItems.length
? 'border border-solid border-primary-active hover:border-primary-light active:border-white'
: 'border border-dashed border-secondary-light';
const getSelectedItemTextClasses = itemIndex =>
itemIndex <= selectedItems.length
? 'text-primary-light'
: 'text-primary-active';
const getErrorComponent = (): ReactElement => {
return (
<div className="flex flex-col gap-4 min-h-[1px] grow">
<div className="text-primary-light text-[20px]">
{t(`Error fetching ${itemLabels[selectedItems.length]} list`)}
</div>
<div className="bg-black text-[14px] grow p-4">{errorMessage}</div>
</div>
);
};
const getSelectedItemsComponent = (): ReactElement => {
return (
<div className="flex gap-4">
{itemLabels.map((itemLabel, itemLabelIndex) => {
return (
<div
key={itemLabel}
className={classNames(
'rounded-md p-3.5 flex flex-col gap-1 shrink min-w-[1px] basis-[200px]',
getSelectedItemCursorClasses(itemLabelIndex),
getSelectedItemBackgroundClasses(itemLabelIndex),
getSelectedItemBorderClasses(itemLabelIndex),
getSelectedItemTextClasses(itemLabelIndex)
)}
onClick={
(showFullConfig && itemLabelIndex < currentSelectedItemIndex) ||
itemLabelIndex <= currentSelectedItemIndex
? () => {
setShowFullConfig(false);
setSelectedItems(theList =>
theList.slice(0, itemLabelIndex)
);
}
: undefined
}
>
<div className="flex gap-2 items-center text-">
{itemLabelIndex < selectedItems.length ? (
<Icon name="status-tracked" />
) : (
<Icon name="status-untracked" />
)}
<div className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES)}>
{t(itemLabel)}
</div>
</div>
{itemLabelIndex < selectedItems.length ? (
<div
className={classNames(
'text-white text-[14px]',
NO_WRAP_ELLIPSIS_CLASS_NAMES
)}
>
{selectedItems[itemLabelIndex].name}
</div>
) : (
<br></br>
)}
</div>
);
})}
</div>
);
};
return (
<div className="h-[calc(100vh-300px)] flex flex-col pt-0.5 gap-4 select-none">
{getSelectedItemsComponent()}
<div className="w-full h-0.5 shrink-0 bg-black"></div>
{errorMessage ? (
getErrorComponent()
) : (
<ItemListComponent
itemLabel={itemLabels[currentSelectedItemIndex + 1]}
itemList={itemList}
onItemClicked={item => {
setShowFullConfig(false);
setSelectedItems(theList => [
...theList.slice(0, currentSelectedItemIndex + 1),
item,
]);
}}
></ItemListComponent>
)}
</div>
);
}
export default DataSourceConfigurationModalComponent;
@@ -0,0 +1,93 @@
import classNames from 'classnames';
import React, { ReactElement, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
Icon,
InputFilterText,
LoadingIndicatorProgress,
} from '@ohif/ui';
import { Types } from '@ohif/core';
type ItemListComponentProps = {
itemLabel: string;
itemList: Array<Types.BaseDataSourceConfigurationAPIItem>;
onItemClicked: (item: Types.BaseDataSourceConfigurationAPIItem) => void;
};
function ItemListComponent({
itemLabel,
itemList,
onItemClicked,
}: ItemListComponentProps): ReactElement {
const { t } = useTranslation('DataSourceConfiguration');
const [filterValue, setFilterValue] = useState('');
useEffect(() => {
setFilterValue('');
}, [itemList]);
return (
<div className="flex flex-col gap-4 min-h-[1px] grow">
<div className="flex justify-between items-center">
<div className="text-primary-light text-[20px]">
{t(`Select ${itemLabel}`)}
</div>
<InputFilterText
className="grow max-w-[40%]"
value={filterValue}
onDebounceChange={setFilterValue}
placeholder={t(`Search ${itemLabel} list`)}
></InputFilterText>
</div>
<div className="flex flex-col relative min-h-[1px] grow text-[14px] bg-black">
{itemList == null ? (
<LoadingIndicatorProgress className={'w-full h-full'} />
) : itemList.length === 0 ? (
<div className="flex flex-col h-full px-6 py-4 items-center justify-center text-primary-light">
<Icon name="magnifier" className="mb-4" />
<span>{t(`No ${itemLabel} available`)}</span>
</div>
) : (
<>
<div className="bg-secondary-dark text-white px-3 py-1.5">
{t(itemLabel)}
</div>
<div className="overflow-auto ohif-scrollbar">
{itemList
.filter(
item =>
!filterValue ||
item.name.toLowerCase().includes(filterValue.toLowerCase())
)
.map(item => {
const border =
'rounded border-transparent border-b-secondary-light border-[1px] hover:border-primary-light';
return (
<div
className={classNames(
'group mx-2 px-6 py-2 flex justify-between items-center hover:text-primary-light hover:bg-primary-dark',
border
)}
key={item.id}
>
<div>{item.name}</div>
<Button
onClick={() => onItemClicked(item)}
className="invisible group-hover:visible"
endIcon={<Icon name="arrow-left" />}
>
{t('Select')}
</Button>
</div>
);
})}
</div>
</>
)}
</div>
</div>
);
}
export default ItemListComponent;
@@ -0,0 +1,234 @@
import { ExtensionManager, Types } from '@ohif/core';
/**
* This file contains the implementations of BaseDataSourceConfigurationAPIItem
* and BaseDataSourceConfigurationAPI for the Google cloud healthcare API. To
* better understand this implementation and/or to implement custom implementations,
* see the platform\core\src\types\DataSourceConfigurationAPI.ts and its JS doc
* comments as a guide.
*/
/**
* The various Google Cloud Healthcare path item types.
*/
enum ItemType {
projects = 0,
locations = 1,
datasets = 2,
dicomStores = 3,
}
interface NamedItem {
name: string;
}
interface Project extends NamedItem {
projectId: string;
}
const initialUrl = 'https://cloudresourcemanager.googleapis.com/v1';
const baseHealthcareUrl = 'https://healthcare.googleapis.com/v1';
class GoogleCloudDataSourceConfigurationAPIItem
implements Types.BaseDataSourceConfigurationAPIItem {
id: string;
name: string;
url: string;
itemType: ItemType;
}
class GoogleCloudDataSourceConfigurationAPI
implements Types.BaseDataSourceConfigurationAPI {
private _extensionManager: ExtensionManager;
private _fetchOptions: { method: string; headers: unknown };
private _dataSourceName: string;
constructor(dataSourceName, servicesManager, extensionManager) {
this._dataSourceName = dataSourceName;
this._extensionManager = extensionManager;
const userAuthenticationService =
servicesManager.services.userAuthenticationService;
this._fetchOptions = {
method: 'GET',
headers: userAuthenticationService.getAuthorizationHeader(),
};
}
getItemLabels = () => ['Project', 'Location', 'Data set', 'DICOM store'];
async initialize(): Promise<Types.BaseDataSourceConfigurationAPIItem[]> {
const url = `${initialUrl}/projects`;
const projects = (await GoogleCloudDataSourceConfigurationAPI._doFetch(
url,
ItemType.projects,
this._fetchOptions
)) as Array<Project>;
if (!projects?.length) {
return [];
}
const projectItems = projects.map(project => {
return {
id: project.projectId,
name: project.name,
itemType: ItemType.projects,
url: `${baseHealthcareUrl}/projects/${project.projectId}`,
};
});
return projectItems;
}
async setCurrentItem(
anItem: Types.BaseDataSourceConfigurationAPIItem
): Promise<Types.BaseDataSourceConfigurationAPIItem[]> {
const googleCloudItem = anItem as GoogleCloudDataSourceConfigurationAPIItem;
if (googleCloudItem.itemType === ItemType.dicomStores) {
// Last configurable item, so update the data source configuration.
const url = `${googleCloudItem.url}/dicomWeb`;
const dataSourceDefCopy = JSON.parse(
JSON.stringify(
this._extensionManager.getDataSourceDefinition(this._dataSourceName)
)
);
dataSourceDefCopy.configuration = {
...dataSourceDefCopy.configuration,
wadoUriRoot: url,
qidoRoot: url,
wadoRoot: url,
};
this._extensionManager.updateDataSourceConfiguration(
dataSourceDefCopy.sourceName,
dataSourceDefCopy.configuration
);
return [];
}
const subItemType = googleCloudItem.itemType + 1;
const subItemField = `${ItemType[subItemType]}`;
const url = `${googleCloudItem.url}/${subItemField}`;
const fetchedSubItems = await GoogleCloudDataSourceConfigurationAPI._doFetch(
url,
subItemType,
this._fetchOptions
);
if (!fetchedSubItems?.length) {
return [];
}
const subItems = fetchedSubItems.map(subItem => {
const nameSplit = subItem.name.split('/');
return {
id: subItem.name,
name: nameSplit[nameSplit.length - 1],
itemType: subItemType,
url: `${baseHealthcareUrl}/${subItem.name}`,
};
});
return subItems;
}
async getConfiguredItems(): Promise<
Array<GoogleCloudDataSourceConfigurationAPIItem>
> {
const dataSourceDefinition = this._extensionManager.getDataSourceDefinition(
this._dataSourceName
);
const url = dataSourceDefinition.configuration.wadoUriRoot;
const projectsIndex = url.indexOf('projects');
const urlSplit = url.substring(projectsIndex).split('/');
const configuredItems = [];
for (let itemType = 0; itemType < 4; itemType += 1) {
if (itemType === ItemType.projects) {
const projectId = urlSplit[1];
const projectUrl = `${initialUrl}/projects/${projectId}`;
const data = await GoogleCloudDataSourceConfigurationAPI._doFetch(
projectUrl,
ItemType.projects,
this._fetchOptions
);
const project = data[0] as Project;
configuredItems.push({
id: project.projectId,
name: project.name,
itemType: itemType,
url: `${baseHealthcareUrl}/projects/${project.projectId}`,
});
} else {
const relativePath = urlSplit.slice(0, itemType * 2 + 2).join('/');
configuredItems.push({
id: relativePath,
name: urlSplit[itemType * 2 + 1],
itemType: itemType,
url: `${baseHealthcareUrl}/${relativePath}`,
});
}
}
return configuredItems;
}
/**
* Fetches an array of items the specified item type.
* @param urlStr the fetch url
* @param fetchItemType the type to fetch
* @param fetchOptions the header options for the fetch (e.g. authorization header)
* @param fetchSearchParams any search query params; currently only used for paging results
* @returns an array of items of the specified type
*/
private static async _doFetch(
urlStr: string,
fetchItemType: ItemType,
fetchOptions = {},
fetchSearchParams: Record<string, string> = {}
): Promise<Array<Project> | Array<NamedItem>> {
try {
const url = new URL(urlStr);
url.search = new URLSearchParams(fetchSearchParams).toString();
const response = await fetch(url, fetchOptions);
const data = await response.json();
if (response.status >= 200 && response.status < 300 && data != null) {
if (data.nextPageToken != null) {
fetchSearchParams.pageToken = data.nextPageToken;
const subPageData = await this._doFetch(
urlStr,
fetchItemType,
fetchOptions,
fetchSearchParams
);
data[ItemType[fetchItemType]] = data[ItemType[fetchItemType]].concat(
subPageData
);
}
if (data[ItemType[fetchItemType]]) {
return data[ItemType[fetchItemType]];
} else if (data.name) {
return [data];
} else {
return [];
}
} else {
const message =
data?.error?.message ||
`Error returned from Google Cloud Healthcare: ${response.status} - ${response.statusText}`;
throw new Error(message);
}
} catch (err) {
const message = err?.message || 'Error occurred during fetch request.';
throw new Error(message);
}
}
}
export { GoogleCloudDataSourceConfigurationAPI };
@@ -13,6 +13,7 @@ const mappings = {
let _store = {
urls: [],
studyInstanceUIDMap: new Map(), // map of urls to array of study instance UIDs
// {
// url: url1
// studies: [Study1, Study2], // if multiple studies
@@ -41,11 +42,13 @@ const findStudies = (key, value) => {
};
function createDicomJSONApi(dicomJsonConfig) {
const { name, wadoRoot } = dicomJsonConfig;
const { wadoRoot } = dicomJsonConfig;
const implementation = {
initialize: async ({ params, query, url }) => {
if (!url) url = query.get('url');
initialize: async ({ query, url }) => {
if (!url) {
url = query.get('url');
}
let metaData = getMetaDataByURL(url);
// if we have already cached the data from this specific url
@@ -58,11 +61,7 @@ function createDicomJSONApi(dicomJsonConfig) {
}
const response = await fetch(url);
let data = await response.json();
const studyInstanceUIDs = data.studies.map(
study => study.StudyInstanceUID
);
const data = await response.json();
let StudyInstanceUID;
let SeriesInstanceUID;
@@ -89,12 +88,14 @@ function createDicomJSONApi(dicomJsonConfig) {
url,
studies: [...data.studies],
});
return studyInstanceUIDs;
_store.studyInstanceUIDMap.set(
url,
data.studies.map(study => study.StudyInstanceUID)
);
},
query: {
studies: {
mapParams: () => { },
mapParams: () => {},
search: async param => {
const [key, value] = Object.entries(param)[0];
const mappedParam = mappings[key];
@@ -213,7 +214,9 @@ function createDicomJSONApi(dicomJsonConfig) {
return obj;
});
storeInstances(instances);
if (index === numberOfSeries - 1) setSuccessFlag();
if (index === numberOfSeries - 1) {
setSuccessFlag();
}
});
},
},
@@ -252,12 +255,13 @@ function createDicomJSONApi(dicomJsonConfig) {
return imageIds;
},
getImageIdsForInstance({ instance, frame }) {
const imageIds = getImageId({
instance,
frame,
});
const imageIds = getImageId({ instance, frame });
return imageIds;
},
getStudyInstanceUIDs: ({ params, query }) => {
const url = query.get('url');
return _store.studyInstanceUIDMap.get(url);
},
};
return IWebApiDataSource.create(implementation);
}
@@ -12,8 +12,12 @@ const END_MODALITIES = {
};
const compareValue = (v1, v2, def = 0) => {
if (v1 === v2) return def;
if (v1 < v2) return -1;
if (v1 === v2) {
return def;
}
if (v1 < v2) {
return -1;
}
return 1;
};
@@ -41,25 +45,7 @@ function createDicomLocalApi(dicomLocalConfig) {
const { name } = dicomLocalConfig;
const implementation = {
initialize: ({ params, query }) => {
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
const StudyInstanceUIDs =
queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
const StudyInstanceUIDsAsArray =
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
? StudyInstanceUIDs
: [StudyInstanceUIDs];
// Put SRs at the end of series list to make sure images are loaded first
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
study.series = study.series.sort(customSort);
});
return StudyInstanceUIDsAsArray;
},
initialize: ({ params, query }) => {},
query: {
studies: {
mapParams: () => {},
@@ -246,6 +232,29 @@ function createDicomLocalApi(dicomLocalConfig) {
deleteStudyMetadataPromise() {
console.log('deleteStudyMetadataPromise not implemented');
},
getStudyInstanceUIDs: ({ params, query }) => {
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
const StudyInstanceUIDs =
queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
const StudyInstanceUIDsAsArray =
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
? StudyInstanceUIDs
: [StudyInstanceUIDs];
// Put SRs at the end of series list to make sure images are loaded first
let isStudyInCache = false;
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
if (study) {
study.series = study.series.sort(customSort);
isStudyInCache = true;
}
});
return isStudyInCache ? StudyInstanceUIDsAsArray : [];
},
};
return IWebApiDataSource.create(implementation);
}
@@ -1,10 +1,9 @@
import dcmjs from 'dcmjs';
import moment from 'moment';
import React, { useState, useMemo, useEffect, useRef } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import { classes } from '@ohif/core';
import { Icon, InputRange, Select, Typography } from '@ohif/ui';
import { InputRange, Select, Typography, InputFilterText } from '@ohif/ui';
import debounce from 'lodash.debounce';
import classNames from 'classnames';
import DicomTagTable from './DicomTagTable';
import './DicomTagBrowser.css';
@@ -34,8 +33,6 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
setInstanceNumber(1);
};
const searchInputRef = useRef(null);
const activeDisplaySet = displaySets.find(
ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID
);
@@ -158,33 +155,11 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
</div>
<div className="w-full h-1 bg-black"></div>
<div className="flex flex-row my-3 w-1/2">
{/* TODO - refactor the following into its own reusable component */}
<label className="relative block w-full mr-8">
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
<Icon name="icon-search"></Icon>
</span>
<input
ref={searchInputRef}
type="text"
className="block bg-black w-full shadow transition duration-300 appearance-none border border-inputfield-main focus:border-inputfield-focus focus:outline-none disabled:border-inputfield-disabled rounded w-full py-2 px-9 text-base leading-tight placeholder:text-inputfield-placeholder"
placeholder="Search metadata..."
onChange={event => debouncedSetFilterValue(event.target.value)}
autoComplete="off"
></input>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
<Icon
name="icon-clear-field"
className={classNames(
'cursor-pointer',
filterValue ? '' : 'hidden'
)}
onClick={() => {
searchInputRef.current.value = '';
debouncedSetFilterValue('');
}}
></Icon>
</span>
</label>
<InputFilterText
className="block w-full mr-8"
placeholder="Search metadata..."
onDebounceChange={setFilterValue}
></InputFilterText>
</div>
<DicomTagTable rows={filteredRows} />
</div>
@@ -1,4 +1,4 @@
export default function (wadoRoot) {
export default function(wadoRoot) {
return {
series: (StudyInstanceUID, SeriesInstanceUID) => {
return new Promise((resolve, reject) => {
@@ -15,7 +15,7 @@ export default function (wadoRoot) {
console.log(xhr);
xhr.onreadystatechange = function () {
xhr.onreadystatechange = function() {
//Call a function when the state changes.
if (xhr.readyState == 4) {
switch (xhr.status) {
@@ -51,74 +51,87 @@ const metadataProvider = classes.MetadataProvider;
* @param {string|bool} singlepart - indicates of the retrieves can fetch singlepart. Options are bulkdata, video, image or boolean true
*/
function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
const {
qidoRoot,
wadoRoot,
enableStudyLazyLoad,
supportsFuzzyMatching,
supportsWildcard,
supportsReject,
staticWado,
singlepart,
} = dicomWebConfig;
const dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig));
const qidoConfig = {
url: qidoRoot,
staticWado,
singlepart,
headers: userAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
};
const wadoConfig = {
url: wadoRoot,
staticWado,
singlepart,
headers: userAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
};
// TODO -> Two clients sucks, but its better than 1000.
// TODO -> We'll need to merge auth later.
const qidoDicomWebClient = staticWado
? new StaticWadoClient(qidoConfig)
: new api.DICOMwebClient(qidoConfig);
const wadoDicomWebClient = staticWado
? new StaticWadoClient(wadoConfig)
: new api.DICOMwebClient(wadoConfig);
let dicomWebConfigCopy,
qidoConfig,
wadoConfig,
qidoDicomWebClient,
wadoDicomWebClient,
getAuthrorizationHeader,
generateWadoHeader;
const implementation = {
initialize: ({ params, query }) => {
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
const queryStudyInstanceUIDs = utils.splitComma(
query.getAll('StudyInstanceUIDs')
);
if (
dicomWebConfig.onConfiguration &&
typeof dicomWebConfig.onConfiguration === 'function'
) {
dicomWebConfig = dicomWebConfig.onConfiguration(dicomWebConfig, {
params,
query,
});
}
const StudyInstanceUIDs =
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) ||
paramsStudyInstanceUIDs;
const StudyInstanceUIDsAsArray =
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
? StudyInstanceUIDs
: [StudyInstanceUIDs];
return StudyInstanceUIDsAsArray;
dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig));
getAuthrorizationHeader = () => {
const xhrRequestHeaders = {};
const authHeaders = userAuthenticationService.getAuthorizationHeader();
if (authHeaders && authHeaders.Authorization) {
xhrRequestHeaders.Authorization = authHeaders.Authorization;
}
return xhrRequestHeaders;
};
generateWadoHeader = () => {
let authorizationHeader = getAuthrorizationHeader();
//Generate accept header depending on config params
let formattedAcceptHeader = utils.generateAcceptHeader(
dicomWebConfig.acceptHeader,
dicomWebConfig.requestTransferSyntaxUID,
dicomWebConfig.omitQuotationForMultipartRequest
);
return {
...authorizationHeader,
Accept: formattedAcceptHeader,
};
};
qidoConfig = {
url: dicomWebConfig.qidoRoot,
staticWado: dicomWebConfig.staticWado,
singlepart: dicomWebConfig.singlepart,
headers: userAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
};
wadoConfig = {
url: dicomWebConfig.wadoRoot,
staticWado: dicomWebConfig.staticWado,
singlepart: dicomWebConfig.singlepart,
headers: userAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
};
// TODO -> Two clients sucks, but its better than 1000.
// TODO -> We'll need to merge auth later.
qidoDicomWebClient = dicomWebConfig.staticWado
? new StaticWadoClient(qidoConfig)
: new api.DICOMwebClient(qidoConfig);
wadoDicomWebClient = dicomWebConfig.staticWado
? new StaticWadoClient(wadoConfig)
: new api.DICOMwebClient(wadoConfig);
},
query: {
studies: {
mapParams: mapParams.bind(),
search: async function(origParams) {
const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
qidoDicomWebClient.headers = getAuthrorizationHeader();
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
mapParams(origParams, {
supportsFuzzyMatching,
supportsWildcard,
supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching,
supportsWildcard: dicomWebConfig.supportsWildcard,
}) || {};
const results = await qidoSearch(
@@ -135,11 +148,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
series: {
// mapParams: mapParams.bind(),
search: async function(studyInstanceUid) {
const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
qidoDicomWebClient.headers = getAuthrorizationHeader();
const results = await seriesInStudy(
qidoDicomWebClient,
studyInstanceUid
@@ -151,11 +160,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
},
instances: {
search: (studyInstanceUid, queryParameters) => {
const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
qidoDicomWebClient.headers = getAuthrorizationHeader();
qidoSearch.call(
undefined,
qidoDicomWebClient,
@@ -179,9 +184,16 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
*/
directURL: params => {
return getDirectURL({ wadoRoot, singlepart }, params);
return getDirectURL(
{
wadoRoot: dicomWebConfig.wadoRoot,
singlepart: dicomWebConfig.singlepart,
},
params
);
},
bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => {
qidoDicomWebClient.headers = getAuthrorizationHeader();
const options = {
multipart: false,
BulkDataURI,
@@ -200,18 +212,13 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
sortFunction,
madeInClient = false,
} = {}) => {
const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) {
wadoDicomWebClient.headers = headers;
}
if (!StudyInstanceUID) {
throw new Error(
'Unable to query for SeriesMetadata without StudyInstanceUID'
);
}
if (enableStudyLazyLoad) {
if (dicomWebConfig.enableStudyLazyLoad) {
return implementation._retrieveSeriesMetadataAsync(
StudyInstanceUID,
filters,
@@ -234,17 +241,12 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
store: {
dicom: async (dataset, request) => {
const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) {
wadoDicomWebClient.headers = headers;
}
wadoDicomWebClient.headers = getAuthrorizationHeader();
if (dataset instanceof ArrayBuffer) {
const options = {
datasets: [dataset],
request,
};
await wadoDicomWebClient.storeInstances(options);
} else {
const meta = {
@@ -282,7 +284,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
madeInClient
) => {
const enableStudyLazyLoad = false;
wadoDicomWebClient.headers = generateWadoHeader();
// data is all SOPInstanceUIDs
const data = await retrieveStudyMetadata(
wadoDicomWebClient,
@@ -353,6 +355,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
madeInClient = false
) => {
const enableStudyLazyLoad = true;
wadoDicomWebClient.headers = generateWadoHeader();
// Get Series
const {
preLoadData: seriesSummaryMetadata,
@@ -515,10 +518,26 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
getConfig() {
return dicomWebConfigCopy;
},
getStudyInstanceUIDs({ params, query }) {
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
const queryStudyInstanceUIDs = utils.splitComma(
query.getAll('StudyInstanceUIDs')
);
const StudyInstanceUIDs =
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) ||
paramsStudyInstanceUIDs;
const StudyInstanceUIDsAsArray =
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
? StudyInstanceUIDs
: [StudyInstanceUIDs];
return StudyInstanceUIDsAsArray;
},
};
if (supportsReject) {
implementation.reject = dcm4cheeReject(wadoRoot);
if (dicomWebConfig.supportsReject) {
implementation.reject = dcm4cheeReject(dicomWebConfig.wadoRoot);
}
return IWebApiDataSource.create(implementation);
@@ -36,12 +36,16 @@ export default class StaticWadoClient extends api.DICOMwebClient {
* @returns
*/
async searchForStudies(options) {
if (!this.staticWado) return super.searchForStudies(options);
if (!this.staticWado) {
return super.searchForStudies(options);
}
const searchResult = await super.searchForStudies(options);
const { queryParams } = options;
if (!queryParams) return searchResult;
if (!queryParams) {
return searchResult;
}
const lowerParams = this.toLowerParams(queryParams);
const filtered = searchResult.filter(study => {
@@ -63,11 +67,15 @@ export default class StaticWadoClient extends api.DICOMwebClient {
}
async searchForSeries(options) {
if (!this.staticWado) return super.searchForSeries(options);
if (!this.staticWado) {
return super.searchForSeries(options);
}
const searchResult = await super.searchForSeries(options);
const { queryParams } = options;
if (!queryParams) return searchResult;
if (!queryParams) {
return searchResult;
}
const lowerParams = this.toLowerParams(queryParams);
const filtered = searchResult.filter(series => {
@@ -112,8 +120,12 @@ export default class StaticWadoClient extends api.DICOMwebClient {
actual = actual.Alphabetic;
}
if (typeof actual == 'string') {
if (actual.length === 0) return true;
if (desired.length === 0 || desired === '*') return true;
if (actual.length === 0) {
return true;
}
if (desired.length === 0 || desired === '*') {
return true;
}
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
// console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`)
return actual.indexOf(desired.substring(1, desired.length - 1)) != -1;
@@ -131,9 +143,13 @@ export default class StaticWadoClient extends api.DICOMwebClient {
/** Compares a pair of dates to see if the value is within the range */
compareDateRange(range, value) {
if (!value) return true;
if (!value) {
return true;
}
const dash = range.indexOf('-');
if (dash === -1) return this.compareValues(range, value);
if (dash === -1) {
return this.compareValues(range, value);
}
const start = range.substring(0, dash);
const end = range.substring(dash + 1);
return (!start || value >= start) && (!end || value <= end);
@@ -150,11 +166,17 @@ export default class StaticWadoClient extends api.DICOMwebClient {
*/
filterItem(key: string, queryParams, study, sourceFilterMap) {
const altKey = sourceFilterMap[key] || key;
if (!queryParams) return true;
if (!queryParams) {
return true;
}
const testValue = queryParams[key] || queryParams[altKey];
if (!testValue) return true;
if (!testValue) {
return true;
}
const valueElem = study[key] || study[altKey];
if (!valueElem) return false;
if (!valueElem) {
return false;
}
if (valueElem.vr == 'DA') {
return this.compareDateRange(testValue, valueElem.Value[0]);
}
@@ -18,15 +18,6 @@ function createDicomWebProxyApi(
const implementation = {
initialize: async ({ params, query }) => {
let studyInstanceUIDs = [];
// there seem to be a couple of variations of the case for this parameter
const queryStudyInstanceUIDs =
query.get('studyInstanceUIDs') || query.get('studyInstanceUids');
if (!queryStudyInstanceUIDs) {
throw new Error(`No studyInstanceUids in request for '${name}'`);
}
const url = query.get('url');
if (!url) {
@@ -39,12 +30,11 @@ function createDicomWebProxyApi(
}
dicomWebDelegate = createDicomWebApi(
data.servers.dicomWeb[0],
data.servers.dicomWeb[0].configuration,
UserAuthenticationService
);
studyInstanceUIDs = queryStudyInstanceUIDs.split(';');
dicomWebDelegate.initialize({ params, query });
}
return studyInstanceUIDs;
},
query: {
studies: {
@@ -77,6 +67,18 @@ function createDicomWebProxyApi(
dicomWebDelegate.getImageIdsForDisplaySet(...args),
getImageIdsForInstance: (...args) =>
dicomWebDelegate.getImageIdsForInstance(...args),
getStudyInstanceUIDs({ params, query }) {
let studyInstanceUIDs = [];
// there seem to be a couple of variations of the case for this parameter
const queryStudyInstanceUIDs =
query.get('studyInstanceUIDs') || query.get('studyInstanceUids');
if (!queryStudyInstanceUIDs) {
throw new Error(`No studyInstanceUids in request for '${name}'`);
}
studyInstanceUIDs = queryStudyInstanceUIDs.split(';');
return studyInstanceUIDs;
},
};
return IWebApiDataSource.create(implementation);
}
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, ButtonGroup } from '@ohif/ui';
import { LegacyButton, ButtonGroup } from '@ohif/ui';
function ActionButtons({ onExportClick, onCreateReportClick }) {
const { t } = useTranslation('MeasurementTable');
@@ -10,12 +10,16 @@ function ActionButtons({ onExportClick, onCreateReportClick }) {
return (
<React.Fragment>
<ButtonGroup color="black" size="inherit">
<Button className="px-2 py-2 text-base" onClick={onExportClick}>
{/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButton className="px-2 py-2 text-base" onClick={onExportClick}>
{t('Export CSV')}
</Button>
<Button className="px-2 py-2 text-base" onClick={onCreateReportClick}>
</LegacyButton>
<LegacyButton
className="px-2 py-2 text-base"
onClick={onCreateReportClick}
>
{t('Create Report')}
</Button>
</LegacyButton>
</ButtonGroup>
</React.Fragment>
);
@@ -3,7 +3,7 @@ import classnames from 'classnames';
import { useNavigate } from 'react-router-dom';
import { useAppConfig } from '@state';
import { Button } from '@ohif/ui';
import { Button, ButtonEnums } from '@ohif/ui';
function DataSourceSelector() {
const [appConfig] = useAppConfig();
@@ -31,9 +31,12 @@ function DataSourceSelector() {
)
.map(ds => (
<div key={ds.sourceName}>
<h1 className="text-white">{ds.friendlyName}</h1>
<h1 className="text-white">
{ds.configuration?.friendlyName || ds.friendlyName}
</h1>
<Button
className={classnames('font-bold', 'ml-2')}
type={ButtonEnums.type.primary}
className={classnames('ml-2')}
onClick={() => {
navigate({
pathname: '/',
@@ -1,7 +1,13 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { utils, ServicesManager } from '@ohif/core';
import { MeasurementTable, Dialog, Input, useViewportGrid } from '@ohif/ui';
import {
MeasurementTable,
Dialog,
Input,
useViewportGrid,
ButtonEnums,
} from '@ohif/ui';
import ActionButtons from './ActionButtons';
import debounce from 'lodash.debounce';
@@ -189,9 +195,8 @@ export default function PanelMeasurementTable({
);
},
actions: [
// temp: swap button types until colors are updated
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
},
@@ -271,7 +276,9 @@ function _mapMeasurementToDisplay(measurement, index, types) {
if (findingSites) {
const siteText = [];
findingSites.forEach(site => {
if (site?.text !== label) siteText.push(site.text);
if (site?.text !== label) {
siteText.push(site.text);
}
});
displayText = [...siteText, ...displayText];
}
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui';
import { utils } from '@ohif/core';
import { useNavigate } from 'react-router-dom';
const { sortStudyInstances, formatDate } = utils;
@@ -21,6 +22,8 @@ function PanelStudyBrowser({
displaySetService,
uiNotificationService,
} = servicesManager.services;
const navigate = useNavigate();
// Normally you nest the components so the tree isn't so deep, and the data
// doesn't have to have such an intense shape. This works well enough for now.
// Tabs --> Studies --> DisplaySets --> Thumbnails
@@ -69,6 +72,11 @@ function PanelStudyBrowser({
studyInstanceUid: StudyInstanceUID,
});
if (!qidoForStudyUID?.length) {
navigate('/notfoundstudy', '_self');
throw new Error('Invalid study URL');
}
let qidoStudiesForPatient = qidoForStudyUID;
// try to fetch the prior studies based on the patientID if the
@@ -123,7 +131,7 @@ function PanelStudyBrowser({
const imageId = imageIds[Math.floor(imageIds.length / 2)];
// TODO: Is it okay that imageIds are not returned here for SR displaySets?
if (imageId) {
if (imageId && !displaySet?.unsupported) {
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
imageId
@@ -167,20 +175,22 @@ function PanelStudyBrowser({
const displaySet = displaySetService.getDisplaySetByUID(
dSet.displaySetInstanceUID
);
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
const imageId = imageIds[Math.floor(imageIds.length / 2)];
if (!displaySet?.unsupported) {
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
const imageId = imageIds[Math.floor(imageIds.length / 2)];
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
if (imageId) {
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
imageId,
dSet.initialViewport
);
if (isMounted.current) {
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
if (imageId) {
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
imageId,
dSet.initialViewport
);
if (isMounted.current) {
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
}
}
}
});
@@ -200,9 +210,22 @@ function PanelStudyBrowser({
}
);
const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
() => {
const mappedDisplaySets = _mapDisplaySets(
displaySetService.getActiveDisplaySets(),
thumbnailImageSrcMap
);
setDisplaySets(mappedDisplaySets);
}
);
return () => {
SubscriptionDisplaySetsAdded.unsubscribe();
SubscriptionDisplaySetsChanged.unsubscribe();
SubscriptionDisplaySetMetaDataInvalidated.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -300,7 +323,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
.filter(ds => !ds.excludeFromThumbnailBrowser)
.forEach(ds => {
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
const componentType = _getComponentType(ds.Modality);
const componentType = _getComponentType(ds);
const array =
componentType === 'thumbnail'
@@ -317,6 +340,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
numInstances: ds.numImageFrames,
countIcon: ds.countIcon,
StudyInstanceUID: ds.StudyInstanceUID,
messages: ds.messages,
componentType,
imageSrc,
dragData: {
@@ -324,6 +348,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
displaySetInstanceUID: ds.displaySetInstanceUID,
// .. Any other data to pass
},
isHydratedForDerivedDisplaySet: ds.isHydrated,
});
});
@@ -339,8 +364,8 @@ const thumbnailNoImageModalities = [
'RTDOSE',
];
function _getComponentType(Modality) {
if (thumbnailNoImageModalities.includes(Modality)) {
function _getComponentType(ds) {
if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) {
// TODO probably others.
return 'thumbnailNoImage';
}
@@ -1,6 +1,6 @@
/* eslint-disable react/display-name */
import React from 'react';
import { Dialog, Input, Select } from '@ohif/ui';
import { ButtonEnums, Dialog, Input, Select } from '@ohif/ui';
export const CREATE_REPORT_DIALOG_RESPONSE = {
CANCEL: 0,
@@ -81,8 +81,8 @@ export default function createReportDialogPrompt(
noCloseButton: true,
onClose: _handleClose,
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
],
// TODO: Should be on button press...
onSubmit: _handleFormSubmit,
+6 -2
View File
@@ -9,12 +9,16 @@ function debounce(func, wait, immediate) {
args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
if (!immediate) {
func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
if (callNow) {
func.apply(context, args);
}
};
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default function Toolbar({ servicesManager }) {
return (
<>
{toolbarButtons.map((toolDef, index) => {
{toolbarButtons.map(toolDef => {
const { id, Component, componentProps } = toolDef;
// TODO: ...
@@ -40,8 +40,8 @@ NestedMenu.propTypes = {
};
NestedMenu.defaultProps = {
icon: "tool-more-menu",
label: "More",
icon: 'tool-more-menu',
label: 'More',
};
export default NestedMenu;
@@ -55,8 +55,11 @@ function ViewerLayout({
const query = new URLSearchParams(window.location.search);
const configUrl = query.get('configUrl');
const dataSourceName = pathname.substring(dataSourceIdx + 1);
const existingDataSource = extensionManager.getDataSources(dataSourceName);
const searchQuery = new URLSearchParams();
if (dataSourceIdx !== -1) {
if (dataSourceIdx !== -1 && existingDataSource) {
searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1));
}
+6 -2
View File
@@ -129,13 +129,17 @@ const commandsModule = ({
stage,
} = hangingProtocolService.getActiveProtocol();
const enableListener = button => {
if (!button.id) return;
if (!button.id) {
return;
}
const { commands, items } = button.props || button;
if (items) {
items.forEach(enableListener);
}
const hpCommand = commands?.find?.(isHangingProtocolCommand);
if (!hpCommand) return;
if (!hpCommand) {
return;
}
const { protocolId, stageIndex, stageId } = hpCommand.commandOptions;
const isActive =
(!protocolId || protocolId === protocol.id) &&
@@ -22,7 +22,9 @@ export const findOrCreateViewport = (
options: Record<string, unknown>
) => {
const byPositionViewport = viewportsByPosition?.[positionId];
if (byPositionViewport) return { ...byPositionViewport };
if (byPositionViewport) {
return { ...byPositionViewport };
}
const { protocolId, stageIndex } = hangingProtocolService.getState();
// Setup the initial in display correctly for initial view/select
@@ -1,6 +1,8 @@
import { CustomizationService } from '@ohif/core';
import React from 'react';
import DataSourceSelector from './Panels/DataSourceSelector';
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
/**
*
@@ -11,7 +13,10 @@ import DataSourceSelector from './Panels/DataSourceSelector';
* custom page for the user to view their profile, or to add a custom
* page for login etc.
*/
export default function getCustomizationModule() {
export default function getCustomizationModule({
servicesManager,
extensionManager,
}) {
return [
{
name: 'helloPage',
@@ -83,17 +88,21 @@ export default function getCustomizationModule() {
*/
{
id: 'ohif.overlayItem',
content: function (props) {
if (this.condition && !this.condition(props)) return null;
content: function(props) {
if (this.condition && !this.condition(props)) {
return null;
}
const { instance } = props;
const value =
instance && this.attribute
? instance[this.attribute]
: this.contentF && typeof this.contentF === 'function'
? this.contentF(props)
: null;
if (!value) return null;
? this.contentF(props)
: null;
if (!value) {
return null;
}
return (
<span
@@ -117,7 +126,7 @@ export default function getCustomizationModule() {
* This function clones the object and child objects to prevent
* changes to the original customization object.
*/
transform: function (customizationService: CustomizationService) {
transform: function(customizationService: CustomizationService) {
// Don't modify the children, as those are copied by reference
const clonedObject = { ...this };
clonedObject.menus = this.menus.map(menu => ({ ...menu }));
@@ -132,6 +141,26 @@ export default function getCustomizationModule() {
return clonedObject;
},
},
{
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
id: 'ohif.dataSourceConfigurationComponent',
component: DataSourceConfigurationComponent.bind(null, {
servicesManager,
extensionManager,
}),
},
{
// The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare
id: 'ohif.dataSourceConfigurationAPI.google',
factory: (dataSourceName: string) =>
new GoogleCloudDataSourceConfigurationAPI(
dataSourceName,
servicesManager,
extensionManager
),
},
],
},
];
@@ -0,0 +1,50 @@
import sortInstancesByPosition from '@ohif/core/src/utils/sortInstancesByPosition';
import { constructableModalities } from '@ohif/core/src/utils/isDisplaySetReconstructable';
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
import checkMultiFrame from './utils/validations/checkMultiframe';
import checkSingleFrames from './utils/validations/checkSingleFrames';
/**
* Checks if a series is reconstructable to a 3D volume.
*
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
*/
export default function getDisplaySetMessages(
instances: Array<any>,
isReconstructable: boolean
): DisplaySetMessageList {
const messages = new DisplaySetMessageList();
if (!instances.length) {
messages.addMessage(DisplaySetMessage.CODES.NO_VALID_INSTANCES);
}
const firstInstance = instances[0];
// Due to current requirements, LOCALIZER series doesn't have any messages
if (firstInstance.ImageType.includes('LOCALIZER')) {
return messages;
}
const Modality = firstInstance.Modality;
if (!constructableModalities.includes(Modality)) {
return messages;
}
const isMultiframe = firstInstance.NumberOfFrames > 1;
// Can't reconstruct if all instances don't have the ImagePositionPatient.
if (
!isMultiframe &&
!instances.every(instance => instance.ImagePositionPatient)
) {
messages.addMessage(DisplaySetMessage.CODES.NO_POSITION_INFORMATION);
}
const sortedInstances = sortInstancesByPosition(instances);
isMultiframe
? checkMultiFrame(sortedInstances[0], messages)
: checkSingleFrames(sortedInstances, messages);
if (!isReconstructable) {
messages.addMessage(DisplaySetMessage.CODES.NOT_RECONSTRUCTABLE);
}
return messages;
}
@@ -0,0 +1,30 @@
import ImageSet from '@ohif/core/src/classes/ImageSet';
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
/**
* Default handler for a instance list with an unsupported sopClassUID
*/
export default function getDisplaySetsFromUnsupportedSeries(instances) {
const imageSet = new ImageSet(instances);
const messages = new DisplaySetMessageList();
messages.addMessage(DisplaySetMessage.CODES.UNSUPPORTED_DISPLAYSET);
const instance = instances[0];
imageSet.setAttributes({
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
SeriesDate: instance.SeriesDate,
SeriesTime: instance.SeriesTime,
SeriesInstanceUID: instance.SeriesInstanceUID,
StudyInstanceUID: instance.StudyInstanceUID,
SeriesNumber: instance.SeriesNumber || 0,
FrameRate: instance.FrameTime,
SOPClassUID: instance.SOPClassUID,
SeriesDescription: instance.SeriesDescription || '',
Modality: instance.Modality,
numImageFrames: instances.length,
unsupported: true,
SOPClassHandlerId: 'unsupported',
isReconstructable: false,
messages,
});
return [imageSet];
}
Loaded 100 of 812 files, more files were not shown because too many files have changed in this diff. Show more