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
296 changed files with 8813 additions and 4489 deletions

No files matched your search

+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 -1
View File
@@ -79,6 +79,11 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
module: {
noParse: [/(codec)/, /(dicomicc)/],
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'source-map-loader',
},
transpileJavaScriptRule(mode),
loadWebWorkersRule,
// loadShadersRule,
@@ -126,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({
@@ -150,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))
+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
+8 -4
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>
@@ -163,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 |
| ---------------------------- | ------------------------------------------------------------- |
@@ -249,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
@@ -257,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 @@
7bbc213f58a1be3560c0414242549007885d0f13
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.36",
"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.36",
"@ohif/extension-cornerstone": "3.7.0-beta.36",
"@ohif/extension-default": "3.7.0-beta.36",
"@ohif/i18n": "3.7.0-beta.36",
"@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 };
@@ -12,6 +12,7 @@ function promptHydrateRT({
rtDisplaySet,
viewportIndex,
toolGroupId = 'default',
preHydrateCallbacks,
}) {
const { uiViewportDialogService } = servicesManager.services;
@@ -22,6 +23,10 @@ function promptHydrateRT({
);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
@@ -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.36",
"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.36",
"@ohif/extension-cornerstone": "3.7.0-beta.36",
"@ohif/extension-default": "3.7.0-beta.36",
"@ohif/i18n": "3.7.0-beta.36",
"@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({
@@ -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 };
@@ -7,7 +7,12 @@ const RESPONSE = {
HYDRATE_SEG: 5,
};
function promptHydrateSEG({ servicesManager, segDisplaySet, viewportIndex }) {
function promptHydrateSEG({
servicesManager,
segDisplaySet,
viewportIndex,
preHydrateCallbacks,
}) {
const { uiViewportDialogService } = servicesManager.services;
return new Promise(async function(resolve, reject) {
@@ -17,6 +22,10 @@ function promptHydrateSEG({ servicesManager, segDisplaySet, viewportIndex }) {
);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
@@ -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
@@ -93,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'
@@ -159,6 +167,7 @@ function OHIFCornerstoneSEGViewport(props) {
servicesManager,
viewportIndex,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
@@ -248,8 +257,6 @@ function OHIFCornerstoneSEGViewport(props) {
toolGroupId
);
setToolGroupCreated(true);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
@@ -309,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,
@@ -317,7 +331,6 @@ function OHIFCornerstoneSEGViewport(props) {
setIsHydrated(isHydrated);
};
return (
<>
<ViewportActionBar
@@ -344,10 +357,13 @@ function OHIFCornerstoneSEGViewport(props) {
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}mm` : '',
thickness: SliceThickness
? utils.roundNumber(SliceThickness, 2)
: '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing:
SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
? utils.roundNumber(SpacingBetweenSlices, 2)
: '',
scanner: ManufacturerModelName || '',
},
@@ -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.36",
"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.36",
"@ohif/extension-cornerstone": "3.7.0-beta.36",
"@ohif/extension-measurement-tracking": "3.7.0-beta.36",
"@ohif/ui": "3.7.0-beta.36",
"@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.5.0",
"@cornerstonejs/core": "^1.5.0",
"@cornerstonejs/tools": "^1.5.0",
"@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(
@@ -508,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.36",
"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.4.1",
"@ohif/core": "3.7.0-beta.36",
"@ohif/ui": "3.7.0-beta.36",
"@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.5.0",
"@cornerstonejs/core": "^1.5.0",
"@cornerstonejs/streaming-image-volume-loader": "^1.5.0",
"@cornerstonejs/tools": "^1.5.0",
"@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,6 +113,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
onElementDisabled,
isJumpToMeasurementDisabled,
@@ -127,8 +124,6 @@ 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();
@@ -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
);
@@ -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;
@@ -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;
@@ -28,6 +28,7 @@ function commandsModule({
toolGroupService,
cineService,
toolbarService,
stateSyncService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
@@ -616,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 = {
@@ -740,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;
@@ -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',
-1
View File
@@ -61,7 +61,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
imageRetrievalPoolManager.clearRequestStack(type);
});
csWADOImageLoader.destroy();
enabledElementReset();
},
+5 -6
View File
@@ -54,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();
+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('');
}
@@ -563,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'
);
}
@@ -593,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 = {
@@ -691,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);
@@ -1218,6 +1123,8 @@ class SegmentationService extends PubSubService {
);
}
this._setDisplaySetIsHydrated(segmentationId, true);
segmentation.hydrated = true;
if (!suppressEvents) {
@@ -1227,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,
@@ -1398,6 +1317,8 @@ class SegmentationService extends PubSubService {
}
}
this._setDisplaySetIsHydrated(segmentationId, false);
this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, {
segmentationId,
});
@@ -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 || viewport instanceof VolumeViewport3D) {
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 =>
@@ -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]);
@@ -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;
@@ -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})`
@@ -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})`
@@ -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.
@@ -25,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;
@@ -95,8 +98,11 @@ function getMappedAnnotations(annotation, DisplaySetService) {
);
}
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
getSOPInstanceAttributes(referencedImageId);
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
+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.36",
"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.36",
"@ohif/i18n": "3.7.0-beta.36",
"@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 };
@@ -46,7 +46,9 @@ function createDicomJSONApi(dicomJsonConfig) {
const implementation = {
initialize: async ({ query, url }) => {
if (!url) url = query.get('url');
if (!url) {
url = query.get('url');
}
let metaData = getMetaDataByURL(url);
// if we have already cached the data from this specific url
@@ -212,7 +214,9 @@ function createDicomJSONApi(dicomJsonConfig) {
return obj;
});
storeInstances(instances);
if (index === numberOfSeries - 1) setSuccessFlag();
if (index === numberOfSeries - 1) {
setSuccessFlag();
}
});
},
},
@@ -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;
};
@@ -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) {
@@ -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]);
}
@@ -276,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';
}
+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);
}
};
}
@@ -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];
}
@@ -1,4 +1,5 @@
import hpMNGrid from './hpMNGrid';
import hpMNCompare from './hpCompare';
const defaultProtocol = {
id: 'default',
@@ -6,7 +7,6 @@ const defaultProtocol = {
// Don't store this hanging protocol as it applies to the currently active
// display set by default
// cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'Default',
createdDate: '2021-02-23T19:22:08.894Z',
modifiedDate: '2023-04-01',
@@ -108,6 +108,11 @@ function getHangingProtocolModule() {
name: hpMNGrid.id,
protocol: hpMNGrid,
},
// Create a MxN comparison hanging protocol available by default
{
name: hpMNCompare.id,
protocol: hpMNCompare,
},
];
}
@@ -19,7 +19,6 @@ export default function getPTImageIdInstanceMetadata(
if (
dicomMetaData.SeriesDate === undefined ||
dicomMetaData.SeriesTime === undefined ||
dicomMetaData.PatientWeight === undefined ||
dicomMetaData.CorrectedImage === undefined ||
dicomMetaData.Units === undefined ||
!dicomMetaData.RadiopharmaceuticalInformationSequence ||
@@ -38,6 +37,10 @@ export default function getPTImageIdInstanceMetadata(
throw new Error('required metadata are missing');
}
if (dicomMetaData.PatientWeight === undefined) {
console.warn('PatientWeight missing from PT instance metadata');
}
const instanceMetadata: InstanceMetadata = {
CorrectedImage: dicomMetaData.CorrectedImage,
Units: dicomMetaData.Units,
@@ -3,6 +3,8 @@ import sopClassDictionary from '@ohif/core/src/utils/sopClassDictionary';
import ImageSet from '@ohif/core/src/classes/ImageSet';
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
import { id } from './id';
import getDisplaySetMessages from './getDisplaySetMessages';
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
const sopClassHandlerName = 'stack';
@@ -18,8 +20,9 @@ const makeDisplaySet = instances => {
value: isReconstructable,
averageSpacingBetweenFrames,
} = isDisplaySetReconstructable(instances);
// set appropriate attributes to image set...
const messages = getDisplaySetMessages(instances, isReconstructable);
imageSet.setAttributes({
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
SeriesDate: instance.SeriesDate,
@@ -36,6 +39,7 @@ const makeDisplaySet = instances => {
numImageFrames: instances.length,
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
isReconstructable,
messages,
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
});
@@ -210,6 +214,11 @@ function getSopClassHandlerModule() {
sopClassUids,
getDisplaySetsFromSeries,
},
{
name: 'not-supported-display-sets-handler',
sopClassUids: [],
getDisplaySetsFromSeries: getDisplaySetsFromUnsupportedSeries,
},
];
}
+193
View File
@@ -0,0 +1,193 @@
import { Types } from '@ohif/core';
const defaultDisplaySetSelector = {
studyMatchingRules: [
{
// The priorInstance is a study counter that indicates what position this study is in
// and the value comes from the options parameter.
attribute: 'studyInstanceUIDsIndex',
from: 'options',
required: true,
constraint: {
equals: { value: 0 },
},
},
],
seriesMatchingRules: [
{
attribute: 'numImageFrames',
constraint: {
greaterThan: { value: 0 },
},
},
// This display set will select the specified items by preference
// It has no affect if nothing is specified in the URL.
{
attribute: 'isDisplaySetFromUrl',
weight: 10,
constraint: {
equals: true,
},
},
],
};
const priorDisplaySetSelector = {
studyMatchingRules: [
{
// The priorInstance is a study counter that indicates what position this study is in
// and the value comes from the options parameter.
attribute: 'studyInstanceUIDsIndex',
from: 'options',
required: true,
constraint: {
equals: { value: 1 },
},
},
],
seriesMatchingRules: [
{
attribute: 'numImageFrames',
constraint: {
greaterThan: { value: 0 },
},
},
// This display set will select the specified items by preference
// It has no affect if nothing is specified in the URL.
{
attribute: 'isDisplaySetFromUrl',
weight: 10,
constraint: {
equals: true,
},
},
],
};
const currentDisplaySet = {
id: 'defaultDisplaySetId',
};
const priorDisplaySet = {
id: 'priorDisplaySetId',
};
const currentViewport0 = {
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [currentDisplaySet],
};
const currentViewport1 = {
...currentViewport0,
displaySets: [
{
...currentDisplaySet,
matchedDisplaySetsIndex: 1,
},
],
};
const priorViewport0 = {
...currentViewport0,
displaySets: [priorDisplaySet],
};
const priorViewport1 = {
...priorViewport0,
displaySets: [
{
...priorDisplaySet,
matchedDisplaySetsIndex: 1,
},
],
};
/**
* This hanging protocol can be activated on the primary mode by directly
* referencing it in a URL or by directly including it within a mode, e.g.:
* `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL
* It is not included in the viewer mode by default.
*/
const hpMNCompare: Types.HangingProtocol.Protocol = {
id: '@ohif/hpCompare',
description: 'Compare two studies in various layouts',
name: 'Compare Two Studies',
numberOfPriorsReferenced: 1,
protocolMatchingRules: [
{
id: 'Two Studies',
weight: 1000,
attribute: 'StudyInstanceUID',
// The 'from' attribute says where to get the 'attribute' value from. In this case
// prior means the second study in the study list.
from: 'prior',
required: true,
constraint: {
notNull: true,
},
},
],
toolGroupIds: ['default'],
displaySetSelectors: {
defaultDisplaySetId: defaultDisplaySetSelector,
priorDisplaySetId: priorDisplaySetSelector,
},
defaultViewport: {
viewportOptions: {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: -1,
},
],
},
stages: [
{
name: '2x2',
stageActivation: {
enabled: {
minViewportsMatched: 4,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
currentViewport0,
priorViewport0,
currentViewport1,
priorViewport1,
],
},
{
name: '2x1',
stageActivation: {
enabled: {
minViewportsMatched: 2,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 2,
},
},
viewports: [currentViewport0, priorViewport0],
},
],
};
export default hpMNCompare;
-1
View File
@@ -7,7 +7,6 @@ import { Types } from '@ohif/core';
* It is not included in the viewer mode by default.
*/
const hpMN: Types.HangingProtocol.Protocol = {
hasUpdatedPriorsInformation: false,
id: '@ohif/mnGrid',
description: 'Has various hanging protocol grid layouts',
name: '2x2',
@@ -0,0 +1,20 @@
import { vec3 } from 'gl-matrix';
/**
* Calculates the scanAxisNormal based on a image orientation vector extract from a frame
* @param {*} imageOrientation
* @returns
*/
export default function calculateScanAxisNormal(imageOrientation) {
const rowCosineVec = vec3.fromValues(
imageOrientation[0],
imageOrientation[1],
imageOrientation[2]
);
const colCosineVec = vec3.fromValues(
imageOrientation[3],
imageOrientation[4],
imageOrientation[5]
);
return vec3.cross(vec3.create(), rowCosineVec, colCosineVec);
}
+6 -2
View File
@@ -23,9 +23,13 @@ const getDirectURL = (config, params) => {
singlepart: fetchPart = 'video',
} = params;
const value = instance[tag];
if (!value) return undefined;
if (!value) {
return undefined;
}
if (value.DirectRetrieveURL) return value.DirectRetrieveURL;
if (value.DirectRetrieveURL) {
return value.DirectRetrieveURL;
}
if (value.InlineBinary) {
const blob = utils.b64toBlob(value.InlineBinary, defaultType);
value.DirectRetrieveURL = URL.createObjectURL(blob);
@@ -47,10 +47,14 @@ const reuseCachedLayout = (
for (let idx = 0; idx < state.viewports.length; idx++) {
const viewport = state.viewports[idx];
const { displaySetOptions, displaySetInstanceUIDs } = viewport;
if (!displaySetOptions) continue;
if (!displaySetOptions) {
continue;
}
for (let i = 0; i < displaySetOptions.length; i++) {
const displaySetUID = displaySetInstanceUIDs[i];
if (!displaySetUID) continue;
if (!displaySetUID) {
continue;
}
if (idx === activeViewportIndex && i === 0) {
displaySetSelectorMap[
`${activeStudyUID}:activeDisplaySet:0`
@@ -0,0 +1,26 @@
import toNumber from '@ohif/core/src/utils/toNumber';
/**
* Check if all voxels in series images has same number of components (samplesPerPixel)
* @param {*} instances
* @returns
*/
export default function areAllImageComponentsEqual(
instances: Array<any>
): boolean {
if (!instances?.length) {
return false;
}
const firstImage = instances[0];
const firstImageSamplesPerPixel = toNumber(firstImage.SamplesPerPixel);
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const { SamplesPerPixel } = instance;
if (SamplesPerPixel !== firstImageSamplesPerPixel) {
return false;
}
}
return true;
}
@@ -0,0 +1,27 @@
import toNumber from '@ohif/core/src/utils/toNumber';
/**
* Check if the frames in a series has different dimensions
* @param {*} instances
* @returns
*/
export default function areAllImageDimensionsEqual(
instances: Array<any>
): boolean {
if (!instances?.length) {
return false;
}
const firstImage = instances[0];
const firstImageRows = toNumber(firstImage.Rows);
const firstImageColumns = toNumber(firstImage.Columns);
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const { Rows, Columns } = instance;
if (Rows !== firstImageRows || Columns !== firstImageColumns) {
return false;
}
}
return true;
}
@@ -0,0 +1,31 @@
import toNumber from '@ohif/core/src/utils/toNumber';
import { _isSameOrientation } from '@ohif/core/src/utils/isDisplaySetReconstructable';
/**
* Check is the series has frames with different orientations
* @param {*} instances
* @returns
*/
export default function areAllImageOrientationsEqual(
instances: Array<any>
): boolean {
if (!instances?.length) {
return false;
}
const firstImage = instances[0];
const firstImageOrientationPatient = toNumber(
firstImage.ImageOrientationPatient
);
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const imageOrientationPatient = toNumber(instance.ImageOrientationPatient);
if (
!_isSameOrientation(imageOrientationPatient, firstImageOrientationPatient)
) {
return false;
}
}
return true;
}
@@ -0,0 +1,76 @@
import { vec3 } from 'gl-matrix';
import toNumber from '@ohif/core/src/utils/toNumber';
import { _getPerpendicularDistance } from '@ohif/core/src/utils/isDisplaySetReconstructable';
import calculateScanAxisNormal from '../calculateScanAxisNormal';
/**
* Checks if there is a position shift between consecutive frames
* @param {*} previousPosition
* @param {*} actualPosition
* @param {*} scanAxisNormal
* @param {*} averageSpacingBetweenFrames
* @returns
*/
function _checkSeriesPositionShift(
previousPosition,
actualPosition,
scanAxisNormal,
averageSpacingBetweenFrames
) {
// predicted position should be the previous position added by the multiplication
// of the scanAxisNormal and the average spacing between frames
const predictedPosition = vec3.scaleAndAdd(
vec3.create(),
previousPosition,
scanAxisNormal,
averageSpacingBetweenFrames
);
return (
vec3.distance(actualPosition, predictedPosition) >
averageSpacingBetweenFrames
);
}
/**
* Checks if a series has position shifts between consecutive frames
* @param {*} instances
* @returns
*/
export default function areAllImagePositionsEqual(
instances: Array<any>
): boolean {
if (!instances?.length) {
return false;
}
const firstImageOrientationPatient = toNumber(
instances[0].ImageOrientationPatient
);
const scanAxisNormal = calculateScanAxisNormal(firstImageOrientationPatient);
const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient);
const lastIpp = toNumber(
instances[instances.length - 1].ImagePositionPatient
);
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
(instances.length - 1);
let previousImagePositionPatient = firstImagePositionPatient;
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
if (
_checkSeriesPositionShift(
previousImagePositionPatient,
imagePositionPatient,
scanAxisNormal,
averageSpacingBetweenFrames
)
) {
return false;
}
previousImagePositionPatient = imagePositionPatient;
}
return true;
}
@@ -0,0 +1,67 @@
import {
_getPerpendicularDistance,
_getSpacingIssue,
reconstructionIssues,
} from '@ohif/core/src/utils/isDisplaySetReconstructable';
import { DisplaySetMessage } from '@ohif/core';
import toNumber from '@ohif/core/src/utils/toNumber';
import { DisplaySetMessageList } from '@ohif/core';
/**
* Checks if series has spacing issues
* @param {*} instances
* @param {*} warnings
*/
export default function areAllImageSpacingEqual(
instances: Array<any>,
messages: DisplaySetMessageList
): void {
if (!instances?.length) {
return;
}
const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient);
const lastIpp = toNumber(
instances[instances.length - 1].ImagePositionPatient
);
const averageSpacingBetweenFrames =
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
(instances.length - 1);
let previousImagePositionPatient = firstImagePositionPatient;
const issuesFound = [];
for (let i = 1; i < instances.length; i++) {
const instance = instances[i];
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
const spacingBetweenFrames = _getPerpendicularDistance(
imagePositionPatient,
previousImagePositionPatient
);
const spacingIssue = _getSpacingIssue(
spacingBetweenFrames,
averageSpacingBetweenFrames
);
if (spacingIssue) {
const issue = spacingIssue.issue;
// avoid multiple warning of the same thing
if (!issuesFound.includes(issue)) {
issuesFound.push(issue);
if (issue === reconstructionIssues.MISSING_FRAMES) {
messages.addMessage(DisplaySetMessage.CODES.MISSING_FRAMES);
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
messages.addMessage(DisplaySetMessage.CODES.IRREGULAR_SPACING);
}
}
// we just want to find issues not how many
if (issuesFound.length > 1) {
break;
}
}
previousImagePositionPatient = imagePositionPatient;
}
}
@@ -0,0 +1,32 @@
import {
hasPixelMeasurements,
hasOrientation,
hasPosition,
} from '@ohif/core/src/utils/isDisplaySetReconstructable';
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
/**
* Check various multi frame issues. It calls OHIF core functions
* @param {*} multiFrameInstance
* @param {*} warnings
*/
export default function checkMultiFrame(
multiFrameInstance,
messages: DisplaySetMessageList
): void {
if (!hasPixelMeasurements(multiFrameInstance)) {
messages.addMessage(
DisplaySetMessage.CODES.MULTIFRAME_NO_PIXEL_MEASUREMENTS
);
}
if (!hasOrientation(multiFrameInstance)) {
messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_ORIENTATION);
}
if (!hasPosition(multiFrameInstance)) {
messages.addMessage(
DisplaySetMessage.CODES.MULTIFRAME_NO_POSITION_INFORMATION
);
}
}
@@ -0,0 +1,37 @@
import areAllImageDimensionsEqual from './areAllImageDimensionsEqual';
import areAllImageComponentsEqual from './areAllImageComponentsEqual';
import areAllImageOrientationsEqual from './areAllImageOrientationsEqual';
import areAllImagePositionsEqual from './areAllImagePositionsEqual';
import areAllImageSpacingEqual from './areAllImageSpacingEqual';
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
/**
* Runs various checks in a single frame series
* @param {*} instances
* @param {*} warnings
*/
export default function checkSingleFrames(
instances: Array<any>,
messages: DisplaySetMessageList
): void {
if (instances.length > 2) {
if (!areAllImageDimensionsEqual(instances)) {
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_DIMENSIONS);
}
if (!areAllImageComponentsEqual(instances)) {
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_COMPONENTS);
}
if (!areAllImageOrientationsEqual(instances)) {
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_ORIENTATIONS);
}
if (!areAllImagePositionsEqual(instances)) {
messages.addMessage(
DisplaySetMessage.CODES.INCONSISTENT_POSITION_INFORMATION
);
}
areAllImageSpacingEqual(instances, messages);
}
}
+40
View File
@@ -0,0 +1,40 @@
# 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-dicom-microscopy
# [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-dicom-microscopy
# [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-dicom-microscopy
# [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-dicom-microscopy
# [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-dicom-microscopy
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-microscopy",
"version": "3.7.0-beta.36",
"version": "3.7.0-beta.61",
"description": "OHIF extension for DICOM microscopy",
"author": "Bill Wallace, md-prog",
"license": "MIT",
@@ -28,10 +28,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0-beta.36",
"@ohif/extension-default": "3.7.0-beta.36",
"@ohif/i18n": "3.7.0-beta.36",
"@ohif/ui": "3.7.0-beta.36",
"@ohif/core": "3.7.0-beta.61",
"@ohif/extension-default": "3.7.0-beta.61",
"@ohif/i18n": "3.7.0-beta.61",
"@ohif/ui": "3.7.0-beta.61",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -41,7 +41,9 @@ class DicomMicroscopyViewport extends Component {
const { microscopyService } = this.props.servicesManager.services;
this.microscopyService = microscopyService;
this.debouncedResize = debounce(() => {
if (this.viewer) this.viewer.resize();
if (this.viewer) {
this.viewer.resize();
}
}, 100);
}
@@ -107,8 +107,12 @@ const itemGenerator = (props: any) => {
props.formatTime = formatDICOMTime;
props.formatPN = formatPN;
props.formatNumberPrecision = formatNumberPrecision;
if (condition && !condition(props)) return null;
if (!contents && !valueFunc) return null;
if (condition && !condition(props)) {
return null;
}
if (!contents && !valueFunc) {
return null;
}
const value = valueFunc && valueFunc(props);
const contentsValue = (contents && contents(props)) || [
{ className: 'mr-1', value: title },
@@ -1,10 +1,16 @@
const listComponentGenerator = props => {
const { list, itemGenerator } = props;
if (!list) return;
if (!list) {
return;
}
return list.map(item => {
if (!item) return;
if (!item) {
return;
}
const generator = item.generator || itemGenerator;
if (!generator) throw new Error(`No generator for ${item}`);
if (!generator) {
throw new Error(`No generator for ${item}`);
}
return generator({ ...props, item });
});
};
@@ -22,7 +22,9 @@ export default function getCommandsModule({
deleteMeasurement: ({ uid }) => {
if (uid) {
const roiAnnotation = microscopyService.getAnnotation(uid);
if (roiAnnotation) microscopyService.removeAnnotation(roiAnnotation);
if (roiAnnotation) {
microscopyService.removeAnnotation(roiAnnotation);
}
}
},
@@ -72,7 +74,7 @@ export default function getCommandsModule({
].indexOf(toolName) >= 0
) {
// TODO: read from configuration
let options = {
const options = {
geometryType: toolName,
vertexEnabled: true,
styleOptions: styles.default,
@@ -140,7 +142,9 @@ export default function getCommandsModule({
);
let onoff = false; // true if this will toggle on
for (let i = 0; i < overlays.length; i++) {
if (i === 0) onoff = overlays.item(0).classList.contains('hidden');
if (i === 0) {
onoff = overlays.item(0).classList.contains('hidden');
}
overlays.item(i).classList.toggle('hidden');
}
@@ -5,7 +5,9 @@
* @returns {string} formatted name
*/
export default function formatDICOMPatientName(name) {
if (typeof name !== 'string') return;
if (typeof name !== 'string') {
return;
}
/**
* Convert the first ^ to a ', '. String.replace() only affects
Loaded 100 of 296 files, more files were not shown because too many files have changed in this diff. Show more