Work on refactoring measurements out of Lesion Tracker package
- Removed AssociatedStudies Collection - Measurement API is generated from configuration files - Data exchange methods are defined in configuration
This commit is contained in:
1 parent
c511edd419
commit
ab4d4c9008
537 files changed
+5122
-3545
No files matched your search
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Converts the properties to URL query parameters. Based on:
|
||||
* http://stackoverflow.com/questions/111529/create-query-parameters-in-javascript
|
||||
*
|
||||
* @param data
|
||||
* @returns {string}
|
||||
*/
|
||||
encodeQueryData = function(data) {
|
||||
var ret = [];
|
||||
|
||||
for (var d in data) {
|
||||
if (data[d]) {
|
||||
ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
|
||||
}
|
||||
}
|
||||
|
||||
return ret.join('&');
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export const parseFloatArray = function(obj) {
|
||||
var result = [];
|
||||
|
||||
if (!obj) {
|
||||
return result;
|
||||
}
|
||||
|
||||
var objs = obj.split("\\");
|
||||
for (var i = 0; i < objs.length; i++) {
|
||||
result.push(parseFloat(objs[i]));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const remoteGetValue = function(obj) {
|
||||
return obj ? obj.Value : null;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
Meteor.methods({
|
||||
/**
|
||||
* Retrieves Study metadata given a Study Instance UID
|
||||
* This Meteor method is available from both the client and the server
|
||||
*/
|
||||
GetStudyMetadata: function(studyInstanceUid) {
|
||||
log.info('GetStudyMetadata(%s)', studyInstanceUid);
|
||||
|
||||
// Get the server data. This is user-defined in the config.json files or through servers
|
||||
// configuration modal
|
||||
const server = getCurrentServer();
|
||||
|
||||
if (!server) {
|
||||
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
|
||||
}
|
||||
|
||||
if (server.type === 'dicomWeb') {
|
||||
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
|
||||
} else if (server.type === 'dimse') {
|
||||
return Services.DIMSE.RetrieveMetadata(studyInstanceUid);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { StudyImportStatus } from '../../both/collections';
|
||||
|
||||
var fs = Npm.require('fs');
|
||||
var fiber = Npm.require('fibers');
|
||||
|
||||
WebApp.connectHandlers.use('/uploadFilesToImport', function(req, res) {
|
||||
if (!req.headers.filename) {
|
||||
// Response: BAD REQUEST (400)
|
||||
res.statusCode = 400;
|
||||
res.end();
|
||||
}
|
||||
|
||||
// Store files in temp location (they will be deleted when their import operations are completed)
|
||||
var dicomDir = '/tmp/dicomDir';
|
||||
createFolderIfNotExist(dicomDir);
|
||||
|
||||
var fullFileName = dicomDir + '/' + req.headers.filename;
|
||||
var file = fs.createWriteStream(fullFileName);
|
||||
|
||||
file.on('error',function(error){
|
||||
console.log(error);
|
||||
// Response: INTERNAL SERVER ERROR (500)
|
||||
res.statusCode = 400;
|
||||
res.end();
|
||||
});
|
||||
file.on('finish',function(){
|
||||
// Response: SUCCESS (200)
|
||||
res.writeHead(200, {'Content-Type': 'text/plain'});
|
||||
res.end(fullFileName);
|
||||
});
|
||||
|
||||
// Pipe the request to the file
|
||||
req.pipe(file);
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
/**
|
||||
* Returns true if import is supported for default service type
|
||||
* @returns {boolean}
|
||||
*/
|
||||
importSupported: function() {
|
||||
const server = getCurrentServer();
|
||||
if (server && server.type === 'dimse') {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Imports studies from local into study list
|
||||
* @param studiesToImport Studies to import
|
||||
* @param studyImportStatusId Study import status collection id to track import status
|
||||
*/
|
||||
importStudies: function(studiesToImport, studyImportStatusId) {
|
||||
if (!studiesToImport || !studyImportStatusId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const server = getCurrentServer();
|
||||
|
||||
if (!server) {
|
||||
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
|
||||
}
|
||||
|
||||
if (server.type === 'dicomWeb') {
|
||||
//TODO: Support importing studies into dicomWeb
|
||||
console.log('Importing studies into dicomWeb is currently not supported.');
|
||||
} else if (server.type === 'dimse') {
|
||||
importStudiesDIMSE(studiesToImport, studyImportStatusId);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Create a new study import status item and insert it into the collection to track import status
|
||||
* @returns {studyImportStatusId: string}
|
||||
*/
|
||||
createStudyImportStatus: function() {
|
||||
var studyImportStatus = { numberOfStudiesImported: 0, numberOfStudiesFailed: 0 };
|
||||
return StudyImportStatus.insert(studyImportStatus);
|
||||
},
|
||||
/**
|
||||
* Remove the study import status item from the collection
|
||||
* @param id Collection id of the study import status in the collection
|
||||
*/
|
||||
removeStudyImportStatus: function(id) {
|
||||
StudyImportStatus.remove(id);
|
||||
}
|
||||
});
|
||||
|
||||
function importStudiesDIMSE(studiesToImport, studyImportStatusId) {
|
||||
if (!studiesToImport || !studyImportStatusId) {
|
||||
return;
|
||||
}
|
||||
// Perform C-Store to import studies and handle the callbacks to update import status
|
||||
DIMSE.storeInstances(studiesToImport, function(err, file) {
|
||||
try {
|
||||
// Use fiber to be able to modify meteor collection in callback
|
||||
fiber(function() {
|
||||
try {
|
||||
// Update the import status
|
||||
if (err) {
|
||||
StudyImportStatus.update({_id: studyImportStatusId}, {$inc: {'numberOfStudiesFailed': 1}});
|
||||
console.log("Failed to import study via DIMSE: ", file, err);
|
||||
} else {
|
||||
StudyImportStatus.update({_id: studyImportStatusId}, {$inc: {'numberOfStudiesImported': 1}});
|
||||
console.log("Study successfully imported via DIMSE: ", file);
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
|
||||
StudyImportStatus.update({_id: studyImportStatusId}, {$inc: {'numberOfStudiesFailed': 1}});
|
||||
console.log("Failed to import study via DIMSE: ", file, error);
|
||||
} finally {
|
||||
// The import operation of this file is completed, so delete it if still exists
|
||||
if (fileExists(file)) {
|
||||
fs.unlink(file);
|
||||
}
|
||||
}
|
||||
|
||||
}).run();
|
||||
} catch(error) {
|
||||
StudyImportStatus.update({_id: studyImportStatusId}, {$inc: {'numberOfStudiesFailed': 1}});
|
||||
console.log("Failed to import study via DIMSE: ", file, error);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function createFolderIfNotExist(folder) {
|
||||
var folderParts = folder.split('/');
|
||||
var folderPart = folderParts[0];
|
||||
for (var i = 1; i < folderParts.length; i++) {
|
||||
folderPart += '/' + folderParts[i];
|
||||
if (!folderExists(folderPart)) {
|
||||
fs.mkdirSync(folderPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fileExists(folder) {
|
||||
try {
|
||||
return fs.statSync(folder).isFile();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function folderExists(folder) {
|
||||
try {
|
||||
return fs.statSync(folder).isDirectory();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
Meteor.methods({
|
||||
/**
|
||||
* Use the specified filter to conduct a search from the DICOM server
|
||||
*
|
||||
* @param filter
|
||||
*/
|
||||
StudyListSearch(filter) {
|
||||
// Get the server data. This is user-defined in the config.json files or through servers
|
||||
// configuration modal
|
||||
const server = getCurrentServer();
|
||||
|
||||
if (!server) {
|
||||
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
|
||||
}
|
||||
|
||||
console.log(server);
|
||||
console.log(Services);
|
||||
|
||||
if (server.type === 'dicomWeb') {
|
||||
return Services.QIDO.Studies(server, filter);
|
||||
} else if (server.type === 'dimse') {
|
||||
return Services.DIMSE.Studies(filter);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
Meteor.publish('studyImportStatus', () => {
|
||||
return StudyImportStatus.find();
|
||||
});
|
||||
|
||||
Meteor.publish('servers', () => {
|
||||
return Servers.find();
|
||||
});
|
||||
|
||||
Meteor.publish('currentServer', () => {
|
||||
return CurrentServer.find();
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Meteor.startup(function() {
|
||||
console.log('Adding Servers from JSON Configuration');
|
||||
Servers.remove({
|
||||
origin: 'json'
|
||||
});
|
||||
|
||||
_.each(Meteor.settings.servers, function(endpoints, serverType) {
|
||||
_.each(endpoints, function(endpoint) {
|
||||
const server = _.clone(endpoint);
|
||||
server.origin = 'json';
|
||||
server.type = serverType;
|
||||
Servers.insert(server);
|
||||
});
|
||||
});
|
||||
|
||||
ServersControl.resetCurrentServer();
|
||||
});
|
||||
|
||||
class ServersControl {
|
||||
|
||||
static writeCallback(error, affected) {
|
||||
if (error) {
|
||||
throw new Meteor.Error('data-write', error);
|
||||
}
|
||||
}
|
||||
|
||||
static resetCurrentServer() {
|
||||
const currentServer = CurrentServer.findOne();
|
||||
if (currentServer && Servers.find({ _id: currentServer.serverId }).count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newServer = Servers.findOne({
|
||||
origin: 'json',
|
||||
type: Meteor.settings.defaultServiceType || 'dicomWeb'
|
||||
});
|
||||
|
||||
if (newServer) {
|
||||
CurrentServer.remove({});
|
||||
CurrentServer.insert({
|
||||
serverId: newServer._id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static find(query) {
|
||||
return Servers.find(query).fetch();
|
||||
}
|
||||
|
||||
static save(serverSettings) {
|
||||
const query = {
|
||||
_id: serverSettings._id
|
||||
};
|
||||
const options = {
|
||||
upsert: true
|
||||
};
|
||||
|
||||
if (!serverSettings._id) {
|
||||
delete serverSettings._id;
|
||||
}
|
||||
|
||||
return Servers.update(query, serverSettings, options, this.writeCallback);
|
||||
}
|
||||
|
||||
static setActive(serverId) {
|
||||
CurrentServer.remove({});
|
||||
CurrentServer.insert({
|
||||
serverId: serverId
|
||||
});
|
||||
}
|
||||
|
||||
static remove(serverId) {
|
||||
var query = {
|
||||
_id: serverId
|
||||
};
|
||||
|
||||
const removeStatus = Servers.remove(query, this.writeCallback);
|
||||
|
||||
ServersControl.resetCurrentServer();
|
||||
|
||||
return removeStatus;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Meteor.methods({
|
||||
serverFind: query => ServersControl.find(query),
|
||||
serverSave: serverSettings => ServersControl.save(serverSettings),
|
||||
serverSetActive: serverId => ServersControl.setActive(serverId),
|
||||
serverRemove: serverId => ServersControl.remove(serverId)
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Parses data returned from a study search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instanceRaw) {
|
||||
var instance = instanceRaw.toObject();
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = instance[0x0020000E];
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: instance[0x00200011],
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// TODO: Check which peer it should point to
|
||||
var server = getCurrentServer().peers[0];
|
||||
|
||||
var serverRoot = server.host + ':' + server.port;
|
||||
|
||||
var sopInstanceUid = instance[0x00080018];
|
||||
var uri = serverRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: instance[0x00080016],
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: instance[0x00200013]
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a DIMSE call
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.DIMSE.Instances = function(studyInstanceUid) {
|
||||
//var url = buildUrl(server, studyInstanceUid);
|
||||
var result = DIMSE.retrieveInstances(studyInstanceUid);
|
||||
|
||||
return {
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(result)
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import { parseFloatArray } from '../../lib/parseFloatArray';
|
||||
|
||||
/**
|
||||
* Parses the SourceImageSequence, if it exists, in order
|
||||
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||
*
|
||||
* @param instance
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getSourceImageInstanceUid(instance) {
|
||||
// TODO= Parse the whole Source Image Sequence
|
||||
// This is a really poor workaround for now.
|
||||
// Later we should probably parse the whole sequence.
|
||||
var SourceImageSequence = instance[0x00082112];
|
||||
if (SourceImageSequence && SourceImageSequence.length) {
|
||||
return SourceImageSequence[0][0x00081155];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the element (e.g. '00280009')
|
||||
*
|
||||
* @param element - The group/element of the element (e.g. '00280009')
|
||||
* @param defaultValue - The default value to return if the element does not exist
|
||||
* @returns {*}
|
||||
*/
|
||||
function getValue(element, defaultValue) {
|
||||
if (!element || !element.value) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return element.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a DIMSE search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(studyInstanceUid, resultData) {
|
||||
log.info('resultDataToStudyMetadata');
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0].toObject();
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: anInstance[0x00100010],
|
||||
patientId: anInstance[0x00100020],
|
||||
patientBirthDate: anInstance[0x00100030],
|
||||
patientSex: anInstance[0x00100040],
|
||||
accessionNumber: anInstance[0x00080050],
|
||||
studyDate: anInstance[0x00080020],
|
||||
modalities: anInstance[0x00080061],
|
||||
studyDescription: anInstance[0x00081030],
|
||||
imageCount: anInstance[0x00201208],
|
||||
studyInstanceUid: anInstance[0x0020000D],
|
||||
institutionName: anInstance[0x00080080]
|
||||
};
|
||||
|
||||
resultData.forEach(function(instanceRaw) {
|
||||
var instance = instanceRaw.toObject();
|
||||
var seriesInstanceUid = instance[0x0020000E];
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: instance[0x0008103E],
|
||||
modality: instance[0x00080060],
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: parseFloat(instance[0x00200011]),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var sopInstanceUid = instance[0x00080018];
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: instance[0x00080008],
|
||||
sopClassUid: instance[0x00080016],
|
||||
modality: instance[0x00080060],
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: parseFloat(instance[0x00200013]),
|
||||
imagePositionPatient: instance[0x00200032],
|
||||
imageOrientationPatient: instance[0x00200037],
|
||||
frameOfReferenceUID: instance[0x00200052],
|
||||
sliceThickness: parseFloat(instance[0x00180050]),
|
||||
sliceLocation: parseFloat(instance[0x00201041]),
|
||||
tablePosition: parseFloat(instance[0x00189327]),
|
||||
samplesPerPixel: parseFloat(instance[0x00280002]),
|
||||
photometricInterpretation: instance[0x00280004],
|
||||
rows: parseFloat(instance[0x00280010]),
|
||||
columns: parseFloat(instance[0x00280011]),
|
||||
pixelSpacing: instance[0x00280030],
|
||||
bitsAllocated: parseFloat(instance[0x00280100]),
|
||||
bitsStored: parseFloat(instance[0x00280101]),
|
||||
highBit: parseFloat(instance[0x00280102]),
|
||||
pixelRepresentation: parseFloat(instance[0x00280103]),
|
||||
windowCenter: instance[0x00281050],
|
||||
windowWidth: instance[0x00281051],
|
||||
rescaleIntercept: parseFloat(instance[0x00281052]),
|
||||
rescaleSlope: parseFloat(instance[0x00281053]),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: instance[0x00200062],
|
||||
viewPosition: instance[0x00185101],
|
||||
acquisitionDatetime: instance[0x0008002A],
|
||||
numFrames: parseFloat(instance[0x00280008]),
|
||||
frameIncrementPointer: getValue(instance[0x00280009]),
|
||||
frameTime: parseFloat(instance[0x00181063]),
|
||||
frameTimeVector: parseFloatArray(instance[0x00181065]),
|
||||
lossyImageCompression: instance[0x00282110],
|
||||
derivationDescription: instance[0x00282111],
|
||||
lossyImageCompressionRatio: instance[0x00282112],
|
||||
lossyImageCompressionMethod: instance[0x00282114],
|
||||
spacingBetweenSlices: instance[0x00180088]
|
||||
};
|
||||
|
||||
// Retrieve the actual data over WADO-URI
|
||||
var server = getCurrentServer();
|
||||
instanceSummary.wadouri = WADOProxy.convertURL(server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom', server.requestOptions);
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
|
||||
studyData.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using DIMSE
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.DIMSE.RetrieveMetadata = function(studyInstanceUid) {
|
||||
// TODO: Check which peer it should point to
|
||||
const activeServer = getCurrentServer().peers[0];
|
||||
const supportsInstanceRetrievalByStudyUid = activeServer.supportsInstanceRetrievalByStudyUid;
|
||||
let results;
|
||||
|
||||
// Check explicitly for a value of false, since this property
|
||||
// may be left undefined in config files
|
||||
if (supportsInstanceRetrievalByStudyUid === false) {
|
||||
results = DIMSE.retrieveInstancesByStudyOnly(studyInstanceUid);
|
||||
} else {
|
||||
results = DIMSE.retrieveInstances(studyInstanceUid);
|
||||
}
|
||||
|
||||
return resultDataToStudyMetadata(studyInstanceUid, results);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const setupDIMSE = () => {
|
||||
// Terminate existing DIMSE servers and sockets and clean up the connection object
|
||||
DIMSE.connection.reset();
|
||||
|
||||
// Get the new server configuration
|
||||
const server = getCurrentServer();
|
||||
|
||||
// Stop here if the new server is not of DIMSE type
|
||||
if (server.type !== 'dimse') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if peers were defined in the server configuration and throw an error if not
|
||||
const peers = server.peers;
|
||||
if (!peers || !peers.length) {
|
||||
console.error('dimse-config: ' + 'No DIMSE Peers provided.');
|
||||
throw new Meteor.Error('dimse-config', 'No DIMSE Peers provided.');
|
||||
}
|
||||
|
||||
// Add all the DIMSE peers, establishing the connections
|
||||
console.log('Adding DIMSE peers');
|
||||
try {
|
||||
peers.forEach(peer => DIMSE.connection.addPeer(peer));
|
||||
} catch(error) {
|
||||
console.error('dimse-addPeers: ' + error);
|
||||
throw new Meteor.Error('dimse-addPeers', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup the DIMSE connections on startup or when the current server is changed
|
||||
Meteor.startup(() => {
|
||||
CurrentServer.find().observe({
|
||||
added: setupDIMSE,
|
||||
changed: setupDIMSE
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { moment } from 'meteor/mrt:moment';
|
||||
|
||||
/**
|
||||
* Parses resulting data from a QIDO call into a set of Study MetaData
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} An array of Study MetaData objects
|
||||
*/
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
resultData.forEach(function(studyRaw) {
|
||||
var study = studyRaw.toObject();
|
||||
studies.push({
|
||||
studyInstanceUid: study[0x0020000D],
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: study[0x00080020],
|
||||
studyTime: study[0x00080030],
|
||||
accessionNumber: study[0x00080050],
|
||||
referringPhysicianName: study[0x00080090],
|
||||
// 00081190 = URL
|
||||
patientName: study[0x00100010],
|
||||
patientId: study[0x00100020],
|
||||
patientBirthdate: study[0x00100030],
|
||||
patientSex: study[0x00100040],
|
||||
imageCount: study[0x00201208],
|
||||
studyId: study[0x00200010],
|
||||
studyDescription: study[0x00081030],
|
||||
modalities: study[0x00080061]
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.DIMSE.Studies = function(filter) {
|
||||
log.info('Services.DIMSE.Studies');
|
||||
|
||||
let filterStudyDate = '';
|
||||
if (filter.studyDateFrom && filter.studyDateTo) {
|
||||
const convertDate = date => moment(date, 'MM/DD/YYYY').format('YYYYMMDD');
|
||||
const dateFrom = convertDate(filter.studyDateFrom);
|
||||
const dateTo = convertDate(filter.studyDateTo);
|
||||
filterStudyDate = `${dateFrom}-${dateTo}`;
|
||||
}
|
||||
|
||||
var parameters = {
|
||||
0x00100010: filter.patientName,
|
||||
0x00100020: filter.patientId,
|
||||
0x00080050: filter.accessionNumber,
|
||||
0x00080020: filterStudyDate,
|
||||
0x00081030: filter.studyDescription,
|
||||
0x00100040: '',
|
||||
0x00201208: '',
|
||||
0x00080061: filter.modalitiesInStudy
|
||||
};
|
||||
|
||||
var results = DIMSE.retrieveStudies(parameters);
|
||||
return resultDataToStudies(results);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
Services = {};
|
||||
Services.QIDO = {};
|
||||
Services.WADO = {};
|
||||
Services.DIMSE = {};
|
||||
Services.REMOTE = {};
|
||||
|
||||
remoteGetValue = function(obj) {
|
||||
return obj ? obj.Value : null;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Creates a QIDO URL given the server settings and a study instance UID
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string} URL to be used for QIDO calls
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
return server.qidoRoot + '/studies/' + studyInstanceUid + '/instances';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data returned from a QIDO search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param server The DICOM server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: DICOMWeb.getString(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// The uri for the dicomweb
|
||||
// NOTE: DCM4CHEE seems to return the data zipped
|
||||
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
|
||||
// know how to parse yet
|
||||
//var uri = DICOMWeb.getString(instance['00081190']);
|
||||
//uri = uri.replace('wado-rs', 'dicom-web');
|
||||
|
||||
// manually create a WADO-URI from the UIDs
|
||||
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom';
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: DICOMWeb.getString(instance['00200013'])
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a QIDO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.QIDO.Instances = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
return {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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 "";
|
||||
var year = date.getFullYear().toString();
|
||||
var month = (date.getMonth() + 1).toString();
|
||||
var 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a QIDO URL given server details and a set of specified search filter
|
||||
* items
|
||||
*
|
||||
* @param server
|
||||
* @param filter
|
||||
* @returns {string} The URL with encoded filter query data
|
||||
*/
|
||||
function filterToQIDOURL(server, filter) {
|
||||
var commaSeparatedFields = [
|
||||
'00081030', // Study Description
|
||||
'00080060' //Modality
|
||||
// Add more fields here if you want them in the Study List
|
||||
].join(',');
|
||||
|
||||
var parameters = {
|
||||
PatientName: filter.patientName,
|
||||
PatientID: filter.patientId,
|
||||
AccessionNumber: filter.accessionNumber,
|
||||
StudyDescription: filter.studyDescription,
|
||||
ModalitiesInStudy: filter.modalitiesInStudy,
|
||||
limit: filter.limit || 20,
|
||||
includefield: server.qidoSupportsIncludeField ? 'all' : commaSeparatedFields
|
||||
};
|
||||
|
||||
// build the StudyDate range parameter
|
||||
if (filter.studyDateFrom || filter.sutydDateTo) {
|
||||
var date = "".concat(dateToString(new Date(filter.studyDateFrom)), "-", dateToString(new Date(filter.studyDateTo)));
|
||||
parameters.StudyDate = date;
|
||||
}
|
||||
|
||||
return server.qidoRoot + '/studies?' + encodeQueryData(parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses resulting data from a QIDO call into a set of Study MetaData
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} An array of Study MetaData objects
|
||||
*/
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
if (!resultData || !resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
resultData.forEach(function(study) {
|
||||
studies.push({
|
||||
studyInstanceUid: DICOMWeb.getString(study['0020000D']),
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: DICOMWeb.getString(study['00080020']),
|
||||
studyTime: DICOMWeb.getString(study['00080030']),
|
||||
accessionNumber: DICOMWeb.getNumber(study['00080050']),
|
||||
referringPhysicianName: DICOMWeb.getString(study['00080090']),
|
||||
// 00081190 = URL
|
||||
patientName: DICOMWeb.getName(study['00100010']),
|
||||
patientId: DICOMWeb.getString(study['00100020']),
|
||||
patientBirthdate: DICOMWeb.getString(study['00100030']),
|
||||
patientSex: DICOMWeb.getString(study['00100040']),
|
||||
studyId: DICOMWeb.getString(study['00200010']),
|
||||
numberOfStudyRelatedSeries: DICOMWeb.getString(study['00201206']),
|
||||
numberOfStudyRelatedInstances: DICOMWeb.getString(study['00201208']),
|
||||
studyDescription: DICOMWeb.getString(study['00081030']),
|
||||
// modality: DICOMWeb.getString(study['00080060']),
|
||||
// modalitiesInStudy: DICOMWeb.getString(study['00080061']),
|
||||
modalities: DICOMWeb.getString(DICOMWeb.getModalities(study['00080060'], study['00080061']))
|
||||
});
|
||||
});
|
||||
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.QIDO.Studies = function(server, filter) {
|
||||
var url = filterToQIDOURL(server, filter);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
return resultDataToStudies(result.data);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import { remoteGetValue } from '../../lib/remoteGetValue';
|
||||
|
||||
/**
|
||||
* Parses data returned from a QIDO search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param server The DICOM server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = remoteGetValue(instance['0020,000e']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if(!series) {
|
||||
series = {
|
||||
seriesInstanceUid : seriesInstanceUid,
|
||||
seriesNumber : parseFloat(remoteGetValue(instance['0020,0011'])),
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// The uri for the dicomweb
|
||||
// NOTE: DCM4CHEE seems to return the data zipped
|
||||
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
|
||||
// know how to parse yet
|
||||
//var uri = DICOMWeb.getString(instance['00081190']);
|
||||
//uri = uri.replace('wado-rs', 'dicom-web');
|
||||
|
||||
// manually create a WADO-URI from the UIDs
|
||||
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
|
||||
var sopInstanceUid = remoteGetValue(instance['0008,0018']);
|
||||
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: remoteGetValue(instance['0008,0016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: parseFloat(remoteGetValue(instance['0020,0013']))
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a QIDO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.REMOTE.Instances = function(server, studyInstanceUid) {
|
||||
var parameters = {
|
||||
PatientName: "",
|
||||
PatientID: "",
|
||||
AccessionNumber: "",
|
||||
SeriesInstanceUID: "",
|
||||
SeriesNumber : "",
|
||||
SOPClassUID : "",
|
||||
InstanceNumber : ""
|
||||
};
|
||||
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
|
||||
return {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, remote.findInstances(server.modality, studyInstanceUid, null, parameters))
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
import { remoteGetValue } from '../../lib/remoteGetValue';
|
||||
import { parseFloatArray } from '../../lib/parseFloatArray';
|
||||
|
||||
/**
|
||||
* Parses the SourceImageSequence, if it exists, in order
|
||||
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||
*
|
||||
* @param instance
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getSourceImageInstanceUid(instance) {
|
||||
// TODO= Parse the whole Source Image Sequence
|
||||
// This is a really poor workaround for now.
|
||||
// Later we should probably parse the whole sequence.
|
||||
var SourceImageSequence = remoteGetValue(instance['0008,2112']);
|
||||
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
|
||||
return SourceImageSequence.Value[0]['0008,1155'].Value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a WADO search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0];
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: remoteGetValue(anInstance['0010,0010']),
|
||||
patientId: remoteGetValue(anInstance['0010,0020']),
|
||||
accessionNumber: remoteGetValue(anInstance['0008,0050']),
|
||||
studyDate: remoteGetValue(anInstance['0008,0020']),
|
||||
modalities: remoteGetValue(anInstance['0008,0061']),
|
||||
studyDescription: remoteGetValue(anInstance['0008,1030']),
|
||||
imageCount: remoteGetValue(anInstance['0020,1208']),
|
||||
studyInstanceUid: remoteGetValue(anInstance['0020,000d'])
|
||||
};
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = remoteGetValue(instance['0020,000e']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: remoteGetValue(instance['0008,103e']),
|
||||
modality: remoteGetValue(instance['0008,0060']),
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: parseFloat(remoteGetValue(instance['0020,0011'])),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var sopInstanceUid = remoteGetValue(instance['0008,0018']);
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: remoteGetValue(instance['0008,0008']),
|
||||
sopClassUid: remoteGetValue(instance['0008,0016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: parseFloat(remoteGetValue(instance['0020,0013'])),
|
||||
imagePositionPatient: remoteGetValue(instance['0020,0032']),
|
||||
imageOrientationPatient: remoteGetValue(instance['0020,0037']),
|
||||
frameOfReferenceUID: remoteGetValue(instance['0020,0052']),
|
||||
sliceLocation: parseFloat(remoteGetValue(instance['0020,1041'])),
|
||||
samplesPerPixel: parseFloat(remoteGetValue(instance['0028,0002'])),
|
||||
photometricInterpretation: remoteGetValue(instance['0028,0004']),
|
||||
rows: parseFloat(remoteGetValue(instance['0028,0010'])),
|
||||
columns: parseFloat(remoteGetValue(instance['0028,0011'])),
|
||||
pixelSpacing: remoteGetValue(instance['0028,0030']),
|
||||
bitsAllocated: parseFloat(remoteGetValue(instance['0028,0100'])),
|
||||
bitsStored: parseFloat(remoteGetValue(instance['0028,0101'])),
|
||||
highBit: parseFloat(remoteGetValue(instance['0028,0102'])),
|
||||
pixelRepresentation: parseFloat(remoteGetValue(instance['0028,0103'])),
|
||||
windowCenter: remoteGetValue(instance['0028,1050']),
|
||||
windowWidth: remoteGetValue(instance['0028,1051']),
|
||||
rescaleIntercept: parseFloat(remoteGetValue(instance['0028,1052'])),
|
||||
rescaleSlope: parseFloat(remoteGetValue(instance['0028,1053'])),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: remoteGetValue(instance['0020,0062']),
|
||||
viewPosition: remoteGetValue(instance['0018,5101']),
|
||||
acquisitionDatetime: remoteGetValue(instance['0008,002A']),
|
||||
numFrames: parseFloat(remoteGetValue(instance['0028,0008'])),
|
||||
frameIncrementPointer: remoteGetValue(instance['0028,0009']),
|
||||
frameTime: parseFloat(remoteGetValue(instance['0018,1063'])),
|
||||
frameTimeVector: parseFloatArray(remoteGetValue(instance['0018,1065']))
|
||||
};
|
||||
|
||||
var iid = instance['xxxx,0001'].Value;
|
||||
if (server.imageRendering === 'wadouri') {
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
} else if (server.imageRendering == 'orthanc') {
|
||||
instanceSummary.wadouri = server.root + '/instances/' + iid + '/file';
|
||||
} else {
|
||||
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
console.log(studyData.seriesList[0].instances);
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using a WADO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.REMOTE.RetrieveMetadata = function(server, studyInstanceUid) {
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
|
||||
var study = resultDataToStudyMetadata(server, studyInstanceUid, remote.retrieveMetadata(server.modality, studyInstanceUid));
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
study.wadoUriRoot = server.wadoUriRoot;
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return study;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { remoteGetValue } from '../../lib/remoteGetValue';
|
||||
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
resultData.forEach(function(study) {
|
||||
studies.push({
|
||||
studyInstanceUid: remoteGetValue(study['0020,000d']),
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: remoteGetValue(study['0008,0020']),
|
||||
studyTime: remoteGetValue(study['0008,0030']),
|
||||
accessionNumber: remoteGetValue(study['0008,0050']),
|
||||
referringPhysicianName: remoteGetValue(study['0008,0090']),
|
||||
// 00081190 = URL
|
||||
patientName: remoteGetValue(study['0010,0010']),
|
||||
patientId: remoteGetValue(study['0010,0020']),
|
||||
patientBirthdate: remoteGetValue(study['0010,0030']),
|
||||
patientSex: remoteGetValue(study['0010,0040']),
|
||||
studyId: remoteGetValue(study['0020,0010']),
|
||||
numberOfStudyRelatedSeries: parseFloat(remoteGetValue(study['0020,1206'])),
|
||||
numberOfStudyRelatedInstances: parseFloat(remoteGetValue(study['0020,1208'])),
|
||||
studyDescription: remoteGetValue(study['0008,1030']),
|
||||
modalities: remoteGetValue(study['0008,0061'])
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.REMOTE.Studies = function(server, filter) {
|
||||
var parameters = {
|
||||
PatientName: filter.patientName ? filter.patientName : "",
|
||||
PatientID: filter.patientId,
|
||||
AccessionNumber: filter.accessionNumber ? filter.accessionNumber : "",
|
||||
StudyDescription: "",
|
||||
StudyDate : "",
|
||||
StudyTime : "",
|
||||
ReferringPhysicianName : "",
|
||||
PatientBirthDate : "",
|
||||
PatientSex : "",
|
||||
StudyID : "",
|
||||
NumberOfStudyRelatedSeries : "",
|
||||
NumberOfStudyRelatedInstances : "",
|
||||
ModalitiesInStudy : ""
|
||||
};
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
var data = remote.findStudies(server.modality, parameters);
|
||||
|
||||
return resultDataToStudies(data.results);
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import { parseFloatArray } from '../../lib/parseFloatArray';
|
||||
|
||||
/**
|
||||
* Creates a URL for a WADO search
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
return server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SourceImageSequence, if it exists, in order
|
||||
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||
*
|
||||
* @param instance
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getSourceImageInstanceUid(instance) {
|
||||
// TODO= Parse the whole Source Image Sequence
|
||||
// This is a really poor workaround for now.
|
||||
// Later we should probably parse the whole sequence.
|
||||
var SourceImageSequence = instance['00082112'];
|
||||
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
|
||||
return SourceImageSequence.Value[0]['00081155'].Value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a WADO search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0];
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: DICOMWeb.getName(anInstance['00100010']),
|
||||
patientId: DICOMWeb.getString(anInstance['00100020']),
|
||||
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
|
||||
studyDate: DICOMWeb.getString(anInstance['00080020']),
|
||||
modalities: DICOMWeb.getString(anInstance['00080061']),
|
||||
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
||||
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
||||
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
|
||||
institutionName: DICOMWeb.getString(anInstance['00080080'])
|
||||
};
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: DICOMWeb.getString(instance['00080008']),
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: DICOMWeb.getString(instance['00200062']),
|
||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||
acquisitionDatetime: DICOMWeb.getString(instance['0008002A']),
|
||||
numFrames: DICOMWeb.getNumber(instance['00280008']),
|
||||
frameIncrementPointer: DICOMWeb.getAttribute(instance['00280009']),
|
||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
||||
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
|
||||
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
||||
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
||||
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
||||
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
||||
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
||||
};
|
||||
|
||||
if (server.imageRendering === 'wadouri') {
|
||||
instanceSummary.wadouri = WADOProxy.convertURL(server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom', server.requestOptions);
|
||||
} else {
|
||||
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using a WADO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.WADO.RetrieveMetadata = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
var study = resultDataToStudyMetadata(server, studyInstanceUid, result.data);
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
study.wadoUriRoot = server.wadoUriRoot;
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return study;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import { ServerConfiguration } from '../both/schema/servers.js';
|
||||
|
||||
/*
|
||||
-- Taking this out for now to prevent confusion.
|
||||
|
||||
TODO: Make the error messages more clear
|
||||
Meteor.startup(() => {
|
||||
console.log('------ Testing Meteor Settings ------');
|
||||
let config = ServerConfiguration.clean(Meteor.settings);
|
||||
console.log(JSON.stringify(config, null, 2));
|
||||
|
||||
Meteor.settings = config;
|
||||
check(config, ServerConfiguration);
|
||||
});*/
|
||||
Reference in new issue
Block a user