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,
};