Improve study browser

This commit is contained in:
Erik Ziegler 2020-05-15 10:05:40 +02:00 committed by James A. Petts
parent 6044190ab1
commit ec2df02e83
8 changed files with 195 additions and 105 deletions

View File

@ -80,13 +80,13 @@ function createDicomWebApi(dicomWebConfig) {
// processResults: processResults.bind(), // processResults: processResults.bind(),
}, },
instances: { instances: {
search: (studyInstanceUid, queryParamaters) => search: (studyInstanceUid, queryParameters) =>
qidoSearch.call( qidoSearch.call(
undefined, undefined,
qidoDicomWebClient, qidoDicomWebClient,
studyInstanceUid, studyInstanceUid,
null, null,
queryParamaters queryParameters
), ),
}, },
}, },

View File

@ -105,12 +105,12 @@ function search(
dicomWebClient, dicomWebClient,
studyInstanceUid, studyInstanceUid,
seriesInstanceUid, seriesInstanceUid,
queryParamaters queryParameters
) { ) {
// Studies // Studies
return dicomWebClient.searchForStudies({ return dicomWebClient.searchForStudies({
studyInstanceUid: undefined, studyInstanceUid: undefined,
queryParams: queryParamaters, queryParams: queryParameters,
}); });
} }

View File

