Skip to content

Commit

Permalink
feat: add Modality Worklist query support for C-FIND
Browse files Browse the repository at this point in the history
Introduce support for Modality Worklist (MWL) query in C-FIND to enable
retrieval of worklist items. This change adds a new method to the
JsCFindScp class, allowing for the retrieval of MWL data. This
enhancement addresses the need for MWL query capability when performing
DICOM C-FIND operations.
  • Loading branch information
Chinlinlee committed Dec 29, 2023
1 parent ec8e658 commit 4965b62
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 3 deletions.
21 changes: 21 additions & 0 deletions dimse/c-find.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const { JsStudyQueryTask } = require("@dimse-study-query-task");
const { JsSeriesQueryTask } = require("@dimse-series-query-task");
const { JsInstanceQueryTask } = require("@dimse-instance-query-task");
const { PATIENT_ROOT_LEVELS, STUDY_ROOT_LEVELS, PATIENT_STUDY_ONLY_LEVELS } = require("./level");
const { Tag } = require("@dcm4che/data/Tag");
const { JsMwlQueryTask } = require("./mwlQueryTask");

class JsCFindScp {
constructor() { }
Expand Down Expand Up @@ -59,6 +61,20 @@ class JsCFindScp {
return basicModCFindSCP;
}

getMwlLevel() {
const cFindScpInject = createCFindSCPInjectProxy(this.getCFindScpInjectProxyMethods(), {
keepAsDaemon: true
});

let basicModCFindSCP = new BasicModCFindSCP(
cFindScpInject,
[UID.ModalityWorklistInformationModelFind]
);

this.scpObj = basicModCFindSCP;
return basicModCFindSCP;
}

getCFindScpInjectProxyMethods() {
/**
* @type { import("@java-wrapper/org/github/chinlinlee/dcm777/net/CFindSCPInject").CFindSCPInjectInterface }
Expand All @@ -83,6 +99,11 @@ class JsCFindScp {
},
calculateMatches: async (as, pc, rq, keys) => {
try {
let requestSopClassUID = await rq.getString(Tag.AffectedSOPClassUID);
if (requestSopClassUID === UID.ModalityWorklistInformationModelFind ) {
return await (new JsMwlQueryTask(as, pc, rq, keys)).get();
}

let level = await this.scpObj.getQrLevel(as, pc, rq, keys);
if (await level.compareTo(QueryRetrieveLevel2.PATIENT) === 0) {
return await (new JsPatientQueryTask(as, pc, rq, keys)).get();
Expand Down
1 change: 1 addition & 0 deletions dimse/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class DcmQrScp {
await dicomServiceRegistry.addDicomService(new JsCFindScp().getPatientRootLevel());
await dicomServiceRegistry.addDicomService(new JsCFindScp().getStudyRootLevel());
await dicomServiceRegistry.addDicomService(new JsCFindScp().getPatientStudyOnlyLevel());
await dicomServiceRegistry.addDicomService(new JsCFindScp().getMwlLevel());
// #endregion

// #region C-MOVE
Expand Down
120 changes: 120 additions & 0 deletions dimse/mwlQueryTask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const _ = require("lodash");
const { PatientQueryTask } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/PatientQueryTask");
const { createQueryTaskInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/QueryTaskInject");
const { Attributes } = require("@dcm4che/data/Attributes");
const { Tag } = require("@dcm4che/data/Tag");
const { VR } = require("@dcm4che/data/VR");
const { Association } = require("@dcm4che/net/Association");
const { PresentationContext } = require("@dcm4che/net/pdu/PresentationContext");
const { logger } = require("@root/utils/logs/log");
const { UID } = require("@dcm4che/data/UID");
const { QueryTaskUtils } = require("./utils");
const { default: BasicModQueryTask } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/BasicModQueryTask");
const { MwlItemModel } = require("@models/mongodb/models/mwlitems.model");


class JsMwlQueryTask {
constructor(as, pc, rq, keys) {
/** @type { Association } */
this.as = as;
/** @type { PresentationContext } */
this.pc = pc;
/** @type { Attributes } */
this.rq = rq;
/** @type { Attributes } */
this.keys = keys;

this.mwlAttr = null;
this.mwl = null;
}

async get() {
let mwlQueryTask = await BasicModQueryTask.newInstanceAsync(
this.as,
this.pc,
this.rq,
this.keys,
this.getQueryTaskInjectProxy()
);

await this.initCursor();

return mwlQueryTask;
}

getQueryTaskInjectProxy() {
// for creating one
if (!this.matchIteratorProxy) {
this.matchIteratorProxy = new MwlMatchIteratorProxy(this);
}

return this.matchIteratorProxy.get();
}

/**
*
* @param {Attributes} match
* @returns
*/
async basicAdjust(match) {
if (match == null) {
return null;
}

let filtered = new Attributes(await match.size());

await filtered.setNull(Tag.SpecificCharacterSet, VR.CS);
await filtered.addSelected(match, this.keys);
await filtered.supplementEmpty(this.keys);
return filtered;
}

async initCursor() {
let queryAuditManager = await QueryTaskUtils.getAuditManager(this.as);
let dbQuery = await QueryTaskUtils.getDbQuery(this.keys, "mwl");
queryAuditManager.onQuery(
UID.ModalityWorklistInformationModelFind,
JSON.stringify(dbQuery),
"UTF-8"
);

let returnKeys = await QueryTaskUtils.getReturnKeys(this.keys, "mwl");

logger.info(`do DIMSE Modality Work List query: ${JSON.stringify(dbQuery)}`);
this.cursor = await MwlItemModel.getDimseResultCursor({
...dbQuery
}, returnKeys);
}
}

