From 4d3d5e794cb99212eba06bf91dbb30a258725efe Mon Sep 17 00:00:00 2001
From: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
Date: Wed, 18 Dec 2024 11:33:40 -0500
Subject: [PATCH] feat(measurements): Provide for the Load (SR) measurements
button to optionally clear existing measurements prior to loading the SR.
(#4586)
---
.../src/viewports/_getStatusComponent.tsx | 10 +-
.../src/viewports/_getStatusComponent.tsx | 10 +-
.../src/commandsModule.ts | 29 ++++-
.../OHIFCornerstoneSRMeasurementViewport.tsx | 109 +++++++-----------
extensions/cornerstone-dicom-sr/src/index.tsx | 28 ++++-
.../src/utils/hydrateStructuredReport.ts | 2 +-
extensions/default/src/commandsModule.ts | 2 +-
.../TrackedMeasurementsContext.tsx | 8 ++
extensions/measurement-tracking/src/index.tsx | 26 +++++
.../services/ToolBarService/ToolbarService.ts | 15 ++-
.../ViewportActionButton.tsx | 28 +++++
.../components/ViewportActionButton/index.tsx | 2 +
platform/ui/src/components/index.js | 2 +
platform/ui/src/index.js | 1 +
yarn.lock | 41 +------
15 files changed, 185 insertions(+), 128 deletions(-)
create mode 100644 platform/ui/src/components/ViewportActionButton/ViewportActionButton.tsx
create mode 100644 platform/ui/src/components/ViewportActionButton/index.tsx
diff --git a/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx b/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx
index fcf4de300a2..d270f278d09 100644
--- a/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx
+++ b/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
-import { Icon, Tooltip } from '@ohif/ui';
+import { Icon, Tooltip, ViewportActionButton } from '@ohif/ui';
import { Icons } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
@@ -35,13 +35,7 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
RTSTRUCT
{!isHydrated && (
-
-
-
- SR
-
- {state === 3 && (
-
- {loadStr}
+ const StatusArea = () => {
+ const { toolbarButtons: loadSRMeasurementsButtons, onInteraction } = useToolbar({
+ servicesManager,
+ buttonSection: 'loadSRMeasurements',
+ });
+
+ const commandOptions = {
+ displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
+ viewportId,
+ };
+
+ return (
+
- );
+ {state === 3 && (
+ <>
+ {loadSRMeasurementsButtons.map(toolDef => {
+ if (!toolDef) {
+ return null;
+ }
+ const { id, Component, componentProps } = toolDef;
+ const tool = (
+
onInteraction({ ...args, ...commandOptions })}
+ {...componentProps}
+ />
+ );
+
+ return {tool}
;
+ })}
+ >
+ )}
+
+ );
+ };
return (
<>
diff --git a/extensions/cornerstone-dicom-sr/src/index.tsx b/extensions/cornerstone-dicom-sr/src/index.tsx
index 02380b11594..ff0c9e3cf9a 100644
--- a/extensions/cornerstone-dicom-sr/src/index.tsx
+++ b/extensions/cornerstone-dicom-sr/src/index.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import { srProtocol } from './getHangingProtocolModule';
-import onModeEnter from './onModeEnter';
import getCommandsModule from './commandsModule';
import preRegistration from './init';
import { id } from './id.js';
@@ -9,6 +8,8 @@ import toolNames from './tools/toolNames';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
import Enums from './enums';
+import { ViewportActionButton } from '@ohif/ui';
+import i18n from '@ohif/i18n';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport');
@@ -30,7 +31,30 @@ const dicomSRExtension = {
* Only required property. Should be a unique value across all extensions.
*/
id,
- onModeEnter,
+
+ onModeEnter({ servicesManager }) {
+ const { toolbarService } = servicesManager.services;
+
+ toolbarService.addButtons([
+ {
+ // A base/default button for loading measurements. It is added to the toolbar below.
+ // Customizations to this button can be made in the mode or by another extension.
+ // For example, the button label can be changed and/or the command to clear
+ // the measurements can be dropped.
+ id: 'loadSRMeasurements',
+ component: props => (
+
{i18n.t('Common:LOAD')}
+ ),
+ props: {
+ commands: ['clearMeasurements', 'loadSRMeasurements'],
+ },
+ },
+ ]);
+
+ // The toolbar used in the viewport's status bar. Modes and extensions can further customize
+ // it to optionally add other buttons.
+ toolbarService.createButtonSection('loadSRMeasurements', ['loadSRMeasurements']);
+ },
preRegistration,
diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts
index bc44814f755..43fb346ddf3 100644
--- a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts
+++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts
@@ -41,7 +41,7 @@ const convertSites = (codingValues, sites) => {
*
*/
export default function hydrateStructuredReport(
- { servicesManager, extensionManager, appConfig }: withAppTypes,
+ { servicesManager, extensionManager }: withAppTypes,
displaySetInstanceUID
) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts
index 8916911af3a..bac5b4a2aed 100644
--- a/extensions/default/src/commandsModule.ts
+++ b/extensions/default/src/commandsModule.ts
@@ -98,7 +98,7 @@ const commandsModule = ({
});
},
clearMeasurements: () => {
- measurementService.clear();
+ measurementService.clearMeasurements();
},
/**
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx
index 5b8c008fad1..cd5b6f22baa 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx
@@ -272,6 +272,14 @@ function TrackedMeasurementsContextProvider(
viewports,
]);
+ useEffect(() => {
+ // The command needs to be bound to the context's sendTrackedMeasurementsEvent
+ // so the command has to be registered in a React component.
+ commandsManager.registerCommand('DEFAULT', 'loadTrackedSRMeasurements', {
+ commandFn: props => sendTrackedMeasurementsEvent('HYDRATE_SR', props),
+ });
+ }, [commandsManager, sendTrackedMeasurementsEvent]);
+
return (
(
+ {i18n.t('Common:LOAD')}
+ ),
+ props: {
+ commands: ['loadTrackedSRMeasurements'],
+ },
+ },
+ ],
+ true // replace the button if it is already defined
+ );
+ },
};
export default measurementTrackingExtension;
diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts
index f8cc6addc7d..11d5383f24b 100644
--- a/platform/core/src/services/ToolBarService/ToolbarService.ts
+++ b/platform/core/src/services/ToolBarService/ToolbarService.ts
@@ -128,10 +128,11 @@ export default class ToolbarService extends PubSubService {
/**
* Adds buttons to the toolbar.
* @param buttons - The buttons to be added.
+ * @param replace - Flag indicating if any existing button with the same id as one being added should be replaced
*/
- public addButtons(buttons: Button[]): void {
+ public addButtons(buttons: Button[], replace: boolean = false): void {
buttons.forEach(button => {
- if (!this.state.buttons[button.id]) {
+ if (replace || !this.state.buttons[button.id]) {
if (!button.props) {
button.props = {};
}
@@ -383,12 +384,18 @@ export default class ToolbarService extends PubSubService {
/**
* Creates a button section with the specified key and buttons.
+ * Buttons already in the section (i.e. with the same ids) will NOT be added twice.
* @param {string} key - The key of the button section.
* @param {Array} buttons - The buttons to be added to the section.
*/
createButtonSection(key, buttons) {
if (this.state.buttonSections[key]) {
- this.state.buttonSections[key].push(...buttons);
+ this.state.buttonSections[key].push(
+ buttons.filter(
+ button =>
+ !this.state.buttonSections[key].find(sectionButton => sectionButton.id === button.id)
+ )
+ );
} else {
this.state.buttonSections[key] = buttons;
}
@@ -452,7 +459,7 @@ export default class ToolbarService extends PubSubService {
const buttonType = buttonTypes[uiType];
- if (!buttonType) {
+ if (!buttonType && !component) {
return;
}
diff --git a/platform/ui/src/components/ViewportActionButton/ViewportActionButton.tsx b/platform/ui/src/components/ViewportActionButton/ViewportActionButton.tsx
new file mode 100644
index 00000000000..d1624205694
--- /dev/null
+++ b/platform/ui/src/components/ViewportActionButton/ViewportActionButton.tsx
@@ -0,0 +1,28 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+function ViewportActionButton({ onInteraction, commands, id, children }) {
+ return (
+ {
+ onInteraction({
+ itemId: id,
+ commands,
+ });
+ }}
+ >
+ {children}
+
+ );
+}
+
+ViewportActionButton.propTypes = {
+ id: PropTypes.string,
+ onInteraction: PropTypes.func.isRequired,
+ commands: PropTypes.array,
+ children: PropTypes.node,
+};
+
+export default ViewportActionButton;
diff --git a/platform/ui/src/components/ViewportActionButton/index.tsx b/platform/ui/src/components/ViewportActionButton/index.tsx
new file mode 100644
index 00000000000..ede7d99daa9
--- /dev/null
+++ b/platform/ui/src/components/ViewportActionButton/index.tsx
@@ -0,0 +1,2 @@
+import ViewportActionButton from './ViewportActionButton';
+export default ViewportActionButton;
diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js
index 0787c8a5391..cd80a766c2f 100644
--- a/platform/ui/src/components/index.js
+++ b/platform/ui/src/components/index.js
@@ -86,6 +86,7 @@ import LabellingFlow from './Labelling';
import SwitchButton, { SwitchLabelLocation } from './SwitchButton';
import * as AllInOneMenu from './AllInOneMenu';
import ViewportActionArrows from './ViewportActionArrows';
+import ViewportActionButton from './ViewportActionButton';
import HeaderPatientInfo from './HeaderPatientInfo';
import LegacySplitButton from './LegacySplitButton';
import { ToolSettings } from './AdvancedToolbox';
@@ -180,6 +181,7 @@ export {
Viewport,
ViewportActionArrows,
ViewportActionBar,
+ ViewportActionButton,
ViewportActionCorners,
ViewportActionCornersLocations,
ViewportDownloadForm,
diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js
index b6c3de3a292..2efd0594d3a 100644
--- a/platform/ui/src/index.js
+++ b/platform/ui/src/index.js
@@ -115,6 +115,7 @@ export {
Viewport,
ViewportActionArrows,
ViewportActionBar,
+ ViewportActionButton,
ViewportActionCorners,
ViewportActionCornersLocations,
ViewportDownloadForm,
diff --git a/yarn.lock b/yarn.lock
index 010ea44ec19..e17113b8907 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3203,7 +3203,7 @@
"@docusaurus/theme-search-algolia" "3.6.1"
"@docusaurus/types" "3.6.1"
-"@docusaurus/react-loadable@5.5.2":
+"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
version "5.5.2"
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
@@ -20853,14 +20853,6 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
dependencies:
"@babel/runtime" "^7.10.3"
-"react-loadable@npm:@docusaurus/react-loadable@5.5.2":
- version "5.5.2"
- resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
- integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
- dependencies:
- "@types/react" "*"
- prop-types "^15.6.2"
-
"react-loadable@npm:@docusaurus/react-loadable@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz#de6c7f73c96542bd70786b8e522d535d69069dc4"
@@ -22773,7 +22765,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
-"string-width-cjs@npm:string-width@^4.2.0":
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -22791,15 +22783,6 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@@ -22910,7 +22893,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -22931,13 +22914,6 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.0, strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -25255,7 +25231,7 @@ worker-loader@3.0.8, worker-loader@^3.0.8:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -25281,15 +25257,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"