293 lines
11 KiB
TypeScript
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 };
|