class MwlMatchIteratorProxy {
constructor(mwlQueryTask) {
/** @type {JsMwlQueryTask} */
this.mwlQueryTask = mwlQueryTask;
}

get() {
return createQueryTaskInjectProxy(this.getProxyMethods(), {
keepAsDaemon: true
});
}

getProxyMethods() {
return {
hasMoreMatches: async () => {
this.mwlQueryTask.mwl = await this.mwlQueryTask.cursor.next();
return !_.isNull(this.mwlQueryTask.mwl);
},
nextMatch: async () => {
this.mwlQueryTask.mwlAttr = this.mwlQueryTask.mwl ? await this.mwlQueryTask.mwl.getAttributes() : null;
return this.mwlQueryTask.mwlAttr;
},
adjust: async (match) => {
return this.mwlQueryTask.basicAdjust(match);
}
};
}
}

module.exports.JsMwlQueryTask = JsMwlQueryTask;
module.exports.MwlMatchIteratorProxy = MwlMatchIteratorProxy;
20 changes: 17 additions & 3 deletions dimse/queryBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,37 @@ const { queryTagsOfEachLevel } = require("./queryTagsOfEachLevel");
const { StringUtils } = require("@dcm4che/util/StringUtils");
const { intTagToString } = require("./utils");
const { convertRequestQueryToMongoQuery } = require("@root/api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory");
const { default: Tag } = require("@dcm4che/data/Tag");

