ohif-viewer/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx

412 lines
13 KiB
React
Raw Normal View History

2020-05-26 15:03:58 +02:00
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
2020-06-26 21:17:25 +02:00
import { utils } from '@ohif/core';
2020-05-28 06:02:05 +02:00
import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui';
import { useTrackedMeasurements } from '../../getContextModule';
2020-05-26 15:03:58 +02:00
2020-06-26 21:17:25 +02:00
const { formatDate } = utils;
2020-06-26 19:29:25 +02:00
2020-05-26 15:03:58 +02:00
/**
*
* @param {*} param0
*/
function PanelStudyBrowserTracking({
MeasurementService,
2020-05-26 15:03:58 +02:00
DisplaySetService,
getImageSrc,
getStudiesForPatientByStudyInstanceUID,
requestDisplaySetCreationForStudy,
dataSource,
}) {
// 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 }, dispatchImageViewer] = useImageViewer();
const [
{ activeViewportIndex, viewports },
viewportGridService,
] = useViewportGrid();
const [
trackedMeasurements,
2020-05-28 03:25:44 +02:00
sendTrackedMeasurementsEvent,
] = useTrackedMeasurements();
2020-05-26 15:03:58 +02:00
const [activeTabName, setActiveTabName] = useState('primary');
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState(
[]
);
2020-05-26 15:03:58 +02:00
const [studyDisplayList, setStudyDisplayList] = useState([]);
const [displaySets, setDisplaySets] = useState([]);
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
const onClickThumbnailHandler = displaySetInstanceUID => {
viewportGridService.setDisplaysetForViewport({
viewportIndex: activeViewportIndex,
displaySetInstanceUID,
});
};
2020-06-09 19:39:17 +02:00
// TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy?
useEffect(() => {
const { unsubscribe } = MeasurementService.subscribe(
MeasurementService.EVENTS.MEASUREMENT_ADDED,
({ source, measurement }) => {
const {
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
} = measurement;
sendTrackedMeasurementsEvent('TRACK_SERIES', {
viewportIndex: activeViewportIndex,
StudyInstanceUID,
SeriesInstanceUID,
});
}
);
return unsubscribe;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [MeasurementService, activeViewportIndex, sendTrackedMeasurementsEvent]);
2020-05-28 03:25:44 +02:00
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
2020-05-26 15:03:58 +02:00
// ~~ studyDisplayList
useEffect(() => {
// Fetch all studies for the patient in each primary study
async function fetchStudiesForPatient(StudyInstanceUID) {
const qidoStudiesForPatient =
(await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || [];
// TODO: This should be "naturalized DICOM JSON" studies
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
return {
studyInstanceUid: qidoStudy.StudyInstanceUID,
2020-06-26 21:17:25 +02:00
date: formatDate(qidoStudy.StudyDate),
2020-05-26 15:03:58 +02:00
description: qidoStudy.StudyDescription,
modalities: qidoStudy.ModalitiesInStudy,
numInstances: qidoStudy.NumInstances,
// displaySets: []
};
});
setStudyDisplayList(actuallyMappedStudies);
}
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
// eslint-disable-next-line react-hooks/exhaustive-deps
2020-05-26 15:03:58 +02:00
}, [StudyInstanceUIDs, getStudiesForPatientByStudyInstanceUID]);
// ~~ 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) {
// 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-26 15:03:58 +02:00
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [DisplaySetService, dataSource, getImageSrc]);
2020-05-26 15:03:58 +02:00
// ~~ displaySets
useEffect(() => {
// TODO: Are we sure `activeDisplaySets` will always be accurate?
const currentDisplaySets = DisplaySetService.activeDisplaySets;
const mappedDisplaySets = _mapDisplaySets(
currentDisplaySets,
2020-05-28 03:25:44 +02:00
thumbnailImageSrcMap,
2020-05-28 06:02:05 +02:00
trackedSeries,
viewports
2020-05-26 15:03:58 +02:00
);
setDisplaySets(mappedDisplaySets);
// eslint-disable-next-line react-hooks/exhaustive-deps
2020-05-28 03:25:44 +02:00
}, [
DisplaySetService.activeDisplaySets,
trackedSeries,
thumbnailImageSrcMap,
2020-05-28 06:02:05 +02:00
viewports,
2020-05-28 03:25:44 +02:00
]);
2020-05-26 15:03:58 +02:00
// ~~ subscriptions --> displaySets
useEffect(() => {
// 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-26 15:03:58 +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,
2020-05-28 03:25:44 +02:00
thumbnailImageSrcMap,
2020-05-28 06:02:05 +02:00
trackedSeries,
viewports
2020-05-26 15:03:58 +02:00
);
setDisplaySets(mappedDisplaySets);
}
);
return () => {
SubscriptionDisplaySetsAdded.unsubscribe();
SubscriptionDisplaySetsChanged.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
2020-05-28 03:25:44 +02:00
}, [
DisplaySetService,
dataSource,
getImageSrc,
thumbnailImageSrcMap,
trackedSeries,
2020-05-28 06:02:05 +02:00
viewports,
2020-05-28 03:25:44 +02:00
]);
2020-05-26 15:03:58 +02:00
const tabs = _createStudyBrowserTabs(
StudyInstanceUIDs,
studyDisplayList,
displaySets
);
// TODO: Should not fire this on "close"
function _handleStudyClick(StudyInstanceUID) {
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(
StudyInstanceUID
);
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
? [
2020-06-26 19:29:25 +02:00
...expandedStudyInstanceUIDs.filter(
stdyUid => stdyUid !== StudyInstanceUID
),
]
2020-05-26 15:03:58 +02:00
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
if (!shouldCollapseStudy) {
requestDisplaySetCreationForStudy(DisplaySetService, StudyInstanceUID);
}
}
return (
<StudyBrowser
tabs={tabs}
activeTabName={activeTabName}
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
onClickStudy={_handleStudyClick}
onClickTab={clickedTabName => {
setActiveTabName(clickedTabName);
}}
onClickUntrack={displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
// TODO: shift this somewhere else where we're centralizing this logic?
// Potentially a helper from displaySetInstanceUID to this
sendTrackedMeasurementsEvent('UNTRACK_SERIES', {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
});
}}
onClickThumbnail={onClickThumbnailHandler}
2020-05-26 15:03:58 +02:00
/>
);
}
PanelStudyBrowserTracking.propTypes = {
2020-05-26 15:03:58 +02:00
DisplaySetService: PropTypes.shape({
EVENTS: PropTypes.object.isRequired,
activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
getDisplaySetByUID: PropTypes.func.isRequired,
hasDisplaySetsForStudy: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
}).isRequired,
dataSource: PropTypes.shape({
getImageIdsForDisplaySet: PropTypes.func.isRequired,
}).isRequired,
getImageSrc: PropTypes.func.isRequired,
getStudiesForPatientByStudyInstanceUID: PropTypes.func.isRequired,
requestDisplaySetCreationForStudy: PropTypes.func.isRequired,
};
export default PanelStudyBrowserTracking;
2020-05-26 15:03:58 +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-28 03:25:44 +02:00
function _mapDisplaySets(
displaySets,
thumbnailImageSrcMap,
2020-05-28 06:02:05 +02:00
trackedSeriesInstanceUIDs,
viewports // TODO: make array of `displaySetInstanceUIDs`?
2020-05-28 03:25:44 +02:00
) {
2020-06-16 06:43:51 +02:00
const thumbnailDisplaySets = [];
const thumbnailNoImageDisplaySets = [];
displaySets.forEach(ds => {
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
const componentType = _getComponentType(ds.Modality);
2020-05-28 06:02:05 +02:00
const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex(
vp => vp.displaySetInstanceUID === ds.displaySetInstanceUID
);
const viewportIdentificator =
_viewportLabels[firstViewportIndexWithMatchingDisplaySetUid] || '';
2020-06-16 06:43:51 +02:00
const array =
componentType === 'thumbnailTracked'
? thumbnailDisplaySets
: thumbnailNoImageDisplaySets;
array.push({
2020-05-26 15:03:58 +02:00
displaySetInstanceUID: ds.displaySetInstanceUID,
description: ds.SeriesDescription,
seriesNumber: ds.SeriesNumber,
modality: ds.Modality,
2020-06-16 06:43:51 +02:00
seriesDate: ds.SeriesDate,
2020-05-26 15:03:58 +02:00
numInstances: ds.numImageFrames,
StudyInstanceUID: ds.StudyInstanceUID,
2020-06-16 06:43:51 +02:00
componentType,
2020-05-26 15:03:58 +02:00
imageSrc,
dragData: {
type: 'displayset',
displaySetInstanceUID: ds.displaySetInstanceUID,
// .. Any other data to pass
},
2020-05-28 03:25:44 +02:00
isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID),
2020-05-28 06:02:05 +02:00
viewportIdentificator,
2020-06-16 06:43:51 +02:00
});
2020-05-26 15:03:58 +02:00
});
2020-06-16 06:43:51 +02:00
return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
}
const thumbnailNoImageModalities = [
'SR',
'SEG',
'RTSTRUCT',
'RTPLAN',
'RTDOSE',
];
function _getComponentType(Modality) {
if (thumbnailNoImageModalities.includes(Modality)) {
return 'thumbnailNoImage';
}
return 'thumbnailTracked';
2020-05-26 15:03:58 +02:00
}
2020-05-28 06:02:05 +02:00
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
2020-05-26 15:03:58 +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,
studyDisplayList,
displaySets
) {
const primaryStudies = [];
const recentStudies = [];
const allStudies = [];
studyDisplayList.forEach(study => {
const displaySetsForStudy = displaySets.filter(
ds => ds.StudyInstanceUID === study.studyInstanceUid
);
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);
}
});
const tabs = [
{
name: 'primary',
label: 'Primary',
studies: primaryStudies,
},
{
name: 'recent',
label: 'Recent',
studies: recentStudies,
},
{
name: 'all',
label: 'All',
studies: allStudies,
},
];
return tabs;
}