ohif-viewer/extensions/default/src/Panels/PanelStudyBrowser.jsx

332 lines
10 KiB
React
Raw Normal View History

2020-05-22 02:51:48 +02:00
import React, { useState, useEffect } from 'react';
2020-05-18 21:05:49 +02:00
import PropTypes from 'prop-types';
2020-06-26 19:29:25 +02:00
import { StudyBrowser, useImageViewer, utils } from '@ohif/ui';
const { getFormattedDate } = utils;
2020-05-18 21:05:49 +02:00
2020-05-22 02:51:48 +02:00
/**
*
* @param {*} param0
*/
2020-05-19 15:56:25 +02:00
function PanelStudyBrowser({
2020-05-21 03:07:25 +02:00
DisplaySetService,
2020-05-19 15:56:25 +02:00
getImageSrc,
2020-05-21 03:07:25 +02:00
getStudiesForPatientByStudyInstanceUID,
requestDisplaySetCreationForStudy,
2020-05-19 15:56:25 +02:00
dataSource,
}) {
2020-05-22 02:51:48 +02:00
// 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
2020-05-21 03:07:25 +02:00
const [{ StudyInstanceUIDs }, dispatch] = useImageViewer();
const [activeTabName, setActiveTabName] = useState('primary');
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState(
[]
);
2020-05-21 03:07:25 +02:00
const [studyDisplayList, setStudyDisplayList] = useState([]);
const [displaySets, setDisplaySets] = useState([]);
2020-05-22 02:51:48 +02:00
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
2020-05-18 21:05:49 +02:00
// ~~ studyDisplayList
2020-05-19 15:56:25 +02:00
useEffect(() => {
2020-05-21 03:07:25 +02:00
// Fetch all studies for the patient in each primary study
async function fetchStudiesForPatient(StudyInstanceUID) {
const qidoStudiesForPatient =
(await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || [];
2020-05-19 10:59:50 +02:00
// TODO: This should be "naturalized DICOM JSON" studies
2020-05-21 03:07:25 +02:00
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
return {
studyInstanceUid: qidoStudy.StudyInstanceUID,
2020-06-26 19:29:25 +02:00
date: getFormattedDate(qidoStudy.StudyDate),
description: qidoStudy.StudyDescription,
modalities: qidoStudy.ModalitiesInStudy,
numInstances: qidoStudy.NumInstances,
// displaySets: []
};
});
setStudyDisplayList(actuallyMappedStudies);
2020-05-21 03:07:25 +02:00
}
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [StudyInstanceUIDs, getStudiesForPatientByStudyInstanceUID]);
2020-05-19 10:59:50 +02:00
2020-05-22 20:31:51 +02:00
// ~~ Initial Thumbnails
useEffect(() => {
const currentDisplaySets = DisplaySetService.activeDisplaySets;
2020-05-22 20:31:51 +02:00
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) {
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
imageId
);
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
}
2020-05-22 20:31:51 +02:00
});
// eslint-disable-next-line react-hooks/exhaustive-deps
2020-05-22 20:31:51 +02:00
}, []);
// ~~ displaySets
useEffect(() => {
2020-05-22 20:31:51 +02:00
// TODO: Are we sure `activeDisplaySets` will always be accurate?
const currentDisplaySets = DisplaySetService.activeDisplaySets;
const mappedDisplaySets = _mapDisplaySets(
currentDisplaySets,
thumbnailImageSrcMap
2020-05-19 19:22:38 +02:00
);
2020-05-19 15:56:25 +02:00
setDisplaySets(mappedDisplaySets);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [thumbnailImageSrcMap]);
2020-05-22 20:31:51 +02:00
// ~~ subscriptions --> displaySets
useEffect(() => {
2020-05-22 20:31:51 +02:00
// DISPLAY_SETS_ADDED returns an array of DisplaySets that were added
const SubscriptionDisplaySetsAdded = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
newDisplaySets => {
newDisplaySets.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) {
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
imageId
);
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
}
2020-05-22 20:31:51 +02:00
});
}
);
2020-05-22 20:31:51 +02:00
// 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);
}
);
2020-05-19 15:56:25 +02:00
return () => {
2020-05-22 20:31:51 +02:00
SubscriptionDisplaySetsAdded.unsubscribe();
SubscriptionDisplaySetsChanged.unsubscribe();
2020-05-19 15:56:25 +02:00
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const tabs = _createStudyBrowserTabs(
StudyInstanceUIDs,
studyDisplayList,
displaySets
);
2020-05-18 21:05:49 +02:00
2020-05-21 03:07:25 +02:00
// TODO: Should not fire this on "close"
function _handleStudyClick(StudyInstanceUID) {
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(
StudyInstanceUID
);
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
? // eslint-disable-next-line prettier/prettier
2020-06-26 19:29:25 +02:00
[
...expandedStudyInstanceUIDs.filter(
stdyUid => stdyUid !== StudyInstanceUID
),
]
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
if (!shouldCollapseStudy) {
requestDisplaySetCreationForStudy(DisplaySetService, StudyInstanceUID);
}
2020-05-18 21:05:49 +02:00
}
return (
<StudyBrowser
tabs={tabs}
2020-05-22 21:33:07 +02:00
activeTabName={activeTabName}
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
2020-05-21 03:07:25 +02:00
onClickStudy={_handleStudyClick}
2020-05-22 21:33:07 +02:00
onClickTab={clickedTabName => {
setActiveTabName(clickedTabName);
}}
2020-05-18 21:05:49 +02:00
/>
);
}
PanelStudyBrowser.propTypes = {
2020-05-21 03:07:25 +02:00
DisplaySetService: PropTypes.shape({
EVENTS: PropTypes.object.isRequired,
activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
2020-05-22 20:31:51 +02:00
getDisplaySetByUID: PropTypes.func.isRequired,
2020-05-21 03:07:25 +02:00
hasDisplaySetsForStudy: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
}).isRequired,
dataSource: PropTypes.shape({
getImageIdsForDisplaySet: PropTypes.func.isRequired,
}).isRequired,
getImageSrc: PropTypes.func.isRequired,
2020-05-21 03:07:25 +02:00
getStudiesForPatientByStudyInstanceUID: PropTypes.func.isRequired,
requestDisplaySetCreationForStudy: PropTypes.func.isRequired,
};
export default PanelStudyBrowser;
2020-05-19 15:56:25 +02:00
/**
* 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,
};
});
2020-05-18 21:05:49 +02:00
}
function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
const thumbnailDisplaySets = [];
const thumbnailNoImageDisplaySets = [];
displaySets.forEach(ds => {
2020-05-22 02:51:48 +02:00
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
const componentType = _getComponentType(ds.Modality);
const array =
componentType === 'thumbnail'
? thumbnailDisplaySets
: thumbnailNoImageDisplaySets;
array.push({
displaySetInstanceUID: ds.displaySetInstanceUID,
description: ds.SeriesDescription,
seriesNumber: ds.SeriesNumber,
modality: ds.Modality,
seriesDate: ds.SeriesDate,
numInstances: ds.numImageFrames,
StudyInstanceUID: ds.StudyInstanceUID,
componentType,
imageSrc,
dragData: {
type: 'displayset',
displaySetInstanceUID: ds.displaySetInstanceUID,
// .. Any other data to pass
},
});
});
return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
}
const thumbnailNoImageModalities = [
'SR',
'SEG',
'RTSTRUCT',
'RTPLAN',
'RTDOSE',
];
function _getComponentType(Modality) {
if (thumbnailNoImageModalities.includes(Modality)) {
// TODO probably others.
return 'thumbnailNoImage';
}
return 'thumbnail';
}
2020-05-22 20:31:51 +02:00
/**
*
* @param {string[]} primaryStudyInstanceUIDs
* @param {object[]} studyDisplayList
* @param {string} studyDisplayList.studyInstanceUid
* @param {string} studyDisplayList.date
* @param {string} studyDisplayList.description
* @param {string} studyDisplayList.modalities
* @param {number} studyDisplayList.numInstances
* @param {object[]} displaySets
* @returns tabs - The prop object expected by the StudyBrowser component
*/
function _createStudyBrowserTabs(
primaryStudyInstanceUIDs,
2020-05-21 03:07:25 +02:00
studyDisplayList,
displaySets
2020-05-21 03:07:25 +02:00
) {
const primaryStudies = [];
const recentStudies = [];
const allStudies = [];
studyDisplayList.forEach(study => {
const displaySetsForStudy = displaySets.filter(
ds => ds.StudyInstanceUID === study.studyInstanceUid
2020-05-19 15:56:25 +02:00
);
const tabStudy = Object.assign({}, study, {
displaySets: displaySetsForStudy,
});
if (primaryStudyInstanceUIDs.includes(study.studyInstanceUid)) {
primaryStudies.push(tabStudy);
} else {
// TODO: Filter allStudies to dates within one year of current date
recentStudies.push(tabStudy);
allStudies.push(tabStudy);
}
2020-05-19 10:59:50 +02:00
});
const tabs = [
{
name: 'primary',
label: 'Primary',
studies: primaryStudies,
},
{
name: 'recent',
label: 'Recent',
studies: recentStudies,
},
{
name: 'all',
label: 'All',
studies: allStudies,
},
];
return tabs;
}