class DimseQueryBuilder {

/**
*
* @param {Attributes} queryKeys
* @param {"patient" | "study" | "series" | "instance"} level
* @param {"patient" | "study" | "series" | "instance" | "mwl"} level
*/
constructor(queryKeys, level="patient") {
constructor(queryKeys, level = "patient") {
this.queryKeys = queryKeys;
this.level = level;
}

async toNormalQuery() {
const queryTags = queryTagsOfEachLevel[this.level];
let query = {};
for (let i = 0 ; i < queryTags.length ; i++) {

let spsKeys = await this.queryKeys.getNestedDataset(Tag.ScheduledProcedureStepSequence);
if (spsKeys != null &&
!(await spsKeys.isEmpty()) &&
this.level === "mwl"
) {
let spsQueryTags = queryTagsOfEachLevel.mwlSps;
for (let i = 0; i < spsQueryTags.length; i++) {
let tagStringValues = await StringUtils.maskNull(await spsKeys.getStrings(spsQueryTags[i]));
query[`${intTagToString(Tag.ScheduledProcedureStepSequence)}.Value.${intTagToString(spsQueryTags[i])}.Value`] = tagStringValues.join(",");
}
}

for (let i = 0; i < queryTags.length; i++) {
let tag = queryTags[i];
/** @type {string[]} */
let tagStringValues = await StringUtils.maskNull(await this.queryKeys.getStrings(tag));
Expand Down
47 changes: 47 additions & 0 deletions dimse/queryTagsOfEachLevel.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,53 @@ const queryTagsOfEachLevel = {
Tag.SOPInstanceUID,
Tag.SOPClassUID,
Tag.InstanceNumber
],
"mwl": [
Tag.AccessionNumber,
Tag.ReferringPhysicianName,
Tag.ReferencedStudySequence,
Tag.ReferencedPatientSequence,
Tag.PatientName,
Tag.PatientID,
Tag.IssuerOfPatientID,
Tag.IssuerOfPatientIDQualifiersSequence,
Tag.PatientBirthDate,
Tag.PatientSex,
Tag.OtherPatientIDsSequence,
Tag.PatientWeight,
Tag.MedicalAlerts,
Tag.Allergies,
Tag.PregnancyStatus,
Tag.StudyInstanceUID,
Tag.RequestingPhysician,
Tag.RequestedProcedureDescription,
Tag.RequestedProcedureCodeSequence,
Tag.AdmissionID,
Tag.SpecialNeeds,
Tag.CurrentPatientLocation,
Tag.PatientState,
Tag.ScheduledProcedureStepSequence,
Tag.RequestedProcedureID,
Tag.RequestedProcedurePriority,
Tag.PatientTransportArrangements,
Tag.ConfidentialityConstraintOnPatientDataDescription,
Tag.WorklistLabel
],
"mwlSps": [
Tag.Modality,
Tag.AnatomicalOrientationType,
Tag.RequestedContrastAgent,
Tag.ScheduledStationAETitle,
Tag.ScheduledProcedureStepStartDate,
Tag.ScheduledProcedureStepStartTime,
Tag.ScheduledPerformingPhysicianName,
Tag.ScheduledProcedureStepDescription,
Tag.ScheduledProtocolCodeSequence,
Tag.ScheduledProcedureStepID,
Tag.ScheduledProcedureStepStatus,
Tag.ScheduledStationName,
Tag.ScheduledProcedureStepLocation,
Tag.PreMedication
]
};

Expand Down
18 changes: 18 additions & 0 deletions models/mongodb/models/mwlitems.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ const { tagsNeedStore } = require("../../DICOM/dicom-tags-mapping");
const { getVRSchema } = require("../schema/dicomJsonAttribute");
const { IncludeFieldsFactory } = require("../service");
const { dictionary } = require("@models/DICOM/dicom-tags-dic");
const { raccoonConfig } = require("@root/config-class");

let Common;
if (raccoonConfig.dicomDimseConfig.enableDimse) {
require("@models/DICOM/dcm4che/java-instance");
Common = require("@java-wrapper/org/github/chinlinlee/dcm777/net/common/Common").Common;
}


let mwlItemSchema = new mongoose.Schema(
{},
Expand All @@ -15,6 +23,12 @@ let mwlItemSchema = new mongoose.Schema(
getters: true
},
statics: {
getDimseResultCursor: async function (query, keys) {
return mongoose.model("mwlItems").find(query, keys).setOptions({
strictQuery: false
})
.cursor();
},
/**
*
* @param {import("../../../utils/typeDef/dicom").DicomJsonMongoQueryOptions} queryOptions
Expand Down Expand Up @@ -71,6 +85,10 @@ let mwlItemSchema = new mongoose.Schema(
delete obj._id;
delete obj.id;
return obj;
},
getAttributes: async function () {
let jsonStr = JSON.stringify(this.toDicomJson());
return await Common.getAttributesFromJsonString(jsonStr);
}
}
}
Expand Down

0 comments on commit 4965b62

Please sign in to comment.