Beginnings of a DICOMWeb API implementation that adheres to IWebApi interface

This commit is contained in:
dannyrb 2020-05-07 14:04:07 -04:00 committed by James A. Petts
parent 91a3c93985
commit f172f26dee
3 changed files with 279 additions and 0 deletions

View File

@ -0,0 +1,54 @@
import { api } from 'dicomweb-client';
import { mapParams, search, processResults } from './qido.js';
import IWebApi from './IWebApi.js';
/**
*
* @param {string} name - Data source name
* @param {string} wadoUriRoot - Legacy?
* @param {string} qidoRoot - Base URL to use for QIDO requests
* @param {string} wadoRoot - Base URL to use for WADO requests
* @param {boolean} qidoSupportsIncludeField - Whether QIDO supports the "Include" option to request additional fields in response
* @param {string} imageRengering - wadors | ? (unsure of where/how this is used)
* @param {string} thumbnailRendering - wadors | ? (unsure of where/how this is used)
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
*/
function createDicomWebApi(dicomWebConfig) {
const { qidoRoot } = dicomWebConfig;
const config = {
url: qidoRoot,
// headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWebClient = new api.DICOMwebClient(config);
return IWebApi({
query: {
studies: {
mapParams: mapParams.bind(),
search: search.bind(undefined, dicomWebClient, null, null),
processResults: processResults.bind(),
},
instances: {
search: (studyInstanceUid, queryParamaters) =>
search.call(
undefined,
dicomWebClient,
studyInstanceUid,
null,
queryParamaters
),
},
},
retrieveMetadata: (dicomMetadataStore, queryParamaters) => {
// TODO -> Write implementation
// WADO-RS retrieve study metadata.
// Then different WADO-RS retrieve series metadata calls.
// Then dicomMetadataStore.addInstance(instance)
// dicomMetadataStore naturalizes the instance and adds it to the store.
// dicomMetadataStore emits an even that the SopClassHandlerManager can listen to.
},
});
}
export { createDicomWebApi };

View File

@ -0,0 +1,178 @@
/**
* QIDO - Query based on ID for DICOM Objects
* search for studies, series and instances by patient ID, and receive their
* unique identifiers for further usage.
*
* Quick: https://www.dicomstandard.org/dicomweb/query-qido-rs/
* Standard: http://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6
*
* Routes:
* ==========
* /studies?
* /studies/{studyInstanceUid}/series?
* /studies/{studyInstanceUid}/series/{seriesInstanceUid}/instances?
*
* Query Parameters:
* ================
* | KEY | VALUE |
* |------------------|--------------------|
* | {attributeId} | {value} |
* | includeField | {attribute} or all |
* | fuzzymatching | true OR false |
* | limit | {number} |
* | offset | {number} |
*/
import { getString, getName, getModalities } from '../../../DICOMWeb/';
/**
* Parses resulting data from a QIDO call into a set of Study MetaData
*
* @param {Array} qidoStudies - An array of study objects. Each object contains a keys for DICOM tags.
* @param {object} qidoStudies[0].qidoStudy - An object where each key is the DICOM Tag group+element
* @param {object} qidoStudies[0].qidoStudy[dicomTag] - Optional object that represents DICOM Tag
* @param {string} qidoStudies[0].qidoStudy[dicomTag].vr - Value Representation
* @param {string[]} qidoStudies[0].qidoStudy[dicomTag].Value - Optional string array representation of the DICOM Tag's value
* @returns {Array} An array of Study MetaData objects
*/
function processResults(qidoStudies) {
if (!qidoStudies || !qidoStudies.length) {
return [];
}
const studies = [];
qidoStudies.forEach(qidoStudy =>
studies.push({
studyInstanceUid: getString(qidoStudy['0020000D']),
studyDate: getString(qidoStudy['00080020']),
studyTime: getString(qidoStudy['00080030']),
accessionNumber: getString(qidoStudy['00080050']),
referringPhysicianName: getString(qidoStudy['00080090']),
patientName: getName(qidoStudy['00100010']),
patientId: getString(qidoStudy['00100020']),
patientBirthdate: getString(qidoStudy['00100030']),
patientSex: getString(qidoStudy['00100040']),
studyId: getString(qidoStudy['00200010']),
numberOfStudyRelatedSeries: getString(qidoStudy['00201206']),
numberOfStudyRelatedInstances: getString(qidoStudy['00201208']),
studyDescription: getString(qidoStudy['00081030']),
modalities: getString(
getModalities(qidoStudy['00080060'], qidoStudy['00080061'])
),
})
);
return studies;
}
/**
*
* @param {object} dicomWebClient - Client similar to what's provided by `dicomweb-client` library
* @param {function} dicomWebClient.searchForStudies -
* @param {string} [studyInstanceUid]
* @param {string} [seriesInstanceUid]
* @param {string} [queryParamaters]
* @returns {Promise<results>} - Promise that resolves results
*/
function search(
dicomWebClient,
studyInstanceUid,
seriesInstanceUid,
queryParamaters
) {
const requestFn = studyInstanceUid
? dicomWebClient.searchForInstances
: dicomWebClient.searchForStudies;
// TODO: Current version does not apply query Params for `searchForInstances` call?
// Just sets `studyInstanceUid` in options...
return requestFn({
studyInstanceUid,
queryParams: queryParamaters,
});
}
export default function searchStudies(server, filter) {
const queryParams = getQIDOQueryParams(
filter,
server.qidoSupportsIncludeField
);
const options = {
queryParams,
};
return dicomWeb.searchForStudies(options).then(resultDataToStudies);
}
/**
* Produces a QIDO URL given server details and a set of specified search filter
* items
*
* @param filter
* @param serverSupportsQIDOIncludeField
* @returns {string} The URL with encoded filter query data
*/
function mapParams(params) {
const commaSeparatedFields = [
'00081030', // Study Description
'00080060', // Modality
// Add more fields here if you want them in the result
].join(',');
const parameters = {
PatientName: filter.patientName,
PatientID: filter.patientId,
AccessionNumber: filter.accessionNumber,
StudyDescription: filter.studyDescription,
ModalitiesInStudy: filter.modalitiesInStudy,
limit: filter.limit,
offset: filter.offset,
fuzzymatching: filter.fuzzymatching,
includefield: serverSupportsQIDOIncludeField ? commaSeparatedFields : 'all',
};
// build the StudyDate range parameter
if (filter.studyDateFrom || filter.studyDateTo) {
const dateFrom = _dateToString(new Date(filter.studyDateFrom));
const dateTo = _dateToString(new Date(filter.studyDateTo));
parameters.StudyDate = `${dateFrom}-${dateTo}`;
}
// Build the StudyInstanceUID parameter
if (filter.studyInstanceUid) {
let studyUids = filter.studyInstanceUid;
studyUids = Array.isArray(studyUids) ? studyUids.join() : studyUids;
studyUids = studyUids.replace(/[^0-9.]+/g, '\\');
parameters.StudyInstanceUID = studyUids;
}
// Clean query params of undefined values.
const params = {};
Object.keys(parameters).forEach(key => {
if (parameters[key] !== undefined && parameters[key] !== '') {
params[key] = parameters[key];
}
});
return params;
}
/**
* Creates a QIDO date string for a date range query
* Assumes the year is positive, at most 4 digits long.
*
* @param date The Date object to be formatted
* @returns {string} The formatted date string
*/
function _dateToString(date) {
if (!date) return '';
let year = date.getFullYear().toString();
let month = (date.getMonth() + 1).toString();
let day = date.getDate().toString();
year = '0'.repeat(4 - year.length).concat(year);
month = '0'.repeat(2 - month.length).concat(month);
day = '0'.repeat(2 - day.length).concat(day);
return ''.concat(year, month, day);
}
export { mapParams, search, processResults };

View File

@ -0,0 +1,47 @@
import {dicomMetadataStore} from '@ohif/core';
/**
* Factory function that creates a new "Web API" data source.
* A "Web API" data source is any source that fetches data over
* HTTP. This function serves as an "adapter" to wrap those calls
* so that all "Web API" data sources have the same interface and can
* be used interchangeably.
*
* It's worth noting that a single implementation of this interface
* can define different underlying sources for "read" and "write" operations.
*/
function createWebApiDataSource({ query, retrieve, retrieveMetadata }) {
const defaultQuery = {
studies: {
/**
* @param {string} params.patientName
* @param {string} params.mrn
* @param {object} params.studyDate
* @param {string} params.description
* @param {string} params.modality
* @param {string} params.accession
* @param {string} params.sortBy
* @param {string} params.sortDirection -
* @param {number} params.page
* @param {number} params.resultsPerPage
*/
mapParams: params => params,
requestResults: () => {},
processResults: results => results,
},
series: {},
instances: {},
};
const defaultRetrieve = {};
return {
query,
retrieve: {},
retrieveMetadata: retrieveMetadata.bind(null, dicomMetadataStore);
// then go get all series level metadata.
// Store this in the DICOM MetadataStore.
};
}
export { createWebApiDataSource };