Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5856ae2464 | ||
|
|
84b7de5d96 | ||
|
|
35fc30c535 | ||
|
|
eafc716d00 | ||
|
|
88d695cf4f | ||
|
|
44f101d3f2 | ||
|
|
a9d6869198 | ||
|
|
77c102f993 | ||
|
|
75f61f85fe | ||
|
|
9fbdf37f44 | ||
|
|
7ecd2f1628 | ||
|
|
a336992971 | ||
|
|
a2ef2b0fcb | ||
|
|
eb57c2f8bd | ||
|
|
65b49aeb1b | ||
|
|
fe0ee050ed | ||
|
|
ee6f399787 | ||
|
|
b117a54aa1 | ||
|
|
ede6300099 | ||
|
|
a8717c8625 | ||
|
|
0f9b36906f | ||
|
|
3fe676638b | ||
|
|
d69dab70e1 | ||
|
|
f845f87716 | ||
|
|
de6976df3b | ||
|
|
ea9903e92b | ||
|
|
ed0b01cd39 | ||
|
|
2c891dae60 | ||
|
|
bb4297af35 | ||
|
|
3c4795d8cb | ||
|
|
c6d5f719e7 | ||
|
|
a5bfae6e27 | ||
|
|
53c16b928f | ||
|
|
ecbe7c56e0 | ||
|
|
c4007d4d0d | ||
|
|
19408c987f | ||
|
|
9c51f17c60 | ||
|
|
e664fffbc0 | ||
|
|
884577a23d | ||
|
|
799d0793fd | ||
|
|
7074c6d806 | ||
|
|
41dc424d4a | ||
|
|
88ef7b733f | ||
|
|
2c38f6ba8c | ||
|
|
5302e5b62b | ||
|
|
aedb9d1576 | ||
|
|
daf734e09e | ||
|
|
b55518e08e | ||
|
|
ca1287d99e | ||
|
|
86e6d28ade | ||
|
|
5d435424e2 | ||
|
|
9ae3d9f769 | ||
|
|
348acb9091 | ||
|
|
8adf663248 | ||
|
|
4f037ae55e | ||
|
|
55a50c9d38 | ||
|
|
156c1ba60b | ||
|
|
ec76ea24dd | ||
|
|
431b307fab | ||
|
|
0493a0d587 | ||
|
|
58d38495f0 | ||
|
|
862f255be2 | ||
|
|
72207e6c78 | ||
|
|
6d3b3d191f | ||
|
|
d8867d3804 | ||
|
|
019dedce34 | ||
|
|
4d104add05 | ||
|
|
924a5478f3 | ||
|
|
d8f628b125 | ||
|
|
cab8d322c2 | ||
|
|
3ef26cc670 | ||
|
|
1efe2db662 | ||
|
|
72933af984 | ||
|
|
2645e3c7aa | ||
|
|
81ab52784a |
No files matched your search
@@ -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,3 +1,4 @@
|
||||
[submodule "testdata"]
|
||||
path = testdata
|
||||
url = https://github.com/OHIF/viewer-testdata-dicomweb.git
|
||||
branch = main
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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))
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
@@ -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',
|
||||
|
||||
@@ -61,7 +61,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
imageRetrievalPoolManager.clearRequestStack(type);
|
||||
});
|
||||
|
||||
csWADOImageLoader.destroy();
|
||||
enabledElementReset();
|
||||
},
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
+234
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 },
|
||||
|
||||
+9
-3
@@ -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
Reference in new issue
Block a user