import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui'; import { utils } from '@ohif/core'; import { useNavigate } from 'react-router-dom'; const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils; /** * * @param {*} param0 */ function PanelStudyBrowser({ servicesManager, getImageSrc, getStudiesForPatientByMRN, requestDisplaySetCreationForStudy, dataSource, }: withAppTypes) { const { hangingProtocolService, displaySetService, uiNotificationService } = servicesManager.services; const navigate = useNavigate(); // 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 [activeTabName, setActiveTabName] = useState('primary'); const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ ...StudyInstanceUIDs, ]); const [studyDisplayList, setStudyDisplayList] = useState([]); const [displaySets, setDisplaySets] = useState([]); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); 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.', type: 'info', duration: 3000, }); } viewportGridService.setDisplaySetsForViewports(updatedViewports); }; // ~~ 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), 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)); }, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); // // ~~ Initial Thumbnails useEffect(() => { const currentDisplaySets = displaySetService.activeDisplaySets; currentDisplaySets.forEach(async dSet => { const newImageSrcEntry = {}; const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); const imageId = imageIds[Math.floor(imageIds.length / 2)]; // 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 }; }); }); }, [StudyInstanceUIDs, dataSource, displaySetService, getImageSrc]); // ~~ displaySets useEffect(() => { // TODO: Are we sure `activeDisplaySets` will always be accurate? const currentDisplaySets = displaySetService.activeDisplaySets; const mappedDisplaySets = _mapDisplaySets(currentDisplaySets, thumbnailImageSrcMap); sortStudyInstances(mappedDisplaySets); setDisplaySets(mappedDisplaySets); }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); // ~~ subscriptions --> displaySets useEffect(() => { // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added const SubscriptionDisplaySetsAdded = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { const { displaySetsAdded, options } = data; displaySetsAdded.forEach(async dSet => { const newImageSrcEntry = {}; const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); if (displaySet?.unsupported) { return; } const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); const imageId = imageIds[Math.floor(imageIds.length / 2)]; // 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[dSet.displaySetInstanceUID] = await getImageSrc( imageId, dSet.initialViewport ); setThumbnailImageSrcMap(prevState => { return { ...prevState, ...newImageSrcEntry }; }); }); } ); return () => { SubscriptionDisplaySetsAdded.unsubscribe(); }; }, [getImageSrc, dataSource, displaySetService]); 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, thumbnailImageSrcMap); setDisplaySets(mappedDisplaySets); } ); const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, () => { const mappedDisplaySets = _mapDisplaySets( displaySetService.getActiveDisplaySets(), thumbnailImageSrcMap ); setDisplaySets(mappedDisplaySets); } ); return () => { SubscriptionDisplaySetsChanged.unsubscribe(); SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; }, [StudyInstanceUIDs, thumbnailImageSrcMap, 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 ? // eslint-disable-next-line prettier/prettier [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); if (!shouldCollapseStudy) { const madeInClient = true; requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient); } } const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs; return ( { setActiveTabName(clickedTabName); }} /> ); } PanelStudyBrowser.propTypes = { servicesManager: PropTypes.object.isRequired, dataSource: PropTypes.shape({ getImageIdsForDisplaySet: PropTypes.func.isRequired, }).isRequired, getImageSrc: PropTypes.func.isRequired, getStudiesForPatientByMRN: PropTypes.func.isRequired, requestDisplaySetCreationForStudy: PropTypes.func.isRequired, }; export default PanelStudyBrowser; /** * Maps from the DataSource's format to a naturalized object * * @param {*} studies */ function _mapDataSourceStudies(studies) { return studies.map(study => { // TODO: Why does the data source return in this format? return { AccessionNumber: study.accession, StudyDate: study.date, StudyDescription: study.description, NumInstances: study.instances, ModalitiesInStudy: study.modalities, PatientID: study.mrn, PatientName: study.patientName, StudyInstanceUID: study.studyInstanceUid, StudyTime: study.time, }; }); } function _mapDisplaySets(displaySets, thumbnailImageSrcMap) { const thumbnailDisplaySets = []; const thumbnailNoImageDisplaySets = []; displaySets .filter(ds => !ds.excludeFromThumbnailBrowser) .forEach(ds => { const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID]; const componentType = _getComponentType(ds); const array = componentType === 'thumbnail' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets; array.push({ displaySetInstanceUID: ds.displaySetInstanceUID, description: ds.SeriesDescription || '', seriesNumber: ds.SeriesNumber, modality: ds.Modality, seriesDate: ds.SeriesDate, seriesTime: ds.SeriesTime, numInstances: ds.numImageFrames, countIcon: ds.countIcon, StudyInstanceUID: ds.StudyInstanceUID, messages: ds.messages, componentType, imageSrc, dragData: { type: 'displayset', displaySetInstanceUID: ds.displaySetInstanceUID, // .. Any other data to pass }, isHydratedForDerivedDisplaySet: ds.isHydrated, }); }); return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets]; } const thumbnailNoImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; function _getComponentType(ds) { if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) { // TODO probably others. return 'thumbnailNoImage'; } return 'thumbnail'; }