Skip to content

Commit

Permalink
feat: add QIDO-RS of study level
Browse files Browse the repository at this point in the history
- `QIDO-RS.service.js` convert the request query into MongoDB query
- Fix the response content-type of `STOW-RS`
> application/dicom+json
- Add `studies.yaml` for API documentation of `QIDO-RS`
- Add date handler for MongoDB query
  • Loading branch information
Chinlinlee committed Apr 29, 2022
1 parent 2a5f1e5 commit 99a6283
Show file tree
Hide file tree
Showing 7 changed files with 490 additions and 4 deletions.
150 changes: 150 additions & 0 deletions api/dicom-web/controller/QIDO-RS/queryAllStudies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
const _ = require('lodash');
const mongoose = require('mongoose');
const moment = require('moment');
const {
convertAllQueryToDICOMTag,
convertRequestQueryToMongoQuery,
getStudyLevelFields
} = require('./service/QIDO-RS.service');
const {
logger
} = require('../../../../utils/log');

/**
* @openapi
* /dicom-web/studies:
* get:
* description: Query for studies
* parameters:
* - $ref: "#/components/parameters/StudyDate"
* - $ref: "#/components/parameters/StudyTime"
* - $ref: "#/components/parameters/AccessionNumber"
* - $ref: "#/components/parameters/ModalitiesInStudy"
* - $ref: "#/components/parameters/ReferringPhysicianName"
* - $ref: "#/components/parameters/PatientName"
* - $ref: "#/components/parameters/PatientID"
* - $ref: "#/components/parameters/StudyID"
* responses:
* 200:
* description: Query successfully
*/

/**
*
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
*/
module.exports = async function (req, res) {
try {
let limit = req.query.limit || 100 ;
let skip = req.query.offset || 0;
delete req.query["limit"];
delete req.query["offset"];
let query = _.cloneDeep(req.query);
let queryKeys = Object.keys(query).sort();
for ( let i = 0 ; i < queryKeys.length ; i++) {
let queryKey = queryKeys[i];
if (!query[queryKey]) delete query[queryKey];
}

let dicomTagQuery = convertAllQueryToDICOMTag(query);
let studiesJson = await getStudyDicomJson(dicomTagQuery, limit, skip);
res.writeHead(200, {
"Content-Type": "application/dicom+json"
});
res.end(JSON.stringify(studiesJson.data));
} catch(e) {
let errorStr = JSON.stringify(e, Object.getOwnPropertyNames(e));
logger.error(`[QIDO-RS] [Error: ${errorStr}]`);
}
}

async function getStudyDicomJson(iQuery , limit , skip) {
logger.info(`[QIDO-RS] [Query Study Level]`);
let result = {
data : '' ,
status: false
}
try {
iQuery = await convertRequestQueryToMongoQuery(iQuery);
// iQuery = iQuery.$match;
logger.info(`[QIDO-RS] [Query for MongoDB: ${JSON.stringify(iQuery)}]`);
let studyFields = getStudyLevelFields();
let aggregateQuery = [
{
$sort: {
studyUID: 1
}
},
iQuery,
{
$limit: limit + skip
},
{
$skip: skip
},
{
$group: {
_id: "$0020000D",
modalitiesInStudy: {
$addToSet: "$00080060.Value"
},
dicomJson: {
$addToSet: "$$ROOT"
}
}
},
{
$project: {
...studyFields,
"dicomJson.00080061": {
"vr": "CS",
"Value": {
$reduce: {
input: "$modalitiesInStudy",
initialValue: [],
in: {
$concatArrays : ["$$value", "$$this"]
}
}
}
}
}
},
{
$project: {
dicomJsonObj: {
$mergeObjects: "$$ROOT.dicomJson"
}
}
},
{
$replaceWith: "$dicomJsonObj"
},
{
$sort: {
"0020000D": 1
}
}
];
let docs = await mongoose.model("dicom").aggregate(aggregateQuery).exec();
result.data = docs.map(v => {
let studyDate = _.get(v , "00080020.Value");
if (studyDate) {
for (let j in studyDate) {
let studyDateYYYYMMDD = moment(studyDate[j]).format("YYYYMMDD").toString();
studyDate[j] = studyDateYYYYMMDD;
}
_.set(v , "00080020.Value" , studyDate);
}
return v;
});
result.status = true;
return result;
} catch (e) {
console.error("get Study DICOM error" , e);
result.data = e;
result.status = false;
return result;
}
}
162 changes: 162 additions & 0 deletions api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
const _ = require('lodash');
const {
mongoDateQuery
} = require('../../../../../models/mongodb/service');
const {
dictionary
} = require('../../../../../models/DICOM/dicom-tags-dic');
const {
tagsOfRequiredMatching
} = require('../../../../../models/DICOM/dicom-tags-mapping');

