feat: Support for local DICOM drag and drop (#2445)

* Added drag and drop from local and local data source

* fix: dicom SR drag and drop from local
This commit is contained in:
Alireza 2021-06-15 10:19:45 -04:00 committed by GitHub
parent 99b8dc4759
commit fdb6ca41d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 656 additions and 89 deletions

View File

@ -43,7 +43,7 @@ function createDicomJSONApi(dicomJsonConfig) {
const { name } = dicomJsonConfig; const { name } = dicomJsonConfig;
const implementation = { const implementation = {
parseRouteParams: async ({ params, query, url }) => { initialize: async ({ params, query, url }) => {
if (!url) url = query.get('url'); if (!url) url = query.get('url');
let metaData = getMetaDataByURL(url); let metaData = getMetaDataByURL(url);
@ -93,11 +93,12 @@ function createDicomJSONApi(dicomJsonConfig) {
}, },
query: { query: {
studies: { studies: {
mapParams: () => {}, mapParams: () => { },
search: async param => { search: async param => {
const [key, value] = Object.entries(param)[0]; const [key, value] = Object.entries(param)[0];
const mappedParam = mappings[key]; const mappedParam = mappings[key];
// todo: should fetch from dicomMetadataStore
const studies = findStudies(mappedParam, value); const studies = findStudies(mappedParam, value);
return studies.map(aStudy => { return studies.map(aStudy => {

View File

@ -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 }

View File

@ -63,7 +63,7 @@ function createDicomWebApi(dicomWebConfig) {
const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig); const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig);
const implementation = { const implementation = {
parseRouteParams: ({ params, query }) => { initialize: ({ params, query }) => {
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs'); const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
@ -78,7 +78,7 @@ function createDicomWebApi(dicomWebConfig) {
query: { query: {
studies: { studies: {
mapParams: mapParams.bind(), mapParams: mapParams.bind(),
search: async function(origParams) { search: async function (origParams) {
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
mapParams(origParams, { mapParams(origParams, {
supportsFuzzyMatching, supportsFuzzyMatching,
@ -98,7 +98,7 @@ function createDicomWebApi(dicomWebConfig) {
}, },
series: { series: {
// mapParams: mapParams.bind(), // mapParams: mapParams.bind(),
search: async function(studyInstanceUid) { search: async function (studyInstanceUid) {
const results = await seriesInStudy( const results = await seriesInStudy(
qidoDicomWebClient, qidoDicomWebClient,
studyInstanceUid studyInstanceUid

View File

@ -4,6 +4,7 @@
import { createDicomWebApi } from './DicomWebDataSource/index.js'; import { createDicomWebApi } from './DicomWebDataSource/index.js';
import { createDicomJSONApi } from './DicomJSONDataSource/index.js'; import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
import { createDicomLocalApi } from './DicomLocalDataSource/index.js';
/** /**
* *
@ -20,6 +21,11 @@ function getDataSourcesModule() {
type: 'jsonApi', type: 'jsonApi',
createDataSource: createDicomJSONApi, createDataSource: createDicomJSONApi,
}, },
{
name: 'dicomlocal',
type: 'localApi',
createDataSource: createDicomLocalApi,
},
]; ];
} }

View File

@ -17,7 +17,7 @@ function create({
retrieve, retrieve,
store, store,
reject, reject,
parseRouteParams, initialize,
retrieveSeriesMetadata, retrieveSeriesMetadata,
deleteStudyMetadataPromise, deleteStudyMetadataPromise,
getImageIdsForDisplaySet, getImageIdsForDisplaySet,
@ -38,7 +38,7 @@ function create({
* @param {number} params.resultsPerPage * @param {number} params.resultsPerPage
*/ */
mapParams: params => params, mapParams: params => params,
requestResults: () => {}, requestResults: () => { },
processResults: results => results, processResults: results => results,
}, },
series: {}, series: {},
@ -63,7 +63,7 @@ function create({
retrieve: retrieve || defaultRetrieve, retrieve: retrieve || defaultRetrieve,
reject: reject || defaultReject, reject: reject || defaultReject,
store: store || defaultStore, store: store || defaultStore,
parseRouteParams, initialize,
retrieveSeriesMetadata, retrieveSeriesMetadata,
deleteStudyMetadataPromise, deleteStudyMetadataPromise,
getImageIdsForDisplaySet, getImageIdsForDisplaySet,

View File

@ -71,47 +71,47 @@ class MetadataProvider {
this.imageIdToUIDs.set(imageId, uids); this.imageIdToUIDs.set(imageId, uids);
} }
_getAndCacheStudy(StudyInstanceUID) { // _getAndCacheStudy(StudyInstanceUID) {
const studies = this.studies; // const studies = this.studies;
let study = studies.get(StudyInstanceUID); // let study = studies.get(StudyInstanceUID);
if (!study) { // if (!study) {
study = { series: new Map() }; // study = { series: new Map() };
studies.set(StudyInstanceUID, study); // studies.set(StudyInstanceUID, study);
} // }
return study; // return study;
} // }
_getAndCacheSeriesFromStudy(study, SeriesInstanceUID) { // _getAndCacheSeriesFromStudy(study, SeriesInstanceUID) {
let series = study.series.get(SeriesInstanceUID); // let series = study.series.get(SeriesInstanceUID);
if (!series) { // if (!series) {
series = { instances: new Map() }; // series = { instances: new Map() };
study.series.set(SeriesInstanceUID, series); // study.series.set(SeriesInstanceUID, series);
} // }
return series; // return series;
} // }
_getAndCacheInstanceFromStudy(series, SOPInstanceUID) { // _getAndCacheInstanceFromStudy(series, SOPInstanceUID) {
let instance = series.instances.get(SOPInstanceUID); // let instance = series.instances.get(SOPInstanceUID);
if (!instance) { // if (!instance) {
instance = {}; // instance = {};
series.instances.set(SOPInstanceUID, instance); // series.instances.set(SOPInstanceUID, instance);
} // }
return instance; // return instance;
} // }
async _checkBulkDataAndInlineBinaries(instance, server) { // async _checkBulkDataAndInlineBinaries(instance, server) {
await fetchOverlayData(instance, server); // await fetchOverlayData(instance, server);
if (instance.PhotometricInterpretation === 'PALETTE COLOR') { // if (instance.PhotometricInterpretation === 'PALETTE COLOR') {
await fetchPaletteColorLookupTableData(instance, server); // await fetchPaletteColorLookupTableData(instance, server);
} // }
} // }
_getInstance(imageId) { _getInstance(imageId) {
const uids = this._getUIDsFromImageID(imageId); const uids = this._getUIDsFromImageID(imageId);

View File

@ -1,3 +1,5 @@
import dcmjs from 'dcmjs'
import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
import createStudyMetadata from './createStudyMetadata'; import createStudyMetadata from './createStudyMetadata';
import EVENTS from './EVENTS'; import EVENTS from './EVENTS';
@ -24,6 +26,10 @@ const _model = {
// }] // }]
}; };
function _getStudyInstanceUIDs() {
return _model.studies.map(aStudy => aStudy.StudyInstanceUID)
}
function _getStudy(StudyInstanceUID) { function _getStudy(StudyInstanceUID) {
return _model.studies.find( return _model.studies.find(
aStudy => aStudy.StudyInstanceUID === StudyInstanceUID aStudy => aStudy.StudyInstanceUID === StudyInstanceUID
@ -69,6 +75,41 @@ function _getInstanceFromImageId(imageId) {
const BaseImplementation = { const BaseImplementation = {
EVENTS, EVENTS,
listeners: {}, 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) { addInstances(instances, madeInClient = false) {
const { StudyInstanceUID, SeriesInstanceUID } = instances[0]; const { StudyInstanceUID, SeriesInstanceUID } = instances[0];
@ -134,6 +175,7 @@ const BaseImplementation = {
_model.studies.push(newStudy); _model.studies.push(newStudy);
} }
}, },
getStudyInstanceUIDs: _getStudyInstanceUIDs,
getStudy: _getStudy, getStudy: _getStudy,
getSeries: _getSeries, getSeries: _getSeries,
getInstance: _getInstance, getInstance: _getInstance,
@ -146,8 +188,6 @@ const DicomMetadataStore = Object.assign(
pubSubServiceInterface pubSubServiceInterface
); );
// TODO => Add instances
//_addInstance(input) // arraybuffer, or other stuff
export { DicomMetadataStore }; export { DicomMetadataStore };
export default DicomMetadataStore; export default DicomMetadataStore;

View File

@ -5,13 +5,31 @@ function createStudyMetadata(StudyInstanceUID) {
StudyInstanceUID, StudyInstanceUID,
isLoaded: false, isLoaded: false,
series: [], 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 {object[]} instances
* @param {string} instances[].SeriesInstanceUID * @param {string} instances[].SeriesInstanceUID
* @returns {bool} true if series were added; false if series already exist * @returns {bool} true if series were added; false if series already exist
*/ */
addInstancesToSeries: function(instances) { addInstancesToSeries: function (instances) {
const { SeriesInstanceUID } = instances[0]; const { SeriesInstanceUID } = instances[0];
const existingSeries = this.series.find( const existingSeries = this.series.find(
s => s.SeriesInstanceUID === SeriesInstanceUID s => s.SeriesInstanceUID === SeriesInstanceUID
@ -25,7 +43,7 @@ function createStudyMetadata(StudyInstanceUID) {
} }
}, },
setSeriesMetadata: function(SeriesInstanceUID, seriesMetadata) { setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) {
let existingSeries = this.series.find( let existingSeries = this.series.find(
s => s.SeriesInstanceUID === SeriesInstanceUID s => s.SeriesInstanceUID === SeriesInstanceUID
); );

View File

@ -179,7 +179,7 @@ export function ViewportGridProvider({ children, service }) {
]); ]);
const api = { const api = {
// getState, getState,
setActiveViewportIndex, setActiveViewportIndex,
setDisplaysetForViewport, setDisplaysetForViewport,
setLayout, setLayout,

View File

@ -68,6 +68,7 @@
"moment": "^2.24.0", "moment": "^2.24.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"query-string": "^6.12.1", "query-string": "^6.12.1",
"react-dropzone": "^10.1.7",
"react-i18next": "^10.11.0", "react-i18next": "^10.11.0",
"react-resize-detector": "^4.2.0", "react-resize-detector": "^4.2.0",
"react-router": "^5.2.0", "react-router": "^5.2.0",

View File

@ -24,6 +24,20 @@ window.config = {
supportsWildcard: true, 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: { // whiteLabeling: {
// /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ // /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */

View File

@ -75,7 +75,7 @@
<title>OHIF Viewer</title> <title>OHIF Viewer</title>
<!-- WEB FONTS --> <!-- WEB FONTS -->
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css?family=Lato:100,300,400,500,700&display=swap" rel="stylesheet" />
<!-- EXTENSIONS --> <!-- EXTENSIONS -->
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script> <!-- <script type="text/javascript" src="path/to/some-extension.js"></script>

View File

@ -1,7 +1,7 @@
/** /**
* CSS Grid Reference: http://grid.malven.co/ * CSS Grid Reference: http://grid.malven.co/
*/ */
import React, { useEffect } from 'react'; import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
import EmptyViewport from './EmptyViewport'; import EmptyViewport from './EmptyViewport';
@ -27,53 +27,69 @@ function ViewerViewportGrid(props) {
} = servicesManager.services; } = servicesManager.services;
// Using Hanging protocol engine to match the displaysets const updateDisplaysetForViewports = useCallback(
(displaySets) => {
const [
matchDetails,
hpAlreadyApplied,
] = HangingProtocolService.getState();
if (!matchDetails.length) return;
// Match each viewport individually
const numViewports = viewportGrid.numRows * viewportGrid.numCols;
for (let i = 0; i < numViewports; i++) {
if (hpAlreadyApplied[i] === true) {
continue;
}
// if current viewport doesn't have a match
if (matchDetails[i] === undefined) return
const { SeriesInstanceUID } = matchDetails[i];
const matchingDisplaySet = displaySets.find(ds => {
return ds.SeriesInstanceUID === SeriesInstanceUID;
});
if (!matchingDisplaySet) {
continue;
}
viewportGridService.setDisplaysetForViewport({
viewportIndex: i,
displaySetInstanceUID: matchingDisplaySet.displaySetInstanceUID,
});
HangingProtocolService.setHangingProtocolAppliedForViewport(i);
}
},
[viewportGrid, numRows, numCols],
)
// Using Hanging protocol engine to match the displaySets
useEffect(() => { useEffect(() => {
const { unsubscribe } = DisplaySetService.subscribe( const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
eventData => { eventData => {
const { displaySetsAdded } = eventData; const { displaySetsAdded } = eventData;
updateDisplaysetForViewports(displaySetsAdded)
const [
matchDetails,
hpAlreadyApplied,
] = HangingProtocolService.getState();
if (!matchDetails.length) return;
// Match each viewport individually
const numViewports = viewportGrid.numRows * viewportGrid.numCols;
for (let i = 0; i < numViewports; i++) {
if (hpAlreadyApplied[i] === true) {
continue;
}
// if current viewport doesn't have a match
if (matchDetails[i] === undefined) return
const { SeriesInstanceUID } = matchDetails[i];
const matchingDisplaySet = displaySetsAdded.find(ds => {
return ds.SeriesInstanceUID === SeriesInstanceUID;
});
if (!matchingDisplaySet) {
continue;
}
viewportGridService.setDisplaysetForViewport({
viewportIndex: i,
displaySetInstanceUID: matchingDisplaySet.displaySetInstanceUID,
});
HangingProtocolService.setHangingProtocolAppliedForViewport(i);
}
} }
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [numRows, numCols]); }, [viewportGrid]);
// Changing the Hanging protocol while viewing
useEffect(() => {
const displaySets = DisplaySetService.getActiveDisplaySets();
updateDisplaysetForViewports(displaySets)
}, [viewportGrid])
// Layout change based on hanging protocols // Layout change based on hanging protocols

View File

@ -0,0 +1,116 @@
import React, { useEffect, useRef } from 'react'
import classnames from 'classnames'
import { MODULE_TYPES } from '@ohif/core'
import PropTypes from 'prop-types'
import Dropzone from 'react-dropzone'
import filesToStudies from './filesToStudies'
import { extensionManager } from '../../App.jsx'
import { Icon, Button } from '@ohif/ui'
const getLoadButton = (onDrop, text, isDir) => {
return (
<Dropzone onDrop={onDrop} noDrag>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<Button
rounded="full"
variant="contained" // outlined
disabled={false}
endIcon={<Icon name="launch-arrow" />} // launch-arrow | launch-info
className={classnames('font-bold', 'ml-2')}
onClick={() => { }}
>
{text}
{isDir ? (
<input
{...getInputProps()}
webkitdirectory="true"
mozdirectory="true"
/>
) : (
<input {...getInputProps()} />
)}
</Button>
</div>
)}
</Dropzone>)
}
function Local(props) {
const { history } = props
const dropzoneRef = useRef()
// Initializing the dicom local dataSource
const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE]
const localDataSources = dataSourceModules.reduce((acc, curr) => {
const mods = []
curr.module.forEach((mod) => {
if (mod.type === 'localApi') {
mods.push(mod)
}
})
return acc.concat(mods)
}, [])
const fistLocalDataSource = localDataSources[0]
const dataSource = fistLocalDataSource.createDataSource({})
const onDrop = async (acceptedFiles) => {
const studies = await filesToStudies(acceptedFiles, dataSource)
// Todo: navigate to work list and let user select a mode
history.push(`/viewer/dicomlocal?StudyInstanceUIDs=${studies[0]}`)
}
// Set body style
useEffect(() => {
document.body.classList.add('bg-black')
return () => {
document.body.classList.remove('bg-black')
}
}, [])
return (
<Dropzone ref={dropzoneRef} onDrop={onDrop} noClick>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
<div className="h-screen w-screen flex justify-center items-center ">
<div className="py-8 px-8 mx-auto bg-secondary-dark shadow-md space-y-2 rounded-lg">
<img
className="block mx-auto h-10"
src="./customLogo.svg"
alt="OHIF"
/>
<div className="text-center space-y-2 pt-4">
<div className="space-y-2">
<p className="text-blue-300 text-base">
Note: You data is not uploaded to any server, it will stay
in your local browser application
</p>
<p className="text-xg text-primary-active font-semibold pt-8">
Drag and Drop DICOM files here to load them in the Viewer
</p>
<p className="text-blue-300 text-lg">Or click to </p>
</div>
</div>
<div className="flex justify-around pt-4">
{getLoadButton(onDrop, 'Load files', false)}
{getLoadButton(onDrop, 'Load folders', true)}
</div>
</div>
</div>
</div>
)}
</Dropzone>
)
}
Local.propTypes = {
history: PropTypes.shape({
push: PropTypes.func,
}).isRequired,
};
export default Local

View File

@ -0,0 +1,29 @@
import dcmjs from 'dcmjs';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import FileLoader from './fileLoader';
const DICOMFileLoader = new (class extends FileLoader {
fileType = 'application/dicom';
loadFile(file, imageId) {
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
}
getDataset(image, imageId) {
const dicomData = dcmjs.data.DicomMessage.readFile(image);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
dataset.url = imageId
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
dicomData.meta
);
return dataset
}
})();
export default DICOMFileLoader;

View File

@ -0,0 +1,6 @@
export default class FileLoader {
fileType;
loadFile(file, imageId) { }
getDataset(image, imageId) { }
getStudies(dataset, imageId) { }
}

View File

@ -0,0 +1,40 @@
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import { DicomMetadataStore } from '@ohif/core';
import FileLoader from './fileLoader';
import PDFFileLoader from './pdfFileLoader';
import DICOMFileLoader from './dicomFileLoader';
class FileLoaderService extends FileLoader {
fileType;
loader;
constructor(file) {
super();
const fileType = file && file.type;
this.loader = this.getLoader(fileType);
this.fileType = this.loader.fileType;
}
addFile(file) {
return cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
}
loadFile(file, imageId) {
return this.loader.loadFile(file, imageId);
}
getDataset(image, imageId) {
return this.loader.getDataset(image, imageId);
}
getLoader(fileType) {
if (fileType === 'application/pdf') {
return PDFFileLoader;
} else {
// Default to dicom loader
return DICOMFileLoader;
}
}
}
export default FileLoaderService;

View File

@ -0,0 +1,26 @@
import FileLoaderService from './fileLoaderService';
import { DicomMetadataStore } from '@ohif/core';
const processFile = async file => {
try {
const fileLoaderService = new FileLoaderService(file);
const imageId = fileLoaderService.addFile(file);
const image = await fileLoaderService.loadFile(file, imageId);
const dicomJSONDataset = await fileLoaderService.getDataset(image, imageId);
DicomMetadataStore.addInstance(dicomJSONDataset);
} catch (error) {
console.log(
error.name,
':Error when trying to load and process local files:',
error.message
);
}
};
export default async function filesToStudies(files) {
const processFilesPromises = files.map(processFile);
await Promise.all(processFilesPromises);
return DicomMetadataStore.getStudyInstanceUIDs();
}

View File

@ -0,0 +1 @@
export { default } from './Local';

View File

@ -0,0 +1,17 @@
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import FileLoader from './fileLoader';
const PDFFileLoader = new (class extends FileLoader {
fileType = 'application/pdf';
loadFile(file, imageId) {
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
}
getDataset(image, imageId) {
const dataset = {};
dataset.imageId = image.imageId || imageId;
return dataset;
}
})();
export default PDFFileLoader;

View File

@ -19,7 +19,7 @@ async function defaultRouteInit({
} = servicesManager.services; } = servicesManager.services;
const unsubscriptions = []; const unsubscriptions = [];
// TODO: This should be baked into core, not manuall? // TODO: This should be baked into core, not manual?
// DisplaySetService would wire this up? // DisplaySetService would wire this up?
const { const {
unsubscribe: instanceAddedUnsubscribe, unsubscribe: instanceAddedUnsubscribe,
@ -37,10 +37,6 @@ async function defaultRouteInit({
unsubscriptions.push(instanceAddedUnsubscribe); unsubscriptions.push(instanceAddedUnsubscribe);
studyInstanceUIDs.forEach(StudyInstanceUID => {
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
});
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe( const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED, DicomMetadataStore.EVENTS.SERIES_ADDED,
({ StudyInstanceUID }) => { ({ StudyInstanceUID }) => {
@ -50,6 +46,10 @@ async function defaultRouteInit({
); );
unsubscriptions.push(seriesAddedUnsubscribe); unsubscriptions.push(seriesAddedUnsubscribe);
studyInstanceUIDs.forEach(StudyInstanceUID => {
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
});
return unsubscriptions; return unsubscriptions;
} }
@ -132,7 +132,7 @@ export default function ModeRoute({
useEffect(() => { useEffect(() => {
// Todo: this should not be here, data source should not care about params // Todo: this should not be here, data source should not care about params
const initializeDataSource = async (params, query) => { const initializeDataSource = async (params, query) => {
const studyInstanceUIDs = await dataSource.parseRouteParams({ const studyInstanceUIDs = await dataSource.initialize({
params, params,
query, query,
}); });
@ -261,7 +261,7 @@ export default function ModeRoute({
<ImageViewerProvider <ImageViewerProvider
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }} // initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
StudyInstanceUIDs={studyInstanceUIDs} StudyInstanceUIDs={studyInstanceUIDs}
// reducer={reducer} // reducer={reducer}
> >
<CombinedContextProvider> <CombinedContextProvider>
<DragAndDropProvider> <DragAndDropProvider>

View File

@ -3,6 +3,7 @@ import { Switch, Route } from 'react-router-dom';
// Route Components // Route Components
import DataSourceWrapper from './DataSourceWrapper'; import DataSourceWrapper from './DataSourceWrapper';
import WorkList from './WorkList'; import WorkList from './WorkList';
import Local from './Local';
import NotFound from './NotFound'; import NotFound from './NotFound';
import buildModeRoutes from './buildModeRoutes'; import buildModeRoutes from './buildModeRoutes';
import { ErrorBoundary } from '@ohif/ui'; import { ErrorBoundary } from '@ohif/ui';
@ -17,6 +18,11 @@ const bakedInRoutes = [
component: DataSourceWrapper, component: DataSourceWrapper,
props: { children: WorkList }, props: { children: WorkList },
}, },
{
path: '/local',
exact: true,
component: Local,
},
// NOT FOUND (404) // NOT FOUND (404)
{ component: NotFound }, { component: NotFound },
]; ];

View File

@ -4333,6 +4333,11 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^2.0.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
autoprefixer@10.2.4: autoprefixer@10.2.4:
version "10.2.4" version "10.2.4"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.4.tgz#c0e7cf24fcc6a1ae5d6250c623f0cb8beef2f7e1" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.4.tgz#c0e7cf24fcc6a1ae5d6250c623f0cb8beef2f7e1"
@ -8955,6 +8960,13 @@ file-loader@^1.1.11:
loader-utils "^1.0.2" loader-utils "^1.0.2"
schema-utils "^0.4.5" schema-utils "^0.4.5"
file-selector@^0.1.12:
version "0.1.19"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.19.tgz#8ecc9d069a6f544f2e4a096b64a8052e70ec8abf"
integrity sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==
dependencies:
tslib "^2.0.1"
file-type@^16.0.0: file-type@^16.0.0:
version "16.2.0" version "16.2.0"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.2.0.tgz#d4f1da71ddda758db7f15f93adfaed09ce9e2715" resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.2.0.tgz#d4f1da71ddda758db7f15f93adfaed09ce9e2715"
@ -16999,6 +17011,15 @@ react-draggable@4.4.3, react-draggable@^4.1.0:
classnames "^2.2.5" classnames "^2.2.5"
prop-types "^15.6.0" prop-types "^15.6.0"
react-dropzone@^10.1.7:
version "10.2.2"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.2.2.tgz#67b4db7459589a42c3b891a82eaf9ade7650b815"
integrity sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==
dependencies:
attr-accept "^2.0.0"
file-selector "^0.1.12"
prop-types "^15.7.2"
react-error-boundary@2.2.x: react-error-boundary@2.2.x:
version "2.2.3" version "2.2.3"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-2.2.3.tgz#34c8238012d3b4148cec47a1b3cec669d5206578" resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-2.2.3.tgz#34c8238012d3b4148cec47a1b3cec669d5206578"
@ -20234,6 +20255,11 @@ tslib@^2, tslib@^2.0.0, tslib@^2.0.3, tslib@~2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tslib@^2.0.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
tslib@~2.0.1: tslib@~2.0.1:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"