Skip to content

Commit

Permalink
feat: create FHIR resource when doing STOW-RS
Browse files Browse the repository at this point in the history
- Add logger for FHIR
- Not append await on `dicomToFHIR`, if FHIR is not operate completly,
Please check the log.
- Add class to operate dicom convert to FHIR
ImagingStudy, Patient, Endpoint
  • Loading branch information
Chinlinlee committed Apr 25, 2022
1 parent 638d35a commit b381036
Show file tree
Hide file tree
Showing 10 changed files with 638 additions and 191 deletions.
26 changes: 22 additions & 4 deletions api/dicom-web/controller/STOW-RS/storeInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ const errorResponseMessage = require('../../../../utils/errorResponse/errorRespo
const { dcm2jsonV8 } = require('../../../../models/DICOM/dcmtk');
const { logger } = require('../../../../utils/log');
const dicomBulkDataModel = require('../../../../models/mongodb/models/dicomBulkData');
const dicomModel = require('../../../../models/mongodb/models/dicom');
const mongoose = require('mongoose');
const { dicomJsonToFHIRImagingStudy } = require('../../../../models/FHIR/DICOMToFHIRImagingStudy');
const { DICOMFHIRConverter } = require('../../../../models/FHIR/DICOM/DICOMToFHIR');

/**
* *The SQ of Whole slide may have over thousand of length cause process block.
Expand Down Expand Up @@ -127,7 +126,8 @@ async function storeInstance(req, multipartData) {
_.set(sopSeq, "00081190.vr", "UT");
_.set(sopSeq, "00081190.Value", [retrieveUrlObj.instance]);
storeMessage["00081199"]["Value"].push(sopSeq);


dicomToFHIR(req, removedTagsDicomJson.dicomJson);
// dicomJsonToFHIRImagingStudy(removedTagsDicomJson.dicomJson);
}
return {
Expand All @@ -136,6 +136,24 @@ async function storeInstance(req, multipartData) {
};
}

/**
*
* @param {import('http').IncomingMessage} req
* @param {JSON} dicomJson
*/
async function dicomToFHIR(req, dicomJson) {
let dicomFHIRConverter = new DICOMFHIRConverter();
dicomFHIRConverter.dicomWeb.name = `raccoon-dicom-web-server`;

let protocol = req.secure? "https" : "http";
dicomFHIRConverter.dicomWeb.retrieveStudiesUrl = `${protocol}://${req.headers.host}/${process.env.DICOMWEB_API}/studies`;

dicomFHIRConverter.dicomJsonToFHIR(dicomJson);

dicomFHIRConverter.fhir.baseUrl = process.env.FHIRSERVER_BASE_URL;
await dicomFHIRConverter.postDicomFhir();
}

/**
*
* @param {import('http').IncomingMessage} req
Expand Down Expand Up @@ -423,7 +441,7 @@ async function storeDICOMJsonToDB(uidObj, dicomJson) {
}
delete dicomJson.sopClass;
delete dicomJson.sopInstanceUID;
await dicomModel.findOneAndUpdate(query, dicomJson, {
await mongoose.model("dicom").findOneAndUpdate(query, dicomJson, {
upsert: true
});
} catch(e) {
Expand Down
7 changes: 7 additions & 0 deletions config/log4js.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
],
"level": "DEBUG",
"enableCallStack": true
},
"raccoon-polka-fhir": {
"appenders": [
"console"
],
"level": "DEBUG",
"enableCallStack": true
}
}
}
264 changes: 264 additions & 0 deletions models/FHIR/DICOM/DICOMToFHIR.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
const { URL } = require('url');
const axios = require('axios').default;
const _ = require('lodash');
const { urlJoin } = require('../../../utils/url');
const { fhirLogger } = require('../../../utils/log');
const {
dicomJsonToFHIRImagingStudy
} = require('./DICOMToFHIRImagingStudy');
const {
dicomJsonToFHIRPatient
} = require('./DICOMToFHIRPatient');
const {
dicomJsonToFHIREndpoint
} = require('./DICOMToFHIREndpoint');

class DICOMFHIRConverter {
constructor() {
this.dicomFHIR = {
patient: {},
endpoint: {},
imagingStudy: {}
};
this.fhirPatient = {};
this.fhirEndpoint = {};
this.fhirImagingStudy = {};
this.dicomWeb = {
retrieveStudiesUrl: "",
name: ""
};
this.fhir = {
baseUrl: ""
}
}

/**
*
* @param {JSON} dicomJson The DICOM Json model
*/
dicomJsonToFHIR(dicomJson) {
let patient = dicomJsonToFHIRPatient(dicomJson);
let imagingStudy = dicomJsonToFHIRImagingStudy(dicomJson);
if ( !imagingStudy.subject.reference.includes(patient.id) ) {
imagingStudy.subject.reference = `Patient/${patient.id}`;
}
let endpoint = dicomJsonToFHIREndpoint(this.dicomWeb.retrieveStudiesUrl, this.dicomWeb.name);
this.dicomFHIR.patient = patient;
this.dicomFHIR.imagingStudy = imagingStudy;
this.dicomFHIR.endpoint = endpoint;
return this.dicomFHIR;
}

//#region ImagingStudy

async getImagingStudyFromFHIRServer() {
try {
let getUrl = urlJoin(`/ImagingStudy`, this.fhir.baseUrl);
let getURLObj = new URL(getUrl);
let identifier = this.dicomFHIR.imagingStudy.identifier.find(v=> v.system === "urn:dicom:uid");
fhirLogger.info(`[FHIR] [Get ImagingStudy from FHIR server, identifier: ${identifier.value}]`);
getURLObj.searchParams.append("identifier", identifier.value);
let { data } = await axios.get(getURLObj.href, {
headers: {
"Content-Type": "application/fhir+json"
}
});
let fhirBundleEntry = data.entry;
if (_.isUndefined(fhirBundleEntry)) return undefined;
else if (_.isArray(data.entry) && data.entry.length === 0) return undefined;
return data.entry.pop();
} catch(e) {
if (e.isAxiosError) {
console.log(e.response);
} else {
console.error(e);
}
throw e;
}
}

/**
* POST the imagingStudy resource to FHIR server
*/
async createImagingStudy() {
try {
let postUrl = urlJoin(`/ImagingStudy`, this.fhir.baseUrl);
let { data } = await axios.post(postUrl, this.dicomFHIR.imagingStudy, {
headers: {
"Content-Type": "application/fhir+json",
"Accept": "application/fhir+json"
}
});
return data;
} catch(e) {
throw e;
}
}

async updateImagingStudy(existImagingStudy) {
try {
let identifier = existImagingStudy.identifier.find(v=> v.system === "urn:dicom:uid");
fhirLogger.info(`[FHIR] [ImagingStudy exists, identifier: ${identifier.value}, update it]`);
this.updateExistSeries(existImagingStudy);
let putUrl = urlJoin(`/ImagingStudy/${existImagingStudy.id}`, this.fhir.baseUrl);
let { data } = await axios.put(putUrl, existImagingStudy, {
headers: {
"Content-Type": "application/fhir+json",
"Accept": "application/fhir+json"
}
});
return data;
} catch(e) {
throw e;
}
}

updateExistSeries(existImagingStudy) {
let seriesUID = this.dicomFHIR.imagingStudy.series[0].uid;
let existSeriesIndex = existImagingStudy.series.findIndex(v=> v.uid === seriesUID);
if (existSeriesIndex !== -1) {
let existSeries = existImagingStudy.series[existSeriesIndex];
let seriesClone = _.cloneDeep(this.dicomFHIR.imagingStudy.series[0]);
delete seriesClone.instance;
existSeries = {
...existSeries,
...seriesClone
};
this.updateExistInstance(existImagingStudy, existSeriesIndex);
} else {
if (existImagingStudy.series) {
existImagingStudy.series.push(this.dicomFHIR.imagingStudy.series[0]);
this.updateExistInstance(existImagingStudy, existImagingStudy.series.length - 1);
}
else {
existImagingStudy.series = this.dicomFHIR.imagingStudy.series;
}
}
}

async updateExistInstance(existImagingStudy, seriesIndex) {
let series = this.dicomFHIR.imagingStudy.series[0];
let existSeries = existImagingStudy.series[seriesIndex];
let instanceUID = series.instance[0].uid;
let existInstanceIndex = existSeries.instance.findIndex(v=> v.uid === instanceUID);
if (existInstanceIndex !== -1) {
let existInstance = existSeries.instance[existInstanceIndex];
existInstance = {
...existInstance,
...series.instance[0]
};
} else {
existSeries.instance.push(series.instance[0]);
}
}

//#endregion

//#region Patient
async getPatientFromFHIRServer() {
try {
let patientID = this.dicomFHIR.patient.id;
let getUrl = urlJoin(`/Patient/${patientID}`, this.fhir.baseUrl);
fhirLogger.info(`[FHIR] [Get Patient from FHIR server, id: ${patientID}]`);
let { data } = await axios.get(getUrl, {
headers: {
"Content-Type": "application/fhir+json"
}
});
return data;
} catch(e) {
if (e.isAxiosError) {
if (e.response.status === 404) return undefined;
console.log(e.response);
} else {
console.error(e);
}
throw e;
}
}


async createPatientClientId() {
try {
let patientID = this.dicomFHIR.patient.id;
let putUrl = urlJoin(`/Patient/${patientID}`, this.fhir.baseUrl);
fhirLogger.info(`[FHIR] [The Patient id: ${patientID} not exists, creating it]`);
let { data } = await axios.put(putUrl, this.dicomFHIR.patient, {
headers: {
"Content-Type": "application/fhir+json",
"Accept": "application/fhir+json"
}
});
return data;
} catch(e) {
throw e;
}
}

//#endregion

//#region Endpoint

async getEndpointFromFHIRServer() {
try {
let endpointID = this.dicomWeb.name;
let getUrl = urlJoin(`/Endpoint/${endpointID}`, this.fhir.baseUrl);
fhirLogger.info(`[FHIR] [Get Endpoint from FHIR server, id: ${endpointID}]`);
let { data } = await axios.get(getUrl, {
headers: {
"Content-Type": "application/fhir+json",
"Accept": "application/fhir+json"
}
});
return data;
} catch(e) {
if (e.isAxiosError) {
if (e.response.status === 404) return undefined;
console.log(e.response);
} else {
console.error(e);
}
throw e;
}
}

async createEndpointClientId() {
try {
let endpointID = this.dicomWeb.name;
let putUrl = urlJoin(`/Endpoint/${endpointID}`, this.fhir.baseUrl);
fhirLogger.info(`[FHIR] [The Endpoint id: ${endpointID} not exists, creating it]`);
let { data } = await axios.put(putUrl, this.dicomFHIR.endpoint, {
headers: {
"Content-Type": "application/fhir+json",
"Accept": "application/fhir+json"
}
});
return data;
} catch(e) {
throw e;
}
}

//#endregion

async postDicomFhir() {
//Check patient is exists in FHIR server, if not, create it.
let patientInFHIRServer = await this.getPatientFromFHIRServer();
if (!patientInFHIRServer) await this.createPatientClientId();

//Check endpoint is exists in FHIR server, if not, create it.
let endpointInFHIRServer = await this.getEndpointFromFHIRServer();
if (!endpointInFHIRServer) await this.createEndpointClientId();

//Check endpoint is exists in FHIR server, if not, create it, otherwise update it.
let imagingStudyInFHIRServer = await this.getImagingStudyFromFHIRServer();
if (imagingStudyInFHIRServer) { //update the exist imagingStudy
await this.updateImagingStudy(imagingStudyInFHIRServer.resource);
} else { //create a new imagingStudy
await this.createImagingStudy();
}
}
}


module.exports.DICOMFHIRConverter = DICOMFHIRConverter;
37 changes: 37 additions & 0 deletions models/FHIR/DICOM/DICOMToFHIREndpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*connectionType
dicom-wado-rs
dicom-qido-rs
dicom-stow-rs
dicon-wado-uri
direct-project
*/

/**
*
* @param {string} addressUrl The DICOMWeb URL of study level
* @param {*} id The unique id of this DICOMWeb URL of PACS server.
* @returns
*/
function dicomJsonToFHIREndpoint(addressUrl, id) {
let endpoint = {
"resourceType" : "Endpoint" ,
"status": "active",
"id" : id ,
connectionType : {
system : "http://terminology.hl7.org/CodeSystem/endpoint-connection-type" ,
code : "dicom-wado-rs"
},
"payloadType": [
{
"text": "DICOM"
}
],
"payloadMimeType": [
"application/dicom"
],
"address": addressUrl
}
return endpoint;
}

module.exports.dicomJsonToFHIREndpoint = dicomJsonToFHIREndpoint;
Loading

0 comments on commit b381036

Please sign in to comment.