From ec2df02e83015a6506710373c57b08211c8229f9 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Fri, 15 May 2020 10:05:40 +0200 Subject: [PATCH] Improve study browser --- .../default/src/DicomWebDataSource/index.js | 4 +- .../default/src/DicomWebDataSource/qido.js | 4 +- extensions/default/src/ViewerLayout/index.jsx | 2 +- .../default/src/getLayoutTemplateModule.js | 1 - extensions/default/src/getPanelModule.js | 230 +++++++++++------- .../default/src/getSopClassHandlerModule.js | 9 +- platform/core/src/dicomMetadataStore.js | 25 +- .../components/StudyBrowser/StudyBrowser.jsx | 25 +- 8 files changed, 195 insertions(+), 105 deletions(-) diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index d20be0301..f609a2d3e 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -80,13 +80,13 @@ function createDicomWebApi(dicomWebConfig) { // processResults: processResults.bind(), }, instances: { - search: (studyInstanceUid, queryParamaters) => + search: (studyInstanceUid, queryParameters) => qidoSearch.call( undefined, qidoDicomWebClient, studyInstanceUid, null, - queryParamaters + queryParameters ), }, }, diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DicomWebDataSource/qido.js index 3b2ac3592..83613dc0c 100644 --- a/extensions/default/src/DicomWebDataSource/qido.js +++ b/extensions/default/src/DicomWebDataSource/qido.js @@ -105,12 +105,12 @@ function search( dicomWebClient, studyInstanceUid, seriesInstanceUid, - queryParamaters + queryParameters ) { // Studies return dicomWebClient.searchForStudies({ studyInstanceUid: undefined, - queryParams: queryParamaters, + queryParams: queryParameters, }); } diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index 37f03ba51..b163d7d94 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -31,7 +31,7 @@ function ViewerLayout({ const getPanelData = id => { const entry = extensionManager.getModuleEntry(id); // 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 { iconName: entry.iconName, diff --git a/extensions/default/src/getLayoutTemplateModule.js b/extensions/default/src/getLayoutTemplateModule.js index 23ad78db0..6df430358 100644 --- a/extensions/default/src/getLayoutTemplateModule.js +++ b/extensions/default/src/getLayoutTemplateModule.js @@ -1,4 +1,3 @@ - import ViewerLayout from './ViewerLayout'; /* - Define layout for the viewer in mode configuration. diff --git a/extensions/default/src/getPanelModule.js b/extensions/default/src/getPanelModule.js index d0068aa44..392392bd9 100644 --- a/extensions/default/src/getPanelModule.js +++ b/extensions/default/src/getPanelModule.js @@ -1,55 +1,155 @@ -import React from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; 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); - const studies = {}; - displaySets.forEach(displaySet => { + if (!displaySets.length) { + 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( - a => a.displaySetInstanceUid === displaySet.displaySetInstanceUid + a => a.displaySetInstanceUid === ds.displaySetInstanceUid ); if (displaySetViewportData) { displaySet.viewportIdentificator = displaySetViewportData.identifier; } - const trackingInfo = seriesTracking[displaySet.SeriesInstanceUID]; + const trackingInfo = seriesTracking[ds.SeriesInstanceUID]; if (trackingInfo) { - displaySet.viewportIdentificator = trackingInfo.isTracked; + displaySet.isTracked = trackingInfo.isTracked; displaySet.componentType = trackingInfo.isTracked ? 'thumbnailTracked' : 'thumbnail'; + } else { + displaySet.isTracked = false; + displaySet.componentType = 'thumbnail'; } - if (!Object.keys(studies).includes(displaySet.StudyInstanceUID)) { - studies[displaySet.StudyInstanceUID] = { - date: displaySet.SeriesDate, - description: displaySet.SeriesDescription, - seriesNumber: displaySet.SeriesNumber, - modalities: displaySet.modalities, + if (!Object.keys(studiesFromInstanceData).includes(ds.StudyInstanceUID)) { + const study = dicomMetadataStore.getStudy(ds.StudyInstanceUID); + const anInstance = study.series[0].instances[0]; + + studiesFromInstanceData[ds.StudyInstanceUID] = { + date: anInstance.StudyDate, // TODO: Format this date to DD-MMM-YYYY + description: anInstance.StudyDescription, displaySets: [], numInstances: 0, + modalitiesSet: new Set(), }; } - studies[displaySet.StudyInstanceUID].displaySets.push(displaySet); - studies[displaySet.StudyInstanceUID].numInstances += - displaySet.numImageFrames; + studiesFromInstanceData[ds.StudyInstanceUID].displaySets.push(displaySet); + studiesFromInstanceData[ds.StudyInstanceUID].numInstances += + 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 => { - const study = studies[StudyInstanceUID]; + // QIDO for all by MRN + const allStudies = studyData.map(studyLevelData => { + const studyFromInstanceData = + studiesFromInstanceData[studyLevelData.StudyInstanceUID]; + + if (!studyFromInstanceData) { + return { + studyInstanceUid: studyLevelData.StudyInstanceUID, + date: studyLevelData.StudyDate, + description: studyLevelData.StudyDescription, + modalities: studyLevelData.ModalitiesInStudy, + numInstances: studyLevelData.NumInstances, + displaySets: [], + }; + } + return { - studyInstanceUid: StudyInstanceUID, - ...study, + 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? }); - console.log(primary); - + // TODO: Filter allStudies to dates within one year of current date const recentStudies = allStudies.filter(study => { 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 = [ { name: 'primary', label: 'Primary', - studies: [primary], + studies: primary ? [primary] : [], }, { name: 'recent', @@ -100,51 +180,27 @@ function displaySetsToStudyPanelInfo( }, ]; - return tabs; -} + function onClickStudy(StudyInstanceUID) { + if (studiesFromInstanceData[StudyInstanceUID]) { + return; + } -function StudyBrowserPanel({}) { - const viewModel = useViewModel(); + 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}`; - // 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 + dataSource.retrieve.series.metadata( + queryParams, + displaySetManager.makeDisplaySets ); - } else { - tabs = [ - { - name: 'primary', - label: 'Primary', - studies: [], - }, - { - name: 'recent', - label: 'Recent', - studies: [], - }, - { - name: 'all', - label: 'All', - studies: [], - }, - ]; } - return ; + const memoOnClickStudy = useCallback(StudyInstanceUID => { + onClickStudy(StudyInstanceUID); + }); + + return ; } function getPanelModule() { diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js index cc08f6d47..b700a3ef1 100644 --- a/extensions/default/src/getSopClassHandlerModule.js +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -13,13 +13,6 @@ const makeDisplaySet = instances => { const instance = instances[0]; 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... imageSet.setAttributes({ displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID @@ -30,7 +23,7 @@ const makeDisplaySet = instances => { SeriesNumber: instance.SeriesNumber, FrameRate: instance.FrameTime, SeriesDescription: instance.SeriesDescription, - modalities: modalitiesInDisplaySet, + Modality: instance.Modality, isMultiFrame: isMultiFrame(instance), numImageFrames: instances.length, SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, diff --git a/platform/core/src/dicomMetadataStore.js b/platform/core/src/dicomMetadataStore.js index 5084ffcea..eb824df25 100644 --- a/platform/core/src/dicomMetadataStore.js +++ b/platform/core/src/dicomMetadataStore.js @@ -47,7 +47,9 @@ const _model = { }; function _getStudy(StudyInstanceUID) { - return _model.studies.find(StudyInstanceUID); + return _model.studies.find( + aStudy => aStudy.StudyInstanceUID === StudyInstanceUID + ); } function _getSeries(StudyInstanceUID, SeriesInstanceUID) { @@ -90,6 +92,27 @@ const dicomMetadataStore = { 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, getSeries: _getSeries, getInstance: _getInstance, diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx index d2b019be7..782e57bd7 100644 --- a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx +++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx @@ -22,7 +22,7 @@ const getTrackedSeries = displaySets => { return trackedSeries; }; -const StudyBrowser = ({ tabs }) => { +const StudyBrowser = ({ tabs, onClickStudy, onClickThumbnail }) => { const [tabActive, setTabActive] = useState(getInitialActiveTab(tabs)); const [studyActive, setStudyActive] = useState(null); @@ -54,10 +54,27 @@ const StudyBrowser = ({ tabs }) => { isActive={isActive} onClick={() => { setStudyActive(isActive ? null : studyInstanceUid); + + if (onClickStudy) { + onClickStudy(studyInstanceUid); + } }} /> - {isActive && thumbnails && ( - + {isActive && displaySets && ( + { + setThumbnailActive( + thumbnailId === thumbnailActive ? null : thumbnailId + ); + + if (onClickThumbnail) { + // TODO: what is thumbnailId? Should pass display set instead + onClickThumbnail(thumbnailId); + } + }} + /> )} ); @@ -103,6 +120,8 @@ const StudyBrowser = ({ tabs }) => { }; StudyBrowser.propTypes = { + onClickStudy: PropTypes.func, + onClickThumbnail: PropTypes.func, tabs: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string.isRequired,