diff --git a/extensions/default/src/DicomJSONDataSource/index.js b/extensions/default/src/DicomJSONDataSource/index.js index b8f757225..4aaab6dad 100644 --- a/extensions/default/src/DicomJSONDataSource/index.js +++ b/extensions/default/src/DicomJSONDataSource/index.js @@ -43,7 +43,7 @@ function createDicomJSONApi(dicomJsonConfig) { const { name } = dicomJsonConfig; const implementation = { - parseRouteParams: async ({ params, query, url }) => { + initialize: async ({ params, query, url }) => { if (!url) url = query.get('url'); let metaData = getMetaDataByURL(url); @@ -93,11 +93,12 @@ function createDicomJSONApi(dicomJsonConfig) { }, query: { studies: { - mapParams: () => {}, + mapParams: () => { }, search: async param => { const [key, value] = Object.entries(param)[0]; const mappedParam = mappings[key]; + // todo: should fetch from dicomMetadataStore const studies = findStudies(mappedParam, value); return studies.map(aStudy => { diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js new file mode 100644 index 000000000..431fca8ae --- /dev/null +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -0,0 +1,204 @@ +import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core' +import OHIF from '@ohif/core' +import dcmjs from 'dcmjs'; + +const metadataProvider = OHIF.classes.MetadataProvider +const { EVENTS } = DicomMetadataStore + +// Sorting SR modalities to be at the end of series list +function customSort(seriesA, seriesB) { + const modalityA = seriesA.instances[0].Modality + const modalityB = seriesB.instances[0].Modality + + if (modalityA === "SR") { + return +1; + } + if (modalityB === "SR") { + return -1; + } + return 0; +} + + +function createDicomLocalApi(dicomLocalConfig) { + const { name } = dicomLocalConfig + + const implementation = { + initialize: ({ params, query }) => { + const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params + const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs') + + const StudyInstanceUIDs = + queryStudyInstanceUIDs || paramsStudyInstanceUIDs + const StudyInstanceUIDsAsArray = + StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) + ? StudyInstanceUIDs + : [StudyInstanceUIDs] + + + // Put SRs at the end of series list to make sure images are loaded first + StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => { + const study = DicomMetadataStore.getStudy(StudyInstanceUID) + study.series = study.series.sort(customSort) + }) + + return StudyInstanceUIDsAsArray + }, + query: { + studies: { + mapParams: () => { }, + search: (params) => { + const studyUIDs = DicomMetadataStore.getStudyInstanceUIDs() + + return studyUIDs.map(StudyInstanceUID => { + let numInstances = 0 + const modalities = new Set() + + // Calculating the number of instances in the study and modalities + // present in the study + const study = DicomMetadataStore.getStudy(StudyInstanceUID) + study.series.forEach(aSeries => { + numInstances += aSeries.instances.length + modalities.add(aSeries.Modality); + }) + + // first instance in the first series + const firstInstance = study?.series[0]?.instances[0] + + if (firstInstance) { + return { + accession: firstInstance.AccessionNumber, + date: firstInstance.StudyDate, + description: firstInstance.StudyDescription, + mrn: firstInstance.PatientID, + patientName: { Alphabetic: firstInstance.PatientName }, + studyInstanceUid: firstInstance.StudyInstanceUID, + time: firstInstance.StudyTime, + // + instances: numInstances, + modalities: Array.from(modalities).join('/'), + NumInstances: numInstances, + }; + } + }) + }, + processResults: () => { + console.debug(' DICOMLocal QUERY processResults') + }, + }, + series: { + // mapParams: mapParams.bind(), + search: () => { + console.debug(' DICOMLocal QUERY SERIES SEARCH') + }, + }, + instances: { + search: () => { + console.debug(' DICOMLocal QUERY instances SEARCH') + }, + }, + }, + retrieve: { + series: { + metaData: () => { + console.debug(' DICOMLocal retrieve series metadata') + }, + }, + }, + store: { + dicom: (naturalizedReport) => { + const reportBlob = dcmjs.data.datasetToBlob(naturalizedReport); + + //Create a URL for the binary. + var objectUrl = URL.createObjectURL(reportBlob); + window.location.assign(objectUrl); + }, + }, + retrieveSeriesMetadata: async ({ + StudyInstanceUID, + madeInClient = false, + } = {}) => { + if (!StudyInstanceUID) { + throw new Error( + 'Unable to query for SeriesMetadata without StudyInstanceUID' + ) + } + + // Instances metadata already added via local upload + const study = DicomMetadataStore.getStudy(StudyInstanceUID, madeInClient) + + // Series metadata already added via local upload + DicomMetadataStore._broadcastEvent(EVENTS.SERIES_ADDED, { + StudyInstanceUID, + madeInClient, + }) + + study.series.forEach((aSeries) => { + const { SeriesInstanceUID } = aSeries + + aSeries.instances.forEach((instance) => { + const { + url: imageId, + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + } = instance + + // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. + metadataProvider.addImageIdToUIDs(imageId, { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + }) + }) + + DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, { + StudyInstanceUID, + SeriesInstanceUID, + madeInClient, + }) + }) + + }, + getImageIdsForDisplaySet(displaySet) { + const images = displaySet.images + const imageIds = [] + + if (!images) { + return imageIds + } + + displaySet.images.forEach((instance) => { + const NumberOfFrames = instance.NumberOfFrames + + if (NumberOfFrames > 1) { + for (let i = 0; i < NumberOfFrames; i++) { + const imageId = this.getImageIdsForInstance({ + instance, + frame: i, + }) + imageIds.push(imageId) + } + } else { + const imageId = this.getImageIdsForInstance({ instance }) + imageIds.push(imageId) + } + }) + + return imageIds + }, + getImageIdsForInstance({ instance, frame }) { + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance + const storedInstance = DicomMetadataStore.getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) + if (storedInstance.url) { + return storedInstance.url + } + }, + deleteStudyMetadataPromise() { + console.log("deleteStudyMetadataPromise not implemented") + } + } + return IWebApiDataSource.create(implementation) +} + +export { createDicomLocalApi } diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 30a8e3cab..1e3a7c2d8 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -63,7 +63,7 @@ function createDicomWebApi(dicomWebConfig) { const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig); const implementation = { - parseRouteParams: ({ params, query }) => { + initialize: ({ params, query }) => { const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs'); @@ -78,7 +78,7 @@ function createDicomWebApi(dicomWebConfig) { query: { studies: { mapParams: mapParams.bind(), - search: async function(origParams) { + search: async function (origParams) { const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = mapParams(origParams, { supportsFuzzyMatching, @@ -98,7 +98,7 @@ function createDicomWebApi(dicomWebConfig) { }, series: { // mapParams: mapParams.bind(), - search: async function(studyInstanceUid) { + search: async function (studyInstanceUid) { const results = await seriesInStudy( qidoDicomWebClient, studyInstanceUid diff --git a/extensions/default/src/getDataSourcesModule.js b/extensions/default/src/getDataSourcesModule.js index d1d4601a5..dfccc6a18 100644 --- a/extensions/default/src/getDataSourcesModule.js +++ b/extensions/default/src/getDataSourcesModule.js @@ -4,6 +4,7 @@ import { createDicomWebApi } from './DicomWebDataSource/index.js'; import { createDicomJSONApi } from './DicomJSONDataSource/index.js'; +import { createDicomLocalApi } from './DicomLocalDataSource/index.js'; /** * @@ -20,6 +21,11 @@ function getDataSourcesModule() { type: 'jsonApi', createDataSource: createDicomJSONApi, }, + { + name: 'dicomlocal', + type: 'localApi', + createDataSource: createDicomLocalApi, + }, ]; } diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js index fda0fca22..5b0885ff4 100644 --- a/platform/core/src/DataSources/IWebApiDataSource.js +++ b/platform/core/src/DataSources/IWebApiDataSource.js @@ -17,7 +17,7 @@ function create({ retrieve, store, reject, - parseRouteParams, + initialize, retrieveSeriesMetadata, deleteStudyMetadataPromise, getImageIdsForDisplaySet, @@ -38,7 +38,7 @@ function create({ * @param {number} params.resultsPerPage */ mapParams: params => params, - requestResults: () => {}, + requestResults: () => { }, processResults: results => results, }, series: {}, @@ -63,7 +63,7 @@ function create({ retrieve: retrieve || defaultRetrieve, reject: reject || defaultReject, store: store || defaultStore, - parseRouteParams, + initialize, retrieveSeriesMetadata, deleteStudyMetadataPromise, getImageIdsForDisplaySet, diff --git a/platform/core/src/classes/MetadataProvider.js b/platform/core/src/classes/MetadataProvider.js index e6f2c739f..6c3f9b5d9 100644 --- a/platform/core/src/classes/MetadataProvider.js +++ b/platform/core/src/classes/MetadataProvider.js @@ -71,47 +71,47 @@ class MetadataProvider { this.imageIdToUIDs.set(imageId, uids); } - _getAndCacheStudy(StudyInstanceUID) { - const studies = this.studies; + // _getAndCacheStudy(StudyInstanceUID) { + // const studies = this.studies; - let study = studies.get(StudyInstanceUID); + // let study = studies.get(StudyInstanceUID); - if (!study) { - study = { series: new Map() }; - studies.set(StudyInstanceUID, study); - } + // if (!study) { + // study = { series: new Map() }; + // studies.set(StudyInstanceUID, study); + // } - return study; - } - _getAndCacheSeriesFromStudy(study, SeriesInstanceUID) { - let series = study.series.get(SeriesInstanceUID); + // return study; + // } + // _getAndCacheSeriesFromStudy(study, SeriesInstanceUID) { + // let series = study.series.get(SeriesInstanceUID); - if (!series) { - series = { instances: new Map() }; - study.series.set(SeriesInstanceUID, series); - } + // if (!series) { + // series = { instances: new Map() }; + // study.series.set(SeriesInstanceUID, series); + // } - return series; - } + // return series; + // } - _getAndCacheInstanceFromStudy(series, SOPInstanceUID) { - let instance = series.instances.get(SOPInstanceUID); + // _getAndCacheInstanceFromStudy(series, SOPInstanceUID) { + // let instance = series.instances.get(SOPInstanceUID); - if (!instance) { - instance = {}; - series.instances.set(SOPInstanceUID, instance); - } + // if (!instance) { + // instance = {}; + // series.instances.set(SOPInstanceUID, instance); + // } - return instance; - } + // return instance; + // } - async _checkBulkDataAndInlineBinaries(instance, server) { - await fetchOverlayData(instance, server); + // async _checkBulkDataAndInlineBinaries(instance, server) { + // await fetchOverlayData(instance, server); - if (instance.PhotometricInterpretation === 'PALETTE COLOR') { - await fetchPaletteColorLookupTableData(instance, server); - } - } + // if (instance.PhotometricInterpretation === 'PALETTE COLOR') { + // await fetchPaletteColorLookupTableData(instance, server); + // } + // } _getInstance(imageId) { const uids = this._getUIDsFromImageID(imageId); diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index 3ee93ec85..26f2884da 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -1,3 +1,5 @@ +import dcmjs from 'dcmjs' + import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; import createStudyMetadata from './createStudyMetadata'; import EVENTS from './EVENTS'; @@ -24,6 +26,10 @@ const _model = { // }] }; +function _getStudyInstanceUIDs() { + return _model.studies.map(aStudy => aStudy.StudyInstanceUID) +} + function _getStudy(StudyInstanceUID) { return _model.studies.find( aStudy => aStudy.StudyInstanceUID === StudyInstanceUID @@ -69,6 +75,41 @@ function _getInstanceFromImageId(imageId) { const BaseImplementation = { EVENTS, listeners: {}, + addInstance(dicomJSONDatasetOrP10ArrayBuffer) { + let dicomJSONDataset; + + // If Arraybuffer, parse to DICOMJSON before naturalizing. + if (dicomJSONDatasetOrP10ArrayBuffer instanceof ArrayBuffer) { + const dicomData = dcmjs.data.DicomMessage.readFile(dicomJSONDatasetOrP10ArrayBuffer); + + dicomJSONDataset = dicomData.dict; + } else { + dicomJSONDataset = dicomJSONDatasetOrP10ArrayBuffer; + } + + let naturalizedDataset; + + if (dicomJSONDataset['SeriesInstanceUID'] === undefined) { + naturalizedDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( + dicomJSONDataset + ); + } else { + naturalizedDataset = dicomJSONDataset; + } + + const { StudyInstanceUID } = naturalizedDataset; + + let study = _model.studies.find( + study => study.StudyInstanceUID === StudyInstanceUID + ); + + if (!study) { + _model.studies.push(createStudyMetadata(StudyInstanceUID)); + study = _model.studies[_model.studies.length - 1]; + } + + study.addInstanceToSeries(naturalizedDataset); + }, addInstances(instances, madeInClient = false) { const { StudyInstanceUID, SeriesInstanceUID } = instances[0]; @@ -134,6 +175,7 @@ const BaseImplementation = { _model.studies.push(newStudy); } }, + getStudyInstanceUIDs: _getStudyInstanceUIDs, getStudy: _getStudy, getSeries: _getSeries, getInstance: _getInstance, @@ -146,8 +188,6 @@ const DicomMetadataStore = Object.assign( pubSubServiceInterface ); -// TODO => Add instances -//_addInstance(input) // arraybuffer, or other stuff export { DicomMetadataStore }; export default DicomMetadataStore; diff --git a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js index 3f00dd3c8..e3657e51e 100644 --- a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js +++ b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js @@ -5,13 +5,31 @@ function createStudyMetadata(StudyInstanceUID) { StudyInstanceUID, isLoaded: false, series: [], + /** + * + * @param {object} instance + * @returns {bool} true if series were added; false if series already exist + */ + addInstanceToSeries: function (instance) { + const { SeriesInstanceUID } = instance; + const existingSeries = this.series.find( + s => s.SeriesInstanceUID === SeriesInstanceUID + ); + + if (existingSeries) { + existingSeries.instances.push(instance); + } else { + const series = createSeriesMetadata([instance]); + this.series.push(series); + } + }, /** * * @param {object[]} instances * @param {string} instances[].SeriesInstanceUID * @returns {bool} true if series were added; false if series already exist */ - addInstancesToSeries: function(instances) { + addInstancesToSeries: function (instances) { const { SeriesInstanceUID } = instances[0]; const existingSeries = this.series.find( s => s.SeriesInstanceUID === SeriesInstanceUID @@ -25,7 +43,7 @@ function createStudyMetadata(StudyInstanceUID) { } }, - setSeriesMetadata: function(SeriesInstanceUID, seriesMetadata) { + setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) { let existingSeries = this.series.find( s => s.SeriesInstanceUID === SeriesInstanceUID ); diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.jsx b/platform/ui/src/contextProviders/ViewportGridProvider.jsx index c072de235..c4707235b 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.jsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.jsx @@ -179,7 +179,7 @@ export function ViewportGridProvider({ children, service }) { ]); const api = { - // getState, + getState, setActiveViewportIndex, setDisplaysetForViewport, setLayout, diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 0db4aead4..47a6238bd 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -68,6 +68,7 @@ "moment": "^2.24.0", "prop-types": "^15.7.2", "query-string": "^6.12.1", + "react-dropzone": "^10.1.7", "react-i18next": "^10.11.0", "react-resize-detector": "^4.2.0", "react-router": "^5.2.0", diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index 62e374551..eb828a5d5 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -24,6 +24,20 @@ window.config = { supportsWildcard: true, }, }, + { + friendlyName: 'dicom json', + namespace: 'org.ohif.default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + name: 'json', + }, + }, + { + friendlyName: 'dicom local', + namespace: 'org.ohif.default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: {}, + }, ], // whiteLabeling: { // /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ diff --git a/platform/viewer/public/html-templates/index.html b/platform/viewer/public/html-templates/index.html index a01ca7aa3..1dce0e233 100644 --- a/platform/viewer/public/html-templates/index.html +++ b/platform/viewer/public/html-templates/index.html @@ -75,7 +75,7 @@ OHIF Viewer - +