/**
* Convert All of name(tags, keyword) of queries to tags number
* @param {Object} iParam The request query.
* @returns
*/
function convertAllQueryToDICOMTag(iParam) {
let keys = Object.keys(iParam);
let newQS = {};
for (let i = 0; i < keys.length; i++) {
let keyName = keys[i];
let keyNameSplit = keyName.split('.');
let newKeyNames = [];
for (let x = 0; x < keyNameSplit.length; x++) {
if (dictionary.keyword[keyNameSplit[x]]) {
newKeyNames.push(dictionary.dicom[keyNameSplit[x]]);
} else if (dictionary.tag[keyNameSplit[x]]) {
newKeyNames.push(keyNameSplit);
} else {
//newKeyNames.push(keyNameSplit);
}
}
// if (newKeyNames.length == 1) {
// continue;
// }
// let studyTags = Object.keys(QIDORetAtt.study);
// let seriesTags = Object.keys(QIDORetAtt.series);
// let instanceTags = Object.keys(QIDORetAtt.instance);
// for (let seriesTag of seriesTags) {
// if (newKeyNames.find(v => v == seriesTag) && !studyTags.includes(seriesTag)) {
// newKeyNames = [ "series", ...newKeyNames]
// }
// }
// for (let instanceTag of instanceTags) {
// if (newKeyNames.find(v => v == instanceTag) && !studyTags.includes(instanceTag) && !seriesTags.includes(instanceTag)) {
// newKeyNames = [ "series", "instance", ...newKeyNames]
// }
// }
newKeyNames.push('Value');
let retKeyName = newKeyNames.join('.');
newQS[retKeyName] = iParam[keyName];
}
return (newQS);
}
//#endregion


function checkIsOr(value, keyName) {
if (_.isObject(value) && _.get(value[keyName], "$or")) {
return true;
}
return false;
}

/**
* convert value that contains comma to $or query of MongoDB
* @param {string} iKey
* @param {string} iValue
*/
function commaValue(iKey, iValue) {
let $or = [];
iValue = iValue.split(',');
for (let i = 0; i < iValue.length; i++) {
let obj = {};
obj[iKey] = iValue[i];
$or.push(obj);
}
return $or;
}

async function wildCardFirst(iValue) {
return new Promise((resolve) => {
iValue = iValue.replace(/\*/gi, '.*');
return resolve(new RegExp(iValue, 'gi'));
});
}
async function wildCard(iValue) {
return new Promise((resolve) => {
iValue = '^' + iValue;
iValue = iValue.replace(/\*/gi, '.*');
return resolve(new RegExp(iValue, 'gi'));
});
}

/**
* convert all request query object to to $or query and push to $and query
* @param {Object} iQuery
* @returns
*/
async function convertRequestQueryToMongoQuery(iQuery) {
let queryKey = Object.keys(iQuery);
let mongoQs = {
"$match": {
"$and": []
}
};
for (let i = 0; i < queryKey.length; i++) {
let mongoOrs = {
"$or": []
}
let nowKey = queryKey[i];
let value = commaValue(nowKey, iQuery[nowKey]);
for (let x = 0; x < value.length; x++) {
let nowValue = value[x][nowKey];
let wildCardFunc = {};
wildCardFunc[nowValue.indexOf('*')] = wildCard;
wildCardFunc['0'] = wildCardFirst;
wildCardFunc['-1'] = (value) => {
return value;
}
value[x][nowKey] = await wildCardFunc[nowValue.indexOf('*')](nowValue);

try {
let keySplit = nowKey.split(".");
let tag = keySplit[ keySplit.length - 2 ];
let vrOfTag = dictionary.tagVR[tag];
await vrQueryLookup[vrOfTag.vr](value[x], nowKey);
} catch(e) {
if (!e instanceof TypeError)
console.error(e);
}

if (checkIsOr(value[x], nowKey)) {
mongoOrs.$or.push(...(_.get(value[x][nowKey], "$or")));
} else {
mongoOrs.$or.push(value[x]);
}
}
mongoQs.$match.$and.push(mongoOrs);
}
return (mongoQs.$match.$and.length == 0 ? {
$match: {}
} : mongoQs);
}

function getStudyLevelFields() {
let fields = {};
for (let tag in tagsOfRequiredMatching.Study) {
fields[`dicomJson.${tag}`] = 1;
}
return fields;
}

const vrQueryLookup = {
"DA": async(value, tag) => {
let q = await mongoDateQuery(value, tag, false);
}
}

module.exports.convertAllQueryToDICOMTag = convertAllQueryToDICOMTag;
module.exports.convertRequestQueryToMongoQuery = convertRequestQueryToMongoQuery;
module.exports.getStudyLevelFields = getStudyLevelFields;
4 changes: 1 addition & 3 deletions api/dicom-web/controller/STOW-RS/storeInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ module.exports = async function(req, res) {
logger.error(`[STOW-RS] [${errorStr}]`);
let errorMessage = errorResponseMessage.getInternalServerErrorMessage(errorStr);
res.writeHead(500, {
"Content-Type": "application/dicom-json"
"Content-Type": "application/dicom+json"
});
return res.end(JSON.stringify(errorMessage));
}
Expand Down Expand Up @@ -283,8 +283,6 @@ async function processBinaryData(req, removedTagsDicomJson, uidObj) {
binaryKeys.push(key.substring(0, key.lastIndexOf(".vr")));
}
}
let port = process.env.DICOMWEB_PORT || "";
port = (port) ? `:${port}` : "";
for (let key of binaryKeys) {
let studyUID = uidObj.studyUID;
let seriesUID = uidObj.seriesUID;
Expand Down
10 changes: 10 additions & 0 deletions api/dicom-web/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
const { app } = require('../../app');

//#region QIDO-RS

app.get('/studies' , require('./controller/QIDO-RS/queryAllStudies'));

//#endregion

//#region STOW-RS

app.post("/studies", require("./controller/STOW-RS/storeInstance"));

//#endregion

module.exports = app;
Loading

0 comments on commit 99a6283

Please sign in to comment.