import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; import { utils } from '@ohif/core'; import { useImageViewer, useViewportGrid, Dialog, ButtonEnums } from '@ohif/ui'; import { StudyBrowser as NewStudyBrowser } from '@ohif/ui-next'; import { StudyBrowser as OldStudyBrowser } from '@ohif/ui'; import { useAppConfig } from '@state'; import { useTrackedMeasurements } from '../../getContextModule'; import { Separator } from '@ohif/ui-next'; import { PanelStudyBrowserTrackingHeader } from './PanelStudyBrowserTrackingHeader'; import { defaultActionIcons, defaultViewPresets } from './constants'; const { formatDate, createStudyBrowserTabs } = utils; /** * * @param {*} param0 */ function PanelStudyBrowserTracking({ servicesManager, getImageSrc, getStudiesForPatientByMRN, requestDisplaySetCreationForStudy, dataSource, renderHeader, getCloseIcon, tab, }: withAppTypes) { const { displaySetService, uiDialogService, hangingProtocolService, uiNotificationService, measurementService, studyPrefetcherService, customizationService, } = servicesManager.services; const navigate = useNavigate(); const { t } = useTranslation('Common'); const [appConfig] = useAppConfig(); // Normally you nest the components so the tree isn't so deep, and the data // doesn't have to have such an intense shape. This works well enough for now. // Tabs --> Studies --> DisplaySets --> Thumbnails const { StudyInstanceUIDs } = useImageViewer(); const [{ activeViewportId, viewports, isHangingProtocolLayout }, viewportGridService] = useViewportGrid(); const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); const [activeTabName, setActiveTabName] = useState('primary'); const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ ...StudyInstanceUIDs, ]); const [studyDisplayList, setStudyDisplayList] = useState([]); const [hasLoadedViewports, setHasLoadedViewports] = useState(false); const [displaySets, setDisplaySets] = useState([]); const [displaySetsLoadingState, setDisplaySetsLoadingState] = useState({}); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null); const [viewPresets, setViewPresets] = useState( customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets ); const [actionIcons, setActionIcons] = useState(defaultActionIcons); const updateActionIconValue = actionIcon => { actionIcon.value = !actionIcon.value; const newActionIcons = [...actionIcons]; setActionIcons(newActionIcons); }; const updateViewPresetValue = viewPreset => { if (!viewPreset) { return; } const newViewPresets = viewPresets.map(preset => { preset.selected = preset.id === viewPreset.id; return preset; }); setViewPresets(newViewPresets); }; const onDoubleClickThumbnailHandler = displaySetInstanceUID => { let updatedViewports = []; const viewportId = activeViewportId; try { updatedViewports = hangingProtocolService.getViewportsRequireUpdate( viewportId, displaySetInstanceUID, isHangingProtocolLayout ); } catch (error) { console.warn(error); uiNotificationService.show({ title: 'Thumbnail Double Click', message: 'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.', type: 'info', duration: 3000, }); } viewportGridService.setDisplaySetsForViewports(updatedViewports); }; const activeViewportDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs; const { trackedSeries } = trackedMeasurements.context; // ~~ studyDisplayList useEffect(() => { // Fetch all studies for the patient in each primary study async function fetchStudiesForPatient(StudyInstanceUID) { // current study qido const qidoForStudyUID = await dataSource.query.studies.search({ studyInstanceUid: StudyInstanceUID, }); if (!qidoForStudyUID?.length) { navigate('/notfoundstudy', '_self'); throw new Error('Invalid study URL'); } let qidoStudiesForPatient = qidoForStudyUID; // try to fetch the prior studies based on the patientID if the // server can respond. try { qidoStudiesForPatient = await getStudiesForPatientByMRN(qidoForStudyUID); } catch (error) { console.warn(error); } const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient); const actuallyMappedStudies = mappedStudies.map(qidoStudy => { return { studyInstanceUid: qidoStudy.StudyInstanceUID, date: formatDate(qidoStudy.StudyDate) || t('NoStudyDate'), description: qidoStudy.StudyDescription, modalities: qidoStudy.ModalitiesInStudy, numInstances: qidoStudy.NumInstances, }; }); setStudyDisplayList(prevArray => { const ret = [...prevArray]; for (const study of actuallyMappedStudies) { if (!prevArray.find(it => it.studyInstanceUid === study.studyInstanceUid)) { ret.push(study); } } return ret; }); } StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [StudyInstanceUIDs, getStudiesForPatientByMRN]); // ~~ Initial Thumbnails useEffect(() => { if (!hasLoadedViewports) { if (activeViewportId) { // Once there is an active viewport id, it means the layout is ready // so wait a bit of time to allow the viewports preferential loading // which improves user experience of responsiveness significantly on slower // systems. window.setTimeout(() => setHasLoadedViewports(true), 250); } return; } const currentDisplaySets = displaySetService.activeDisplaySets; if (!currentDisplaySets.length) { return; } currentDisplaySets.forEach(async dSet => { const newImageSrcEntry = {}; const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); const imageId = getImageIdForThumbnail(displaySet, imageIds); // TODO: Is it okay that imageIds are not returned here for SR displaySets? if (!imageId || displaySet?.unsupported) { return; } // When the image arrives, render it and store the result in the thumbnailImgSrcMap newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId); setThumbnailImageSrcMap(prevState => { return { ...prevState, ...newImageSrcEntry }; }); }); }, [displaySetService, dataSource, getImageSrc, activeViewportId, hasLoadedViewports]); // ~~ displaySets useEffect(() => { const currentDisplaySets = displaySetService.activeDisplaySets; if (!currentDisplaySets.length) { return; } const mappedDisplaySets = _mapDisplaySets( currentDisplaySets, displaySetsLoadingState, thumbnailImageSrcMap, trackedSeries, viewports, viewportGridService, dataSource, displaySetService, uiDialogService, uiNotificationService ); setDisplaySets(mappedDisplaySets); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ displaySetService.activeDisplaySets, displaySetsLoadingState, trackedSeries, viewports, dataSource, thumbnailImageSrcMap, ]); // -- displaySetsLoadingState useEffect(() => { const { unsubscribe } = studyPrefetcherService.subscribe( studyPrefetcherService.EVENTS.DISPLAYSET_LOAD_PROGRESS, updatedDisplaySetLoadingState => { const { displaySetInstanceUID, loadingProgress } = updatedDisplaySetLoadingState; setDisplaySetsLoadingState(prevState => ({ ...prevState, [displaySetInstanceUID]: loadingProgress, })); } ); return () => unsubscribe(); }, [studyPrefetcherService]); // ~~ subscriptions --> displaySets useEffect(() => { // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added const SubscriptionDisplaySetsAdded = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { if (!hasLoadedViewports) { return; } const { displaySetsAdded, options } = data; displaySetsAdded.forEach(async dSet => { const displaySetInstanceUID = dSet.displaySetInstanceUID; const newImageSrcEntry = {}; const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); if (displaySet?.unsupported) { return; } if (options.madeInClient) { setJumpToDisplaySet(displaySetInstanceUID); } const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); const imageId = getImageIdForThumbnail(displaySet, imageIds); // TODO: Is it okay that imageIds are not returned here for SR displaysets? if (!imageId) { return; } // When the image arrives, render it and store the result in the thumbnailImgSrcMap newImageSrcEntry[displaySetInstanceUID] = await getImageSrc(imageId); setThumbnailImageSrcMap(prevState => { return { ...prevState, ...newImageSrcEntry }; }); }); } ); return () => { SubscriptionDisplaySetsAdded.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [displaySetService, dataSource, getImageSrc, thumbnailImageSrcMap, trackedSeries, viewports]); useEffect(() => { // TODO: Will this always hold _all_ the displaySets we care about? // DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets` const SubscriptionDisplaySetsChanged = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_CHANGED, changedDisplaySets => { const mappedDisplaySets = _mapDisplaySets( changedDisplaySets, displaySetsLoadingState, thumbnailImageSrcMap, trackedSeries, viewports, viewportGridService, dataSource, displaySetService, uiDialogService, uiNotificationService ); setDisplaySets(mappedDisplaySets); } ); const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, () => { const mappedDisplaySets = _mapDisplaySets( displaySetService.getActiveDisplaySets(), displaySetsLoadingState, thumbnailImageSrcMap, trackedSeries, viewports, viewportGridService, dataSource, displaySetService, uiDialogService, uiNotificationService ); setDisplaySets(mappedDisplaySets); } ); return () => { SubscriptionDisplaySetsChanged.unsubscribe(); SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; }, [ displaySetsLoadingState, thumbnailImageSrcMap, trackedSeries, viewports, dataSource, displaySetService, ]); const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); // TODO: Should not fire this on "close" function _handleStudyClick(StudyInstanceUID) { const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); if (!shouldCollapseStudy) { const madeInClient = true; requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient); } } useEffect(() => { if (jumpToDisplaySet) { // Get element by displaySetInstanceUID const displaySetInstanceUID = jumpToDisplaySet; const element = document.getElementById(`thumbnail-${displaySetInstanceUID}`); if (element && typeof element.scrollIntoView === 'function') { // TODO: Any way to support IE here? element.scrollIntoView({ behavior: 'smooth' }); setJumpToDisplaySet(null); } } }, [jumpToDisplaySet, expandedStudyInstanceUIDs, activeTabName]); useEffect(() => { if (!jumpToDisplaySet) { return; } const displaySetInstanceUID = jumpToDisplaySet; // Set the activeTabName and expand the study const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs); if (!thumbnailLocation) { console.warn('jumpToThumbnail: displaySet thumbnail not found.'); return; } const { tabName, StudyInstanceUID } = thumbnailLocation; setActiveTabName(tabName); const studyExpanded = expandedStudyInstanceUIDs.includes(StudyInstanceUID); if (!studyExpanded) { const updatedExpandedStudyInstanceUIDs = [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); } }, [expandedStudyInstanceUIDs, jumpToDisplaySet, tabs]); const onClickUntrack = displaySetInstanceUID => { const onConfirm = () => { const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); sendTrackedMeasurementsEvent('UNTRACK_SERIES', { SeriesInstanceUID: displaySet.SeriesInstanceUID, }); const measurements = measurementService.getMeasurements(); measurements.forEach(m => { if (m.referenceSeriesUID === displaySet.SeriesInstanceUID) { measurementService.remove(m.uid); } }); }; uiDialogService.create({ id: 'untrack-series', centralize: true, isDraggable: false, showOverlay: true, content: Dialog, contentProps: { title: 'Untrack Series', body: () => (
Are you sure you want to untrack this series?
This action cannot be undone and will delete all your existing measurements.
Are you sure you want to delete this report?
This action cannot be undone.