Files
ohif-viewer/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx
2025-05-27 11:05:07 +07:00

293 lines
11 KiB
TypeScript

import React, { useContext, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Machine } from 'xstate';
import { useMachine } from '@xstate/react';
import { useViewportGrid } from '@ohif/ui';
import { promptLabelAnnotation, promptSaveReport } from '@ohif/extension-default';
import { machineConfiguration, defaultOptions, RESPONSE } from './measurementTrackingMachine';
import promptBeginTracking from './promptBeginTracking';
import promptTrackNewSeries from './promptTrackNewSeries';
import promptTrackNewStudy from './promptTrackNewStudy';
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
import hydrateStructuredReport from './hydrateStructuredReport';
import { useAppConfig } from '@state';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
const SR_SOPCLASSHANDLERID = '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr';
/**
*
* @param {*} param0
*/
function TrackedMeasurementsContextProvider(
{ servicesManager, commandsManager, extensionManager }: withAppTypes, // Bound by consumer
{ children } // Component props
) {
const [appConfig] = useAppConfig();
const [viewportGrid, viewportGridService] = useViewportGrid();
const { activeViewportId, viewports } = viewportGrid;
const { measurementService, displaySetService, customizationService } = servicesManager.services;
const machineOptions = Object.assign({}, defaultOptions);
machineOptions.actions = Object.assign({}, machineOptions.actions, {
jumpToFirstMeasurementInActiveViewport: (ctx, evt) => {
const { trackedStudy, trackedSeries, activeViewportId } = ctx;
const measurements = measurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
);
console.log(
'jumping to measurement reset viewport',
activeViewportId,
trackedMeasurements[0]
);
const referencedDisplaySetUID = trackedMeasurements[0].displaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referencedDisplaySetUID);
const referencedImages = referencedDisplaySet.images;
const isVolumeIdReferenced = referencedImages[0].imageId.startsWith('volumeId');
const measurementData = trackedMeasurements[0].data;
let imageIndex = 0;
if (!isVolumeIdReferenced && measurementData) {
// if it is imageId referenced find the index of the imageId, we don't have
// support for volumeId referenced images yet
imageIndex = referencedImages.findIndex(image => {
const imageIdToUse = Object.keys(measurementData)[0].substring(8);
return image.imageId === imageIdToUse;
});
if (imageIndex === -1) {
console.warn('Could not find image index for tracked measurement, using 0');
imageIndex = 0;
}
}
viewportGridService.setDisplaySetsForViewport({
viewportId: activeViewportId,
displaySetInstanceUIDs: [referencedDisplaySetUID],
viewportOptions: {
initialImageOptions: {
index: imageIndex,
},
},
});
},
jumpToSameImageInActiveViewport: (ctx, evt) => {
const { trackedStudy, trackedSeries, activeViewportId } = ctx;
const measurements = measurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
);
const trackedMeasurement = trackedMeasurements[0];
const referencedDisplaySetUID = trackedMeasurement.displaySetInstanceUID;
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId: activeViewportId,
displaySetInstanceUID: referencedDisplaySetUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId: activeViewportId,
displaySetInstanceUIDs: [referencedDisplaySetUID],
});
},
showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => {
if (evt.data.createdDisplaySetInstanceUIDs.length > 0) {
const StructuredReportDisplaySetInstanceUID = evt.data.createdDisplaySetInstanceUIDs[0];
viewportGridService.setDisplaySetsForViewport({
viewportId: evt.data.viewportId,
displaySetInstanceUIDs: [StructuredReportDisplaySetInstanceUID],
});
}
},
discardPreviouslyTrackedMeasurements: (ctx, evt) => {
const measurements = measurementService.getMeasurements();
const filteredMeasurements = measurements.filter(ms =>
ctx.prevTrackedSeries.includes(ms.referenceSeriesUID)
);
const measurementIds = filteredMeasurements.map(fm => fm.id);
for (let i = 0; i < measurementIds.length; i++) {
measurementService.remove(measurementIds[i]);
}
},
clearAllMeasurements: (ctx, evt) => {
const measurements = measurementService.getMeasurements();
const measurementIds = measurements.map(fm => fm.uid);
for (let i = 0; i < measurementIds.length; i++) {
measurementService.remove(measurementIds[i]);
}
},
});
machineOptions.services = Object.assign({}, machineOptions.services, {
promptBeginTracking: promptBeginTracking.bind(null, {
servicesManager,
extensionManager,
appConfig,
}),
promptTrackNewSeries: promptTrackNewSeries.bind(null, {
servicesManager,
extensionManager,
appConfig,
}),
promptTrackNewStudy: promptTrackNewStudy.bind(null, {
servicesManager,
extensionManager,
appConfig,
}),
promptSaveReport: promptSaveReport.bind(null, {
servicesManager,
commandsManager,
extensionManager,
appConfig,
}),
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
servicesManager,
extensionManager,
appConfig,
}),
hydrateStructuredReport: hydrateStructuredReport.bind(null, {
servicesManager,
extensionManager,
appConfig,
}),
promptLabelAnnotation: promptLabelAnnotation.bind(null, {
servicesManager,
extensionManager,
}),
});
machineOptions.guards = Object.assign({}, machineOptions.guards, {
isLabelOnMeasure: (ctx, evt, condMeta) => {
const labelConfig = customizationService.get('measurementLabels');
return labelConfig?.labelOnMeasure;
},
isLabelOnMeasureAndShouldKillMachine: (ctx, evt, condMeta) => {
const labelConfig = customizationService.get('measurementLabels');
return evt.data && evt.data.userResponse === RESPONSE.NO_NEVER && labelConfig?.labelOnMeasure;
},
});
// TODO: IMPROVE
// - Add measurement_updated to cornerstone; debounced? (ext side, or consumption?)
// - Friendlier transition/api in front of measurementTracking machine?
// - Blocked: viewport overlay shouldn't clip when resized
// TODO: PRIORITY
// - Fix "ellipses" series description dynamic truncate length
// - Fix viewport border resize
// - created/destroyed hooks for extensions (cornerstone measurement subscriptions in it's `init`)
const measurementTrackingMachine = useMemo(() => {
return Machine(machineConfiguration, machineOptions);
}, []); // Empty dependency array ensures this is only created once
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useMachine(
measurementTrackingMachine
);
useEffect(() => {
// Update the state machine with the active viewport ID
sendTrackedMeasurementsEvent('UPDATE_ACTIVE_VIEWPORT_ID', {
activeViewportId,
});
}, [activeViewportId, sendTrackedMeasurementsEvent]);
// ~~ Listen for changes to ViewportGrid for potential SRs hung in panes when idle
useEffect(() => {
const triggerPromptHydrateFlow = async () => {
if (viewports.size > 0) {
const activeViewport = viewports.get(activeViewportId);
if (!activeViewport || !activeViewport?.displaySetInstanceUIDs?.length) {
return;
}
// Todo: Getting the first displaySetInstanceUID is wrong, but we don't have
// tracking fusion viewports yet. This should change when we do.
const { displaySetService } = servicesManager.services;
const displaySet = displaySetService.getDisplaySetByUID(
activeViewport.displaySetInstanceUIDs[0]
);
if (!displaySet) {
return;
}
// If this is an SR produced by our SR SOPClassHandler,
// and it hasn't been loaded yet, do that now so we
// can check if it can be rehydrated or not.
//
// Note: This happens:
// - If the viewport is not currently an OHIFCornerstoneSRViewport
// - If the displaySet has never been hung
//
// Otherwise, the displaySet will be loaded by the useEffect handler
// listening to displaySet changes inside OHIFCornerstoneSRViewport.
// The issue here is that this handler in TrackedMeasurementsContext
// ends up occurring before the Viewport is created, so the displaySet
// is not loaded yet, and isRehydratable is undefined unless we call load().
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
!displaySet.isLoaded &&
displaySet.load
) {
await displaySet.load();
}
// Magic string
// load function added by our sopClassHandler module
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
displaySet.isRehydratable === true
) {
console.log('sending event...', trackedMeasurements);
sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', {
displaySetInstanceUID: displaySet.displaySetInstanceUID,
SeriesInstanceUID: displaySet.SeriesInstanceUID,
viewportId: activeViewportId,
});
}
}
};
triggerPromptHydrateFlow();
}, [
trackedMeasurements,
activeViewportId,
sendTrackedMeasurementsEvent,
servicesManager.services,
viewports,
]);
return (
<TrackedMeasurementsContext.Provider
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
>
{children}
</TrackedMeasurementsContext.Provider>
);
}
TrackedMeasurementsContextProvider.propTypes = {
children: PropTypes.oneOf([PropTypes.func, PropTypes.node]),
servicesManager: PropTypes.object.isRequired,
commandsManager: PropTypes.object.isRequired,
extensionManager: PropTypes.object.isRequired,
appConfig: PropTypes.object,
};
export { TrackedMeasurementsContext, TrackedMeasurementsContextProvider, useTrackedMeasurements };