import React, { useEffect, useState, useCallback, useReducer } from 'react'; import PropTypes from 'prop-types'; import { SegmentationTable, Button, Icon } from '@ohif/ui'; import classnames from 'classnames'; import { useTranslation } from 'react-i18next'; import segmentationEditHandler from './segmentationEditHandler'; import ExportReports from './ExportReports'; import ROIThresholdConfiguration, { ROI_STAT, } from './ROIThresholdConfiguration'; const LOWER_THRESHOLD_DEFAULT = 0; const UPPER_THRESHOLD_DEFAULT = 100; const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature const DEFAULT_STRATEGY = ROI_STAT; function reducer(state, action) { const { payload } = action; const { strategy, lower, upper, weight } = payload; switch (action.type) { case 'setStrategy': return { ...state, strategy, }; case 'setThreshold': return { ...state, lower: lower ? lower : state.lower, upper: upper ? upper : state.upper, }; case 'setWeight': return { ...state, weight, }; default: return state; } } export default function PanelRoiThresholdSegmentation({ servicesManager, commandsManager, }) { const { SegmentationService } = servicesManager.services; const { t } = useTranslation('PanelSUV'); const [showConfig, setShowConfig] = useState(false); const [labelmapLoading, setLabelmapLoading] = useState(false); const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); const [segmentations, setSegmentations] = useState(() => SegmentationService.getSegmentations() ); const [config, dispatch] = useReducer(reducer, { strategy: DEFAULT_STRATEGY, lower: LOWER_THRESHOLD_DEFAULT, upper: UPPER_THRESHOLD_DEFAULT, weight: WEIGHT_DEFAULT, }); const [tmtvValue, setTmtvValue] = useState(null); const runCommand = useCallback( (commandName, commandOptions = {}) => { return commandsManager.runCommand(commandName, commandOptions); }, [commandsManager] ); const handleTMTVCalculation = useCallback(() => { const tmtv = runCommand('calculateTMTV', { segmentations }); if (tmtv !== undefined) { setTmtvValue(tmtv.toFixed(2)); } }, [segmentations, runCommand]); const handleROIThresholding = useCallback(() => { const labelmap = runCommand('thresholdSegmentationByRectangleROITool', { segmentationId: selectedSegmentationId, config, }); const lesionStats = runCommand('getLesionStats', { labelmap }); const suvPeak = runCommand('calculateSuvPeak', { labelmap }); const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue; // update segDetails with the suv peak for the active segmentation const segmentation = SegmentationService.getSegmentation( selectedSegmentationId ); const cachedStats = { lesionStats, suvPeak, lesionGlyoclysisStats, }; const notYetUpdatedAtSource = true; SegmentationService.addOrUpdateSegmentation( { ...segmentation, ...Object.assign(segmentation.cachedStats, cachedStats), displayText: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`], }, notYetUpdatedAtSource ); handleTMTVCalculation(); }, [selectedSegmentationId, config]); /** * Update UI based on segmentation changes (added, removed, updated) */ useEffect(() => { // ~~ Subscription const added = SegmentationService.EVENTS.SEGMENTATION_ADDED; const updated = SegmentationService.EVENTS.SEGMENTATION_UPDATED; const subscriptions = []; [added, updated].forEach(evt => { const { unsubscribe } = SegmentationService.subscribe(evt, () => { const segmentations = SegmentationService.getSegmentations(); setSegmentations(segmentations); }); subscriptions.push(unsubscribe); }); return () => { subscriptions.forEach(unsub => { unsub(); }); }; }, []); useEffect(() => { const { unsubscribe } = SegmentationService.subscribe( SegmentationService.EVENTS.SEGMENTATION_REMOVED, () => { const segmentations = SegmentationService.getSegmentations(); setSegmentations(segmentations); if (segmentations.length > 0) { setSelectedSegmentationId(segmentations[0].id); handleTMTVCalculation(); } else { setSelectedSegmentationId(null); setTmtvValue(null); } } ); return () => { unsubscribe(); }; }, []); /** * Whenever the segmentations change, update the TMTV calculations */ useEffect(() => { if (!selectedSegmentationId && segmentations.length > 0) { setSelectedSegmentationId(segmentations[0].id); } handleTMTVCalculation(); }, [segmentations, selectedSegmentationId]); return (
{ setShowConfig(!showConfig); }} >
{t('ROI Threshold Configuration')}
{showConfig && ( )} {/* show segmentation table */}
{segmentations?.length ? ( { runCommand('setSegmentationActiveForToolGroups', { segmentationId: id, }); setSelectedSegmentationId(id); }} onToggleVisibility={id => { SegmentationService.toggleSegmentationVisibility(id); }} onToggleVisibilityAll={ids => { ids.map(id => { SegmentationService.toggleSegmentationVisibility(id); }); }} onDelete={id => { SegmentationService.remove(id); }} onEdit={id => { segmentationEditHandler({ id, servicesManager, }); }} /> ) : null}
{tmtvValue !== null ? (
{'TMTV:'}
{`${tmtvValue} mL`}
) : null}
{ // navigate to a url in a new tab window.open( 'https://github.com/OHIF/Viewers/blob/v3-stable/modes/tmtv/README.md', '_blank' ); }} > {'User Guide'}
); } PanelRoiThresholdSegmentation.propTypes = { commandsManager: PropTypes.shape({ runCommand: PropTypes.func.isRequired, }), servicesManager: PropTypes.shape({ services: PropTypes.shape({ SegmentationService: PropTypes.shape({ getSegmentation: PropTypes.func.isRequired, getSegmentations: PropTypes.func.isRequired, toggleSegmentationVisibility: PropTypes.func.isRequired, subscribe: PropTypes.func.isRequired, EVENTS: PropTypes.object.isRequired, }).isRequired, }).isRequired, }).isRequired, };