basic pub/sub refresh for tabs from displaysets from dicommetadata

This commit is contained in:
dannyrb 2020-05-21 20:34:53 -04:00
parent 1892ec2a2a
commit f87d18936e
9 changed files with 191 additions and 158 deletions

View File

@ -12,107 +12,66 @@ function PanelStudyBrowser({
requestDisplaySetCreationForStudy,
dataSource,
}) {
const currentDisplaySets = DisplaySetService.activeDisplaySets || [];
// TODO: Deep copy? Or By IDs?
// TODO: May need to be mapped to a different shape?
const [displaySets, setDisplaySets] = useState(currentDisplaySets);
const [activeTabName, setActiveTabName] = useState('primary');
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState(new Map());
// Tabs --> Studies --> DisplaySets --> Thumbnails
const [{ StudyInstanceUIDs }, dispatch] = useImageViewer();
// vv What we want to render
const [activeTabName, setActiveTabName] = useState('primary');
const [studyDisplayList, setStudyDisplayList] = useState([]);
const [displaySets, setDisplaySets] = useState([]);
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState(new Map());
// ~~ 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,
date: qidoStudy.StudyDate,
description: qidoStudy.StudyDescription,
modalities: qidoStudy.ModalitiesInStudy,
numInstances: qidoStudy.NumInstances,
// displaySets: []
};
});
const updatedStudyDisplayList = _mergeStudyDisplayListAndDataSourceStudies(
studyDisplayList,
mappedStudies,
displaySets
);
addDisplaySetsToStudyDisplayList(
updatedStudyDisplayList,
displaySets,
thumbnailImageSrcMap
);
setStudyDisplayList(updatedStudyDisplayList);
setStudyDisplayList(actuallyMappedStudies);
}
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
}, [
StudyInstanceUIDs,
getStudiesForPatientByStudyInstanceUID,
]);
}, [StudyInstanceUIDs, getStudiesForPatientByStudyInstanceUID]);
const updateThumbnailMap = (displaySetInstanceUID, imageSrc) => {
setThumbnailImageSrcMap(
thumbnailImageSrcMap.set(displaySetInstanceUID, imageSrc)
// ~~ displaySets
useEffect(() => {
// TODO: Deep copy? Or By IDs?
// TODO: May need to be mapped to a different shape?
// TODO: Iterate over `studyDisplayList` and map these for all studies in list?
const currentDisplaySets = DisplaySetService.activeDisplaySets || [];
const mappedDisplaySets = _mapDisplaySets(
currentDisplaySets,
thumbnailImageSrcMap
);
};
function setLocalDisplaySetsState(displaySets) {
const displaySetsUI = displaySets.map(ds => {
const imageSrc = thumbnailImageSrcMap.get(ds.displaySetInstanceUID);
return {
displaySetInstanceUID: ds.displaySetInstanceUID,
description: ds.SeriesDescription,
seriesNumber: ds.SeriesNumber,
modality: ds.Modality,
date: ds.SeriesDate,
numInstances: ds.numImageFrames,
StudyInstanceUID: ds.StudyInstanceUID,
componentType: 'thumbnail', // 'thumbnailNoImage' || 'thumbnailTracked' // TODO: PUT THIS SOMEWHERE ELSE
imageSrc,
dragData: {
type: 'displayset',
displaySetInstanceUID: ds.displaySetInstanceUID,
// .. Any other data to pass
},
};
});
setDisplaySets(displaySetsUI);
}
function addDisplaySetsToStudyDisplayList(
studies,
displaySets,
thumbnailImageSrcMap
) {
displaySets.forEach(displaySet => {
const study = studies.find(
s => s.studyInstanceUid === displaySet.StudyInstanceUID
);
if (!study) {
return;
}
displaySet.imageSrc = thumbnailImageSrcMap.get(
displaySet.displaySetInstanceUID
);
study.displaySets.push(displaySet);
});
}
setDisplaySets(mappedDisplaySets);
}, [thumbnailImageSrcMap]);
async function handleDisplaySetsAdded(newDisplaySets) {
console.warn('~~ handleDisplaySetsAdded');
// First, launch requests for a thumbnail for the new display sets
newDisplaySets.forEach(async dset => {
const uid = dset.displaySetInstanceUID;
const imageIds = dataSource.getImageIdsForDisplaySet(dset);
const imageId = imageIds[Math.floor(imageIds.length / 2)];
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
const imageSrc = await getImageSrc(imageId);
updateThumbnailMap(uid, imageSrc);
setThumbnailImageSrcMap(
thumbnailImageSrcMap.set(dset.displaySetInstanceUID, imageSrc)
);
});
}
@ -122,9 +81,21 @@ function PanelStudyBrowser({
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
handleDisplaySetsAdded
),
// TODO: Should this event indicate batch/series/study?
// Naming feels odd, and result is non-obvious
// Will this always contain _all_ displaySets we care about?
DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED,
setLocalDisplaySetsState
changedDisplaySets => {
console.warn('DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED', changedDisplaySets);
const mappedDisplaySets = _mapDisplaySets(
changedDisplaySets,
thumbnailImageSrcMap
);
setDisplaySets(mappedDisplaySets);
}
),
];
@ -133,33 +104,12 @@ function PanelStudyBrowser({
};
}, []);
const primaryStudies = studyDisplayList.filter(study =>
StudyInstanceUIDs.includes(study.studyInstanceUid)
const tabs = _createStudyBrowserTabs(
StudyInstanceUIDs,
studyDisplayList,
displaySets
);
// TODO: Filter allStudies to dates within one year of current date
const recentStudies = studyDisplayList.filter(study => {
return true; // TODO: check study.date
});
const tabs = [
{
name: 'primary',
label: 'Primary',
studies: primaryStudies || [],
},
{
name: 'recent',
label: 'Recent',
studies: recentStudies,
},
{
name: 'all',
label: 'All',
studies: studyDisplayList,
},
];
// TODO: Should "expand" appropriate study (already handled by component?)
// TODO: Should not fire this on "close"
function _handleStudyClick(StudyInstanceUID) {
@ -214,28 +164,84 @@ function _mapDataSourceStudies(studies) {
});
}
function _mergeStudyDisplayListAndDataSourceStudies(
studyDisplayList,
mappedQidoStudies
) {
return mappedQidoStudies.map(qidoStudy => {
const { StudyInstanceUID } = qidoStudy;
const existing =
studyDisplayList.find(a => a.studyInstanceUid === StudyInstanceUID) || {};
return Object.assign(
{},
existing,
{
displaySets: [],
function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
console.warn('~~ setLocalDisplaySetsState');
return displaySets.map(ds => {
const imageSrc = thumbnailImageSrcMap.get(ds.displaySetInstanceUID);
return {
displaySetInstanceUID: ds.displaySetInstanceUID,
description: ds.SeriesDescription,
seriesNumber: ds.SeriesNumber,
modality: ds.Modality,
date: ds.SeriesDate,
numInstances: ds.numImageFrames,
StudyInstanceUID: ds.StudyInstanceUID,
componentType: 'thumbnail', // 'thumbnailNoImage' || 'thumbnailTracked' // TODO: PUT THIS SOMEWHERE ELSE
imageSrc,
dragData: {
type: 'displayset',
displaySetInstanceUID: ds.displaySetInstanceUID,
// .. Any other data to pass
},
{
studyInstanceUid: StudyInstanceUID,
date: qidoStudy.StudyDate,
description: qidoStudy.StudyDescription,
modalities: qidoStudy.ModalitiesInStudy,
numInstances: qidoStudy.NumInstances,
}
);
};
});
}
/**
*
* @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;
}

View File

@ -1,5 +1,5 @@
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
import StudyMetadata from './StudyMetadata';
import createStudyMetadata from './createStudyMetadata';
import EVENTS from './EVENTS';
const _model = {
@ -57,20 +57,26 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
const BaseImplementation = {
EVENTS,
listeners: {},
// TODO: The assumption is that this is called per Study per Series
// We should do more to verify/clarify that
addInstances(instances) {
const { StudyInstanceUID } = instances[0];
const { StudyInstanceUID, SeriesInstanceUID } = instances[0];
let study = _model.studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
);
if (!study) {
_model.studies.push(new StudyMetadata(StudyInstanceUID));
_model.studies.push(createStudyMetadata(StudyInstanceUID));
study = _model.studies[_model.studies.length - 1];
}
study.addSeries(instances);
this._broadcastEvent(EVENTS.INSTANCES_ADDED, {
StudyInstanceUID,
SeriesInstanceUID,
});
},
addStudy(study) {
const { StudyInstanceUID } = study;

View File

@ -1,6 +1,5 @@
const EVENTS = {
DISPLAY_SETS_ADDED: 'event::displaySetService:displaySetsAdded',
DISPLAY_SETS_CHANGED: 'event::displaySetService:displaySetsChanged',
INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded',
};
export default EVENTS;

View File

@ -1,9 +0,0 @@
class SeriesMetadata {
constructor(instances) {
const { SeriesInstanceUID } = instances[0];
this.SeriesInstanceUID = SeriesInstanceUID;
this.instances = instances;
}
}
export default SeriesMetadata;

View File

@ -1,14 +0,0 @@
import SeriesMetadata from './SeriesMetadata';
class StudyMetadata {
constructor(StudyInstanceUID) {
this.StudyInstanceUID = StudyInstanceUID;
this.series = [];
}
addSeries(instances) {
this.series.push(new SeriesMetadata(instances));
}
}
export default StudyMetadata;

View File

@ -0,0 +1,10 @@
function createSeriesMetadata(instances) {
const { SeriesInstanceUID } = instances[0];
return {
SeriesInstanceUID,
instances,
};
}
export default createSeriesMetadata;

View File

@ -0,0 +1,14 @@
import createSeriesMetadata from './createSeriesMetadata';
function createStudyMetadata(StudyInstanceUID) {
return {
StudyInstanceUID,
series: [],
addSeries: function(instances) {
const series = createSeriesMetadata(instances);
this.series.push(series);
},
};
}
export default createStudyMetadata;

View File

@ -38,7 +38,6 @@ function DataSourceWrapper(props) {
const firstAppConfigDataSource = appConfig.dataSources[0];
const dataSourceConfig = firstAppConfigDataSource.configuration;
const firstWebApiDataSource = webApiDataSources[0];
debugger;
const dataSource = firstWebApiDataSource.createDataSource(dataSourceConfig);
// Route props --> studies.mapParams

View File

@ -1,8 +1,8 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router';
import PropTypes from 'prop-types';
//
import { ToolBarManager } from '@ohif/core';
// TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore, ToolBarManager } from '@ohif/core';
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
//
import { useQuery } from '@hooks';
@ -91,10 +91,31 @@ export default function ModeRoute({
console.log('queryParams: ', queryParams);
// Call the data source to start building the view model?
dataSource.retrieve.series.metadata(
queryParams,
DisplaySetService.makeDisplaySets
// TODO: This should be called on subscription to DicomMetadataStore?
// dataSource.retrieve.series.metadata(
// queryParams,
// DisplaySetService.makeDisplaySets
// );
// TODO: This should be baked into core, not manuel?
// DisplaySetService would wire this up?
DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
({ StudyInstanceUID, SeriesInstanceUID }) => {
console.warn(`INSTANCES_ADDED::\nStudy:${StudyInstanceUID}\nSeries:${SeriesInstanceUID}`);
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
DisplaySetService.makeDisplaySets(seriesMetadata.instances);
}
);
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
});
}, [
mode,
dataSourceName,
@ -102,7 +123,8 @@ export default function ModeRoute({
DisplaySetService,
extensionManager,
sopClassHandlers,
dataSource.retrieve.series,
StudyInstanceUIDsAsArray,
dataSource,
]);
const reducer = (state, action) => {