* feat(ViewportActionBar): OHIF issue #3123 (#3186) * feat(ViewportActionBar): OHIF issue #3123 - Renamed previous viewport action bar to be LegacyViewportActionBar - Components LegacyViewportActionBar depends on also renamed: LegacyCinePlayer and LegacyPatientInfo - New Viewport coded to specs in issue - added React hook useResizeObserver - added some tailwind classes * Updated tailwind to 3.2.7. Put external imports like React at the top of the import list. * feat(CinePlayer and ViewportActionBar) (#3198) * feat(CinePlayer and ViewportActionBar) - OHIF issue 3123 - new look cine control implemented - new custom blue color in tailwind config for various hover backgrounds in the cine control - new icons added for cine - Tooltip component now can be placed top (center) on hover - Tooltip component border colour now consistent with specs - fixed NPE in ViewportActionBar - upgraded tailwind to 3.2.7 in platform/ui - fixed issues in various button components brought by tailwind 3.2.7 where classes now need important flag - fixed issue with InputRange component so that the tracked value property can change externally - InputRange component can now optionally show its label - added new measurement tracking state service to hydrate SR without prompting - segmentation can also now be hydrated without prompting * PR feedback: - cine centralized to OHIFCornerstoneViewport - introduced a type for the CinePlayer properties * Addressed PR comments and concerns... The DOM ref for the root component of the ViewportActionBar is now added to state so that the various callbacks and ResizeObserver are updated with it. The CinePlayer FPS slider tooltip was moved up so that its arrow does not intersect the FPS text. The hover area for the CinePlayer slider tooltip is now the FPS < > buttons and text. The tracked measurements are now filtered to only include those of the active viewport series when the tracked measurement navigation arrows are used. * Addressed PR comments... The update to tailwind 3.2.7 caused several look-and-feel, UI regressions, so we are rolling back to 3.2.4.
215 lines
7.4 KiB
TypeScript
215 lines
7.4 KiB
TypeScript
import React, { useContext, useEffect } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { Machine } from 'xstate';
|
|
import { useMachine } from '@xstate/react';
|
|
import { useViewportGrid } from '@ohif/ui';
|
|
import {
|
|
machineConfiguration,
|
|
defaultOptions,
|
|
} from './measurementTrackingMachine';
|
|
import promptBeginTracking from './promptBeginTracking';
|
|
import promptTrackNewSeries from './promptTrackNewSeries';
|
|
import promptTrackNewStudy from './promptTrackNewStudy';
|
|
import promptSaveReport from './promptSaveReport';
|
|
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
|
|
import hydrateStructuredReport from './hydrateStructuredReport';
|
|
|
|
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 }, // Bound by consumer
|
|
{ children } // Component props
|
|
) {
|
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
|
const { activeViewportIndex, viewports } = viewportGrid;
|
|
|
|
const machineOptions = Object.assign({}, defaultOptions);
|
|
machineOptions.actions = Object.assign({}, machineOptions.actions, {
|
|
jumpToFirstMeasurementInActiveViewport: (ctx, evt) => {
|
|
const { measurementService } = servicesManager.services;
|
|
|
|
const { trackedStudy, trackedSeries } = ctx;
|
|
const measurements = measurementService.getMeasurements();
|
|
const trackedMeasurements = measurements.filter(
|
|
m =>
|
|
trackedStudy === m.referenceStudyUID &&
|
|
trackedSeries.includes(m.referenceSeriesUID)
|
|
);
|
|
|
|
const uid = trackedMeasurements[0].uid;
|
|
|
|
measurementService.jumpToMeasurement(
|
|
viewportGrid.activeViewportIndex,
|
|
uid
|
|
);
|
|
},
|
|
showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => {
|
|
if (evt.data.createdDisplaySetInstanceUIDs.length > 0) {
|
|
const StructuredReportDisplaySetInstanceUID =
|
|
evt.data.createdDisplaySetInstanceUIDs[0].displaySetInstanceUID;
|
|
|
|
viewportGridService.setDisplaySetsForViewport({
|
|
viewportIndex: evt.data.viewportIndex,
|
|
displaySetInstanceUIDs: [StructuredReportDisplaySetInstanceUID],
|
|
});
|
|
}
|
|
},
|
|
discardPreviouslyTrackedMeasurements: (ctx, evt) => {
|
|
const { measurementService } = servicesManager.services;
|
|
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 { measurementService } = servicesManager.services;
|
|
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,
|
|
}),
|
|
promptTrackNewSeries: promptTrackNewSeries.bind(null, {
|
|
servicesManager,
|
|
extensionManager,
|
|
}),
|
|
promptTrackNewStudy: promptTrackNewStudy.bind(null, {
|
|
servicesManager,
|
|
extensionManager,
|
|
}),
|
|
promptSaveReport: promptSaveReport.bind(null, {
|
|
servicesManager,
|
|
commandsManager,
|
|
extensionManager,
|
|
}),
|
|
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
|
|
servicesManager,
|
|
extensionManager,
|
|
}),
|
|
hydrateStructuredReport: hydrateStructuredReport.bind(null, {
|
|
servicesManager,
|
|
extensionManager,
|
|
}),
|
|
});
|
|
|
|
// 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 = Machine(
|
|
machineConfiguration,
|
|
machineOptions
|
|
);
|
|
|
|
const [
|
|
trackedMeasurements,
|
|
sendTrackedMeasurementsEvent,
|
|
trackedMeasurementsService,
|
|
] = useMachine(measurementTrackingMachine);
|
|
|
|
// ~~ Listen for changes to ViewportGrid for potential SRs hung in panes when idle
|
|
useEffect(() => {
|
|
if (viewports.length > 0) {
|
|
const activeViewport = viewports[activeViewportIndex];
|
|
|
|
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 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
|
|
) {
|
|
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,
|
|
viewportIndex: activeViewportIndex,
|
|
});
|
|
}
|
|
}
|
|
}, [
|
|
activeViewportIndex,
|
|
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,
|
|
};
|
|
|
|
export {
|
|
TrackedMeasurementsContext,
|
|
TrackedMeasurementsContextProvider,
|
|
useTrackedMeasurements,
|
|
};
|