@ -31,7 +31,7 @@ function ViewerLayout({
const getPanelData = id => { const getPanelData = id => {
const entry = extensionManager.getModuleEntry(id); const entry = extensionManager.getModuleEntry(id);
// TODO, not sure why sidepanel content has to be JSX, and not a children prop? // TODO, not sure why sidepanel content has to be JSX, and not a children prop?
const content = entry.component({}); const content = entry.component({ extensionManager });
return { return {
iconName: entry.iconName, iconName: entry.iconName,

View File

@ -1,4 +1,3 @@
import ViewerLayout from './ViewerLayout'; import ViewerLayout from './ViewerLayout';
/* /*
- Define layout for the viewer in mode configuration. - Define layout for the viewer in mode configuration.

View File

@ -1,55 +1,155 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { StudyBrowser } from '@ohif/ui'; import { StudyBrowser } from '@ohif/ui';
import { useViewModel, displaySetManager } from '@ohif/core'; import {
dicomMetadataStore,
useViewModel,
displaySetManager,
} from '@ohif/core';
function StudyBrowserPanel({ extensionManager }) {
// TODO: need to check how this is intended to be passed in
const dataSource = extensionManager.dataSourceMap.dicomweb[0];
const viewModel = useViewModel();
// TODO
const viewportData = []; //useViewportGrid();
const seriesTracking = {}; //useSeriesTracking();
const [studyData, setStudyData] = useState([]);
console.log(viewModel);
const displaySets = viewModel.displaySetInstanceUids.map(
displaySetManager.getDisplaySetByUID
);
function displaySetsToStudyPanelInfo(
displaySets,
viewportData = [],
seriesTracking = {}
) {
console.log(displaySets); console.log(displaySets);
const studies = {}; if (!displaySets.length) {
displaySets.forEach(displaySet => { return;
}
// TODO:
// - Put this in something so it only runs once
// - Have update the query update the dicom data store at the study level and then have this component use the data in the view model
useEffect(() => {
const dSets = viewModel.displaySetInstanceUids.map(
displaySetManager.getDisplaySetByUID
);
const aDisplaySet = dSets[0];
const firstStudy = dicomMetadataStore.getStudy(
aDisplaySet.StudyInstanceUID
);
const firstInstance = firstStudy.series[0].instances[0];
const PatientID = firstInstance.PatientID;
dataSource.query.studies.search({ patientId: PatientID }).then(results => {
const studies = results.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,
};
});
setStudyData(studies);
});
}, [viewModel.displaySetInstanceUids]);
const studiesFromInstanceData = {};
displaySets.forEach(ds => {
const displaySet = {
displaySetInstanceUid: ds.displaySetInstanceUid,
description: ds.SeriesDescription,
seriesNumber: ds.SeriesNumber,
modality: ds.Modality,
date: ds.SeriesDate,
numInstances: ds.numImageFrames,
};
const displaySetViewportData = viewportData.find( const displaySetViewportData = viewportData.find(
a => a.displaySetInstanceUid === displaySet.displaySetInstanceUid a => a.displaySetInstanceUid === ds.displaySetInstanceUid
); );
if (displaySetViewportData) { if (displaySetViewportData) {
displaySet.viewportIdentificator = displaySetViewportData.identifier; displaySet.viewportIdentificator = displaySetViewportData.identifier;
} }
const trackingInfo = seriesTracking[displaySet.SeriesInstanceUID]; const trackingInfo = seriesTracking[ds.SeriesInstanceUID];
if (trackingInfo) { if (trackingInfo) {
displaySet.viewportIdentificator = trackingInfo.isTracked; displaySet.isTracked = trackingInfo.isTracked;
displaySet.componentType = trackingInfo.isTracked displaySet.componentType = trackingInfo.isTracked
? 'thumbnailTracked' ? 'thumbnailTracked'
: 'thumbnail'; : 'thumbnail';
} else {
displaySet.isTracked = false;
displaySet.componentType = 'thumbnail';
} }
if (!Object.keys(studies).includes(displaySet.StudyInstanceUID)) { if (!Object.keys(studiesFromInstanceData).includes(ds.StudyInstanceUID)) {
studies[displaySet.StudyInstanceUID] = { const study = dicomMetadataStore.getStudy(ds.StudyInstanceUID);
date: displaySet.SeriesDate, const anInstance = study.series[0].instances[0];
description: displaySet.SeriesDescription,
seriesNumber: displaySet.SeriesNumber, studiesFromInstanceData[ds.StudyInstanceUID] = {
modalities: displaySet.modalities, date: anInstance.StudyDate, // TODO: Format this date to DD-MMM-YYYY
description: anInstance.StudyDescription,
displaySets: [], displaySets: [],
numInstances: 0, numInstances: 0,
modalitiesSet: new Set(),
}; };
} }
studies[displaySet.StudyInstanceUID].displaySets.push(displaySet); studiesFromInstanceData[ds.StudyInstanceUID].displaySets.push(displaySet);
studies[displaySet.StudyInstanceUID].numInstances += studiesFromInstanceData[ds.StudyInstanceUID].numInstances +=
displaySet.numImageFrames; displaySet.numInstances;
studiesFromInstanceData[ds.StudyInstanceUID].modalitiesSet.add(
displaySet.modality
);
const modalitiesSet =
studiesFromInstanceData[ds.StudyInstanceUID].modalitiesSet;
studiesFromInstanceData[ds.StudyInstanceUID].modalities = Array.from(
modalitiesSet
).join(', ');
}); });
const allStudies = Object.keys(studies).map(StudyInstanceUID => { // QIDO for all by MRN
const study = studies[StudyInstanceUID]; const allStudies = studyData.map(studyLevelData => {
const studyFromInstanceData =
studiesFromInstanceData[studyLevelData.StudyInstanceUID];
if (!studyFromInstanceData) {
return { return {
studyInstanceUid: StudyInstanceUID, studyInstanceUid: studyLevelData.StudyInstanceUID,
...study, date: studyLevelData.StudyDate,
description: studyLevelData.StudyDescription,
modalities: studyLevelData.ModalitiesInStudy,
numInstances: studyLevelData.NumInstances,
displaySets: [],
};
}
return {
studyInstanceUid: studyLevelData.StudyInstanceUID,
date: studyLevelData.StudyDate || studyFromInstanceData.date,
description:
studyLevelData.StudyDescription || studyFromInstanceData.description,
modalities:
studyFromInstanceData.modalities || studyLevelData.ModalitiesInStudy,
numInstances:
studyLevelData.NumInstances || studyFromInstanceData.numInstances,
displaySets: studyFromInstanceData.displaySets,
}; };
}); });
@ -57,36 +157,16 @@ function displaySetsToStudyPanelInfo(
return true; // TODO: check study.StudyInstanceUID matches queryparam? return true; // TODO: check study.StudyInstanceUID matches queryparam?
}); });
console.log(primary); // TODO: Filter allStudies to dates within one year of current date
const recentStudies = allStudies.filter(study => { const recentStudies = allStudies.filter(study => {
return true; // TODO: check study.date return true; // TODO: check study.date
}); });
/*const primary = {
studyInstanceUid: '1',
date: '07-Sept-2010',
description: 'CHEST/ABD/PELVIS W/CONTRAST',
numInstances: 902,
modalities: 'CT,SR',
displaySets: [
{
displaySetInstanceUid: 'f69f6asdasd48c-223e-db7f-c4af-b8906641a66e',
description: 'Multiple line image series description lorem sit',
seriesNumber: 1,
numInstances: 68,
componentType: 'thumbnailTracked',
viewportIdentificator: 'A',
isTracked: true,
},
],
};*/
const tabs = [ const tabs = [
{ {
name: 'primary', name: 'primary',
label: 'Primary', label: 'Primary',
studies: [primary], studies: primary ? [primary] : [],
}, },
{ {
name: 'recent', name: 'recent',
@ -100,51 +180,27 @@ function displaySetsToStudyPanelInfo(
}, },
]; ];
return tabs; function onClickStudy(StudyInstanceUID) {
} if (studiesFromInstanceData[StudyInstanceUID]) {
return;
function StudyBrowserPanel({}) {
const viewModel = useViewModel();
// TODO
const viewportData = []; //useViewportGrid();
const seriesTracking = {}; //useSeriesTracking();
console.log(viewModel);
const displaySets = viewModel.displaySetInstanceUids.map(
displaySetManager.getDisplaySetByUID
);
let tabs;
if (displaySets.length) {
tabs = displaySetsToStudyPanelInfo(
displaySets,
viewportData,
seriesTracking
);
} else {
tabs = [
{
name: 'primary',
label: 'Primary',
studies: [],
},
{
name: 'recent',
label: 'Recent',
studies: [],
},
{
name: 'all',
label: 'All',
studies: [],
},
];
} }
return <StudyBrowser tabs={tabs} />; console.warn(`onClickStudy: ${StudyInstanceUID}`);
// TODO: This is weird, why can't the data source just be used as
// as function that doesn't expect a query string?
const queryParams = `?StudyInstanceUIDs=${StudyInstanceUID}`;
dataSource.retrieve.series.metadata(
queryParams,
displaySetManager.makeDisplaySets
);
}
const memoOnClickStudy = useCallback(StudyInstanceUID => {
onClickStudy(StudyInstanceUID);
});
return <StudyBrowser tabs={tabs} onClickStudy={memoOnClickStudy} />;
} }
function getPanelModule() { function getPanelModule() {

View File

@ -13,13 +13,6 @@ const makeDisplaySet = instances => {
const instance = instances[0]; const instance = instances[0];
const imageSet = new ImageSet(instances); const imageSet = new ImageSet(instances);
const modalities = new Set();
instances.forEach(i => {
modalities.add(i.Modality);
});
const modalitiesInDisplaySet = Array.from(modalities).join(', ');
// set appropriate attributes to image set... // set appropriate attributes to image set...
imageSet.setAttributes({ imageSet.setAttributes({
displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID
@ -30,7 +23,7 @@ const makeDisplaySet = instances => {
SeriesNumber: instance.SeriesNumber, SeriesNumber: instance.SeriesNumber,
FrameRate: instance.FrameTime, FrameRate: instance.FrameTime,
SeriesDescription: instance.SeriesDescription, SeriesDescription: instance.SeriesDescription,
modalities: modalitiesInDisplaySet, Modality: instance.Modality,
isMultiFrame: isMultiFrame(instance), isMultiFrame: isMultiFrame(instance),
numImageFrames: instances.length, numImageFrames: instances.length,
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,

View File

@ -47,7 +47,9 @@ const _model = {
}; };
function _getStudy(StudyInstanceUID) { function _getStudy(StudyInstanceUID) {
return _model.studies.find(StudyInstanceUID); return _model.studies.find(
aStudy => aStudy.StudyInstanceUID === StudyInstanceUID
);
} }
function _getSeries(StudyInstanceUID, SeriesInstanceUID) { function _getSeries(StudyInstanceUID, SeriesInstanceUID) {
@ -90,6 +92,27 @@ const dicomMetadataStore = {
study.addSeries(instances); study.addSeries(instances);
}, },
addStudy(study) {
const { StudyInstanceUID } = study;
let existingStudy = _model.studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
);
if (!existingStudy) {
const newStudy = new StudyMetadata(StudyInstanceUID);
newStudy.PatientID = study.PatientID;
newStudy.PatientName = study.PatientName;
newStudy.StudyDate = study.StudyDate;
newStudy.ModalitiesInStudy = study.ModalitiesInStudy;
newStudy.StudyDescription = study.StudyDescription;
newStudy.AccessionNumber = study.AccessionNumber;
newStudy.NumInstances = study.NumInstances; // todo: Correct naming?
_model.studies.push(newStudy);
}
},
getStudy: _getStudy, getStudy: _getStudy,
getSeries: _getSeries, getSeries: _getSeries,
getInstance: _getInstance, getInstance: _getInstance,

View File

@ -22,7 +22,7 @@ const getTrackedSeries = displaySets => {
return trackedSeries; return trackedSeries;
}; };
const StudyBrowser = ({ tabs }) => { const StudyBrowser = ({ tabs, onClickStudy, onClickThumbnail }) => {
const [tabActive, setTabActive] = useState(getInitialActiveTab(tabs)); const [tabActive, setTabActive] = useState(getInitialActiveTab(tabs));
const [studyActive, setStudyActive] = useState(null); const [studyActive, setStudyActive] = useState(null);
@ -54,10 +54,27 @@ const StudyBrowser = ({ tabs }) => {
isActive={isActive} isActive={isActive}
onClick={() => { onClick={() => {
setStudyActive(isActive ? null : studyInstanceUid); setStudyActive(isActive ? null : studyInstanceUid);
if (onClickStudy) {
onClickStudy(studyInstanceUid);
}
}}
/>
{isActive && displaySets && (
<ThumbnailList
thumbnails={displaySets}
thumbnailActive={thumbnailActive}
onThumbnailClick={thumbnailId => {
setThumbnailActive(
thumbnailId === thumbnailActive ? null : thumbnailId
);
if (onClickThumbnail) {
// TODO: what is thumbnailId? Should pass display set instead
onClickThumbnail(thumbnailId);
}
}} }}
/> />
{isActive && thumbnails && (
<ThumbnailList thumbnails={thumbnails} />
)} )}
</React.Fragment> </React.Fragment>
); );
@ -103,6 +120,8 @@ const StudyBrowser = ({ tabs }) => {
}; };
StudyBrowser.propTypes = { StudyBrowser.propTypes = {
onClickStudy: PropTypes.func,
onClickThumbnail: PropTypes.func,
tabs: PropTypes.arrayOf( tabs: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,