From a323157c6285213962267449c4cb66a5fc120886 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Wed, 20 May 2020 21:07:25 -0400 Subject: [PATCH] capture changes --- .vscode/extensions.json | 5 +- .../default/src/DicomWebDataSource/index.js | 29 +++ .../default/src/Panels/PanelStudyBrowser.jsx | 182 ++++++------------ .../src/Panels/WrappedPanelStudyBrowser.jsx | 23 ++- .../getStudiesForPatientByStudyInstanceUID.js | 23 +++ .../requestDisplaySetCreationForStudy.js | 22 +++ .../DisplaySetService/DisplaySetService.js | 6 + platform/ui/index.js | 3 + .../contextProviders/ImageViewerProvider.jsx | 18 ++ platform/ui/src/contextProviders/index.js | 6 + platform/viewer/src/routes/Mode/Mode.jsx | 43 +++-- 11 files changed, 211 insertions(+), 149 deletions(-) create mode 100644 extensions/default/src/Panels/getStudiesForPatientByStudyInstanceUID.js create mode 100644 extensions/default/src/Panels/requestDisplaySetCreationForStudy.js create mode 100644 platform/ui/src/contextProviders/ImageViewerProvider.jsx diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ecb995a56..eca7ac2a9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -5,6 +5,9 @@ "dbaeumer.vscode-eslint", "mikestead.dotenv", "bungcip.better-toml", - "silvenon.mdx" + "silvenon.mdx", + "gruntfuggly.todo-tree", + "wayou.vscode-todo-highlight", + "bradlc.vscode-tailwindcss" ] } diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index ad0ca2199..5f232a8c5 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -87,6 +87,9 @@ function createDicomWebApi(dicomWebConfig) { }, retrieve: { series: { + // TODO: change queryParams to `StudyInstanceUID` for now? + // Conduct query, return a promise like others + // Await this call and add to dicomMetadataStore after receiving result metadata: (queryParams, callback) => { let { StudyInstanceUIDs } = urlUtil.parse(queryParams, true); @@ -125,6 +128,32 @@ function createDicomWebApi(dicomWebConfig) { }, }, }, + retrieveSeriesMetadata: async ({ StudyInstanceUID } = {}) => { + if (!StudyInstanceUID) { + throw new Error( + 'Unable to query for SeriesMetadata without StudyInstanceUID' + ); + } + + // Get Series + const seriesPromises = await retrieveStudyMetadata( + naturalizeDataset, + StudyInstanceUID, + enableStudyLazyLoad + ); + + // Async load series, store as retrieved + function storeInstances(instances) { + const naturalizedInstances = instances.map(naturalizeDataset); + + dicomMetadataStore.addInstances(naturalizedInstances); + } + + seriesPromises.forEach(async seriesPromise => { + const instances = await seriesPromise; + storeInstances(instances); + }); + }, getImageIdsForDisplaySet(displaySet) { const images = displaySet.images; const imageIds = []; diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index d54ecdd2d..54e6eda4e 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -1,42 +1,53 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; -import { StudyBrowser } from '@ohif/ui'; +// +import { StudyBrowser, useImageViewer } from '@ohif/ui'; import { dicomMetadataStore } from '@ohif/core'; +// This has to import from somewhere else... function PanelStudyBrowser({ - servicesManager, + DisplaySetService, getImageSrc, - getStudiesByPatientId, + getStudiesForPatientByStudyInstanceUID, + requestDisplaySetCreationForStudy, dataSource, }) { - const { DisplaySetService } = servicesManager.services; 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); - // TODO: Grab from URL, or pass in as part of contract to panels - const [primaryStudyInstanceUID, setPrimaryStudyInstanceUID] = useState(); const [activeTabName, setActiveTabName] = useState('primary'); - const [studyData, setStudyData] = useState([]); - const [studiesForPatient, setStudiesForPatient] = useState([]); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState(new Map()); + const [{ StudyInstanceUIDs }, dispatch] = useImageViewer(); + // vv What we want to render + const [studyDisplayList, setStudyDisplayList] = useState([]); useEffect(() => { - const mappedStudiesFromDataSource = _mapDataSourceStudies( - studiesForPatient - ); + // Fetch all studies for the patient in each primary study + async function fetchStudiesForPatient(StudyInstanceUID) { + const qidoStudiesForPatient = + (await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || []; - const newStudyData = _mergeStudyDataAndDataSourceStudies( - studyData, - mappedStudiesFromDataSource, - displaySets - ); + const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient); - addDisplaySetsToStudyData(newStudyData, displaySets, thumbnailImageSrcMap); + const updatedStudyDisplayList = _mergeStudyDisplayListAndDataSourceStudies( + studyDisplayList, + mappedStudies, + displaySets + ); + + addDisplaySetsToStudyDisplayList( + updatedStudyDisplayList, + displaySets, + thumbnailImageSrcMap + ); + + setStudyDisplayList(updatedStudyDisplayList); + } + + StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); + }, [StudyInstanceUIDs, getStudiesForPatientByStudyInstanceUID]); - setStudyData(newStudyData); - }, [studiesForPatient, thumbnailImageSrcMap]); const updateThumbnailMap = (displaySetInstanceUID, imageSrc) => { setThumbnailImageSrcMap( @@ -68,7 +79,7 @@ function PanelStudyBrowser({ setDisplaySets(displaySetsUI); } - function addDisplaySetsToStudyData( + function addDisplaySetsToStudyDisplayList( studies, displaySets, thumbnailImageSrcMap @@ -101,40 +112,9 @@ function PanelStudyBrowser({ const imageSrc = await getImageSrc(imageId); updateThumbnailMap(uid, imageSrc); }); - - const firstDisplaySet = newDisplaySets[0]; - if (!studiesForPatient.length && firstDisplaySet) { - async function doAsyncStuff() { - const qidoStuff = await _getStudiesByPatientId( - firstDisplaySet, - getStudiesByPatientId - ); - - setStudiesForPatient(qidoStuff); - setPrimaryStudyInstanceUID(firstDisplaySet.StudyInstanceUID); - } - - doAsyncStuff(); - } } useEffect(() => { - // TODO: IF WE HAVE OUR FIRST DISPLAY SET... - // QIDO AWAY - // IF WE DO NOT, WAIT FOR FIRST DISPLAY_SETS_ADDED - const firstDisplaySet = displaySets[0]; - if (firstDisplaySet) { - async function doAsyncStuff() { - const qidoStuff = await _getStudiesByPatientId( - firstDisplaySet, - getStudiesByPatientId - ); - setPrimaryStudyInstanceUID(firstDisplaySet.StudyInstanceUID); - setStudiesForPatient(qidoStuff); - } - doAsyncStuff(); - } - const subscriptions = [ DisplaySetService.subscribe( DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, @@ -151,12 +131,12 @@ function PanelStudyBrowser({ }; }, []); - const primary = studyData.find( - study => study.studyInstanceUid === primaryStudyInstanceUID + const primaryStudies = studyDisplayList.filter(study => + StudyInstanceUIDs.includes(study.studyInstanceUid) ); // TODO: Filter allStudies to dates within one year of current date - const recentStudies = studyData.filter(study => { + const recentStudies = studyDisplayList.filter(study => { return true; // TODO: check study.date }); @@ -164,7 +144,7 @@ function PanelStudyBrowser({ { name: 'primary', label: 'Primary', - studies: primary ? [primary] : [], + studies: primaryStudies || [], }, { name: 'recent', @@ -174,63 +154,42 @@ function PanelStudyBrowser({ { name: 'all', label: 'All', - studies: studyData, + studies: studyDisplayList, }, ]; - function onClickStudy(StudyInstanceUID) { - const study = studyData.find(a => a.studyInstanceUid === StudyInstanceUID); - if (study && study.displaySets && study.displaySets.length) { - return; - } - - 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, - DisplaySetService.makeDisplaySets - ); + // TODO: Should "expand" appropriate study (already handled by component?) + // TODO: Should not fire this on "close" + function _handleStudyClick(StudyInstanceUID) { + requestDisplaySetCreationForStudy(DisplaySetService, StudyInstanceUID); } - const memoOnClickStudy = useCallback( - StudyInstanceUID => { - onClickStudy(StudyInstanceUID); - }, - [studyData] - ); - return ( ); } PanelStudyBrowser.propTypes = { + DisplaySetService: PropTypes.shape({ + EVENTS: PropTypes.object.isRequired, + hasDisplaySetsForStudy: PropTypes.func.isRequired, + subscribe: PropTypes.func.isRequired, + }).isRequired, dataSource: PropTypes.shape({ getImageIdsForDisplaySet: PropTypes.func.isRequired, }).isRequired, getImageSrc: PropTypes.func.isRequired, - getStudiesByPatientId: PropTypes.func.isRequired, + getStudiesForPatientByStudyInstanceUID: PropTypes.func.isRequired, + requestDisplaySetCreationForStudy: PropTypes.func.isRequired, }; export default PanelStudyBrowser; -async function _getStudiesByPatientId(displaySet, getStudiesByPatientId) { - const study = dicomMetadataStore.getStudy(displaySet.StudyInstanceUID); - const instance = study.series[0].instances[0]; - const PatientID = instance.PatientID; - const studiesByPatientId = await getStudiesByPatientId(PatientID); - - return studiesByPatientId; -} - /** * Maps from the DataSource's format to a naturalized object * @@ -253,11 +212,14 @@ function _mapDataSourceStudies(studies) { }); } -function _mergeStudyDataAndDataSourceStudies(studyData, mappedQidoStudies) { +function _mergeStudyDisplayListAndDataSourceStudies( + studyDisplayList, + mappedQidoStudies +) { return mappedQidoStudies.map(qidoStudy => { const { StudyInstanceUID } = qidoStudy; const existing = - studyData.find(a => a.studyInstanceUid === StudyInstanceUID) || {}; + studyDisplayList.find(a => a.studyInstanceUid === StudyInstanceUID) || {}; return Object.assign( {}, @@ -275,39 +237,3 @@ function _mergeStudyDataAndDataSourceStudies(studyData, mappedQidoStudies) { ); }); } - -function _mergeStudyDataAndInstanceStudies(studyData, mappedInstanceStudies) { - const merged = []; - mappedInstanceStudies.map(studyFromInstances => { - const existing = studyData.find( - a => a.studyInstanceUid === studyFromInstances.studyInstanceUid - ); - - if (existing) { - merged.push( - Object.assign({}, existing, { - displaySets: studyFromInstances.displaySets, - modalities: studyFromInstances.modalities, - }) - ); - } else { - merged.push(studyFromInstances); - } - }); - - return merged; -} - -function _mapStudyFromInstance(StudyInstanceUID) { - const study = dicomMetadataStore.getStudy(StudyInstanceUID); - const anInstance = study.series[0].instances[0]; - - return { - studyInstanceUid: StudyInstanceUID, - date: anInstance.StudyDate, // TODO: Format this date to DD-MMM-YYYY - description: anInstance.StudyDescription, - displaySets: [], - numInstances: 0, - modalitiesSet: new Set(), - }; -} diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx index df7cf158a..c3bb27d01 100644 --- a/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; // import PanelStudyBrowser from './PanelStudyBrowser'; import getImageSrcFromImageId from './getImageSrcFromImageId'; +import getStudiesForPatientByStudyInstanceUID from './getStudiesForPatientByStudyInstanceUID'; +import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy'; /** * Wraps the PanelStudyBrowser and provides features afforded by managers/services @@ -16,20 +18,30 @@ function WrappedPanelStudyBrowser({ extensionManager, servicesManager, }) { - // Note: this feels odd + // TODO: This should be made available a different way; route should have + // already determined our datasource const dataSource = extensionManager.getDataSources('dicomweb')[0]; - const getStudiesByPatientId = patientId => - dataSource.query.studies.search({ patientId }); + const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind( + null, + dataSource + ); const _getImageSrcFromImageId = _createGetImageSrcFromImageIdFn( commandsManager.getCommand.bind(commandsManager) ); + const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind( + null, + dataSource + ); return ( ); } @@ -60,6 +72,7 @@ function _createGetImageSrcFromImageIdFn(getCommand) { WrappedPanelStudyBrowser.propTypes = { commandsManager: PropTypes.object.isRequired, extensionManager: PropTypes.object.isRequired, + servicesManager: PropTypes.object.isRequired, }; export default WrappedPanelStudyBrowser; diff --git a/extensions/default/src/Panels/getStudiesForPatientByStudyInstanceUID.js b/extensions/default/src/Panels/getStudiesForPatientByStudyInstanceUID.js new file mode 100644 index 000000000..e9084a8f1 --- /dev/null +++ b/extensions/default/src/Panels/getStudiesForPatientByStudyInstanceUID.js @@ -0,0 +1,23 @@ +async function getStudiesForPatientByStudyInstanceUID( + dataSource, + StudyInstanceUID +) { + // TODO: The `dicomMetadataStore` should short-circuit both of these requests + // Data _could_ be here from route query, or if using JSON data source + // We could also force this to "await" these values being available in the DICOMStore? + // Kind of like promise fulfillment in cornerstone-wado-image-loader when there are multiple + // outgoing requests for the same data + const getStudyResult = await dataSource.query.studies.search({ + studyInstanceUid: StudyInstanceUID, + }); + + // TODO: To Erik's point, the data source likely shouldn't deviate from + // Naturalized DICOM JSON when returning. It makes things like this awkward (mrn) + if (getStudyResult && getStudyResult.length && getStudyResult[0].mrn) { + return dataSource.query.studies.search({ + patientId: getStudyResult[0].mrn, + }); + } +}; + +export default getStudiesForPatientByStudyInstanceUID; diff --git a/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js b/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js new file mode 100644 index 000000000..cf0867c37 --- /dev/null +++ b/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js @@ -0,0 +1,22 @@ +function requestDisplaySetCreationForStudy( + dataSource, + DisplaySetService, + StudyInstanceUID +) { + // TODO: is this already short-circuited by the map of Retrieve promises? + if (DisplaySetService.hasDisplaySetsForStudy(StudyInstanceUID)) { + return; + } + + dataSource.retrieveSeriesMetadata({ StudyInstanceUID }); + + // 1. Update DICOMStore + // 2. DICOMStore shoots out events + // 3. IFF instance is in study/series that is "active" (being viewed?), displaySet is created? + // 4. IFF not, just store is updated + // 5. IFF active studies change, splitting logic is refired? + + // DisplaySetService.makeDisplaySets +} + +export default requestDisplaySetCreationForStudy; diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.js b/platform/core/src/services/DisplaySetService/DisplaySetService.js index 08f296c68..e31f1c43b 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.js +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.js @@ -107,6 +107,12 @@ export default class DisplaySetService { } }; + hasDisplaySetsForStudy(StudyInstanceUID) { + return displaySetCache.some( + displaySet => displaySet.StudyInstanceUID === StudyInstanceUID + ); + } + makeDisplaySetForInstances(instances) { const instance = instances[0]; diff --git a/platform/ui/index.js b/platform/ui/index.js index b2ab9db71..7c33669f1 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -11,6 +11,9 @@ export { withModal, ViewportDialogProvider, useViewportDialog, + ImageViewerContext, + ImageViewerProvider, + useImageViewer, } from './src/contextProviders'; /** COMPONENTS */ diff --git a/platform/ui/src/contextProviders/ImageViewerProvider.jsx b/platform/ui/src/contextProviders/ImageViewerProvider.jsx new file mode 100644 index 000000000..a15c1c628 --- /dev/null +++ b/platform/ui/src/contextProviders/ImageViewerProvider.jsx @@ -0,0 +1,18 @@ +import React, { createContext, useContext, useReducer } from 'react'; + +// export const IMAGE_VIEWER_DEFAULT_VALUE = { +// StudyInstanceUIDs: [], +// setImageViewer: () => {}, +// }; + +export const ImageViewerContext = createContext(); + +export function ImageViewerProvider({ reducer, initialState, children }) { + return ( + + {children} + + ); +} + +export const useImageViewer = () => useContext(ImageViewerContext); diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js index 8c5abfbc0..d879ca9ef 100644 --- a/platform/ui/src/contextProviders/index.js +++ b/platform/ui/src/contextProviders/index.js @@ -10,4 +10,10 @@ export { useViewportDialog, } from './ViewportDialogProvider'; +export { + ImageViewerContext, + ImageViewerProvider, + useImageViewer, +} from './ImageViewerProvider'; + export { default as DragAndDropProvider } from './DragAndDropProvider'; diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx index d8c0ed175..e238f6ac4 100644 --- a/platform/viewer/src/routes/Mode/Mode.jsx +++ b/platform/viewer/src/routes/Mode/Mode.jsx @@ -1,14 +1,13 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect } from 'react'; import { useParams } from 'react-router'; import PropTypes from 'prop-types'; // import { ToolBarManager } from '@ohif/core'; -import { DragAndDropProvider } from '@ohif/ui'; +import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui'; // import { useQuery } from '@hooks'; import ViewportGrid from '@components/ViewportGrid'; import Compose from './Compose'; -//import DisplaySetCreator from './DisplaySetCreator'; export default function ModeRoute({ location, @@ -22,6 +21,10 @@ export default function ModeRoute({ const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs'); const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = useParams(); const StudyInstanceUIDs = queryStudyInstanceUIDs || paramsStudyInstanceUIDs; + const StudyInstanceUIDsAsArray = + StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) + ? StudyInstanceUIDs + : [StudyInstanceUIDs]; const { extensions, sopClassHandlers } = mode; // TODO: @@ -89,19 +92,28 @@ export default function ModeRoute({ ); }, [mode, dataSourceName, location]); + const reducer = (state, action) => { + console.log(state, action); + } + return ( - - {/* TODO: extensionManager is already provided to the extension module. - * Use it from there instead of passing as a prop here. - */} - - - - + + + {/* TODO: extensionManager is already provided to the extension module. + * Use it from there instead of passing as a prop here. + */} + + + + + ); } @@ -117,4 +129,5 @@ ModeRoute.propTypes = { mode: PropTypes.object.isRequired, dataSourceName: PropTypes.string, extensionManager: PropTypes.object, + servicesManager: PropTypes.object, };