Skip to content

Commit

Permalink
feat(measurements): Provide for the Load (SR) measurements button to …
Browse files Browse the repository at this point in the history
…optionally clear existing measurements prior to loading the SR. (#4586)
  • Loading branch information
jbocce authored Dec 18, 2024
1 parent 7eec015 commit 4d3d5e7
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 128 deletions.
Original file line number Diff line number Diff line change
@@ -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 }) {
Expand Down Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) {
Expand Down Expand Up @@ -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>
);
Expand Down
29 changes: 27 additions & 2 deletions extensions/cornerstone-dicom-sr/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
/**
*
Expand Down Expand Up @@ -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 = {
Expand All @@ -132,6 +154,9 @@ const commandsModule = (props: withAppTypes) => {
storeMeasurements: {
commandFn: actions.storeMeasurements,
},
loadSRMeasurements: {
commandFn: actions.loadSRMeasurements,
},
};

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;

Expand All @@ -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
Expand All @@ -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],
},
]);
}
};
}

/**
Expand Down Expand Up @@ -296,8 +267,8 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
servicesManager,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<>
Expand Down
28 changes: 26 additions & 2 deletions extensions/cornerstone-dicom-sr/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';
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');
Expand All @@ -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,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion extensions/default/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const commandsModule = ({
});
},
clearMeasurements: () => {
measurementService.clear();
measurementService.clearMeasurements();
},

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
Expand Down
26 changes: 26 additions & 0 deletions extensions/measurement-tracking/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
/**
Expand All @@ -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;
Loading

0 comments on commit 4d3d5e7

Please sign in to comment.