feat(measurements): Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. (#4586)

This commit is contained in:
Joe Boccanfuso authored and GitHub committed 2024-12-18 11:33:40 -05:00
1 parent 7eec015ff1
commit 4d3d5e794c
15 files changed
+185 -128

No files matched your search

@@ -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 }) {
<span className="ml-1">RTSTRUCT</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
@@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Tooltip } from '@ohif/ui';
import { 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 }) {
<span className="ml-1">SEG</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
@@ -5,6 +5,7 @@ import dcmjs from 'dcmjs';
import { adaptersSR } from '@cornerstonejs/adapters';
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { log } = OHIF;
@@ -41,8 +42,8 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
};
const commandsModule = (props: withAppTypes) => {
const { servicesManager } = props;
const { customizationService } = servicesManager.services;
const { servicesManager, extensionManager, commandsManager } = props;
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
const actions = {
/**
*
@@ -123,6 +124,27 @@ const commandsModule = (props: withAppTypes) => {
throw new Error(error.message || 'Error while saving the measurements.');
}
},
/**
* Loads measurements by hydrating and loading the SR for the given display set instance UID
* and displays it in the active viewport.
*/
loadSRMeasurements: ({ displaySetInstanceUID }) => {
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
{
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
},
]);
}
},
};
const definitions = {
@@ -132,6 +154,9 @@ const commandsModule = (props: withAppTypes) => {
storeMeasurements: {
commandFn: actions.storeMeasurements,
},
loadSRMeasurements: {
commandFn: actions.loadSRMeasurements,
},
};
return {
@@ -1,13 +1,11 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ExtensionManager } from '@ohif/core';
import { ExtensionManager, useToolbar } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { Icons } from '@ohif/ui-next';
@@ -19,10 +17,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const [appConfig] = useAppConfig();
const { displaySetService, measurementService, viewportActionCornersService } =
servicesManager.services;
const { displaySetService, viewportActionCornersService } = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@@ -47,7 +42,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
// Optional hook into tracking extension, if present.
let trackedMeasurements;
let sendTrackedMeasurementsEvent;
const hasMeasurementTrackingExtension = extensionManager.registeredExtensionIds.includes(
MEASUREMENT_TRACKING_EXTENSION_ID
@@ -60,29 +54,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const tracked = useContext(contextModule.context);
trackedMeasurements = tracked?.[0];
sendTrackedMeasurementsEvent = tracked?.[1];
}
if (!sendTrackedMeasurementsEvent) {
// if no panels from measurement-tracking extension is used, this code will run
trackedMeasurements = null;
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
measurementService.clearMeasurements();
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
{
viewportId: activeViewportId,
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
},
]);
}
};
}
/**
@@ -296,8 +267,8 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
servicesManager,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
@@ -316,15 +287,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
isLocked,
onMeasurementChange,
sendTrackedMeasurementsEvent,
srDisplaySet,
t,
viewportActionCornersService,
viewportId,
]);
}, [isLocked, onMeasurementChange, srDisplaySet, t, viewportActionCornersService, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -412,16 +375,9 @@ function _getStatusComponent({
viewportId,
isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
servicesManager,
}) {
const handleMouseUp = () => {
sendTrackedMeasurementsEvent('HYDRATE_SR', {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportId,
});
};
const loadStr = t('LOAD');
// 1 - Incompatible
@@ -467,23 +423,46 @@ function _getStatusComponent({
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</div>;
}
const StatusArea = () => (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SR</span>
</div>
{state === 3 && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={handleMouseUp}
>
{loadStr}
const StatusArea = () => {
const { toolbarButtons: loadSRMeasurementsButtons, onInteraction } = useToolbar({
servicesManager,
buttonSection: 'loadSRMeasurements',
});
const commandOptions = {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportId,
};
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SR</span>
</div>
)}
</div>
);
{state === 3 && (
<>
{loadSRMeasurementsButtons.map(toolDef => {
if (!toolDef) {
return null;
}
const { id, Component, componentProps } = toolDef;
const tool = (
<Component
key={id}
id={id}
onInteraction={args => onInteraction({ ...args, ...commandOptions })}
{...componentProps}
/>
);
return <div key={id}>{tool}</div>;
})}
</>
)}
</div>
);
};
return (
<>
+26 -2
View File
@@ -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 => (
<ViewportActionButton {...props}>{i18n.t('Common:LOAD')}</ViewportActionButton>
),
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,
@@ -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();
+1 -1
View File
@@ -98,7 +98,7 @@ const commandsModule = ({
});
},
clearMeasurements: () => {
measurementService.clear();
measurementService.clearMeasurements();
},
/**
@@ -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 (
<TrackedMeasurementsContext.Provider
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
@@ -1,7 +1,11 @@
import React from 'react';
import getContextModule from './getContextModule';
import getPanelModule from './getPanelModule';
import getViewportModule from './getViewportModule';
import { id } from './id.js';
import { ViewportActionButton } from '@ohif/ui';
import i18n from '@ohif/i18n';
const measurementTrackingExtension = {
/**
@@ -12,6 +16,28 @@ const measurementTrackingExtension = {
getContextModule,
getPanelModule,
getViewportModule,
onModeEnter({ servicesManager }) {
const { toolbarService } = servicesManager.services;
toolbarService.addButtons(
[
{
// A button for loading tracked, SR measurements.
// Note that the command run is registered in TrackedMeasurementsContext
// because it must be bound to a React context's data.
id: 'loadSRMeasurements',
component: props => (
<ViewportActionButton {...props}>{i18n.t('Common:LOAD')}</ViewportActionButton>
),
props: {
commands: ['loadTrackedSRMeasurements'],
},
},
],
true // replace the button if it is already defined
);
},
};
export default measurementTrackingExtension;