chore(dicomweb-client): Switch to dicomweb-client.js to push logic out of OHIF

This commit is contained in:
Erik Ziegler 2018-09-23 10:57:21 +02:00
parent 45c5867663
commit 89d97a7a3a
58 changed files with 256 additions and 542 deletions

View File

@ -92,7 +92,6 @@ ohif:core@0.0.1
ohif:cornerstone@0.0.1
ohif:cornerstone-settings@0.0.1
ohif:design@0.0.1
ohif:dicomweb-client@0.0.1
ohif:hanging-protocols@0.0.1
ohif:header@0.0.1
ohif:hotkeys@0.0.1

View File

@ -37,7 +37,6 @@ ohif:cornerstone
ohif:cornerstone-settings
ohif:viewerbase
ohif:study-list
ohif:dicomweb-client
ohif:hanging-protocols
ohif:metadata
ohif:user-oidc

View File

@ -82,7 +82,6 @@ ohif:core@0.0.1
ohif:cornerstone@0.0.1
ohif:cornerstone-settings@0.0.1
ohif:design@0.0.1
ohif:dicomweb-client@0.0.1
ohif:hanging-protocols@0.0.1
ohif:header@0.0.1
ohif:hotkeys@0.0.1

View File

@ -22,10 +22,10 @@ Meteor.startup(function() {
cornerstoneWADOImageLoader.configure({
beforeSend: function(xhr) {
const accessToken = OHIF.user.getAccessToken();
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
if (accessToken) {
xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`);
if (headers.Authorization) {
xhr.setRequestHeader("Authorization", headers.Authorization);
}
}
});

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
// Meteor packages
api.use([

View File

@ -5,7 +5,7 @@
* @param [defaultValue] - The value to return if the element is not present
* @returns {*}
*/
const getAttribute = function(element, defaultValue) {
export default function getAttribute(element, defaultValue) {
if (!element) {
return defaultValue;
}
@ -42,5 +42,3 @@ function convertToInt(input) {
return parseInt(output, 16);
}
export default getAttribute;

View File

@ -0,0 +1,26 @@
import { OHIF } from 'meteor/ohif:core';
import { btoa } from 'isomorphic-base64';
/**
* Returns the Authorization header as part of an Object.
*
* @returns {Object}
*/
export default function getAuthorizationHeader() {
const headers = {};
// Check for OHIF.user since this can also be run on the server
const accessToken = OHIF.user && OHIF.user.getAccessToken && OHIF.user.getAccessToken();
const server = OHIF.servers.getCurrentServer();
if (server &&
server.requestOptions &&
server.requestOptions.auth) {
// HTTP Basic Auth (user:password)
headers.Authorization = `Basic ${btoa(server.requestOptions.auth)}`;
} else if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
return headers;
}

View File

@ -1,4 +1,4 @@
const getModalities = function(modality, modalitiesInStudy) {
export default function getModalities(modality, modalitiesInStudy) {
var modalities = {};
if (modality) {
modalities = modality;
@ -19,5 +19,3 @@ const getModalities = function(modality, modalitiesInStudy) {
}
return modalities;
};
export default getModalities;

View File

@ -1,25 +1,19 @@
import get from './get.js';
import { OHIF } from 'meteor/ohif:core';
import getAttribute from './getAttribute.js';
import getBulkData from './getBulkData.js';
import getJSON from './getJSON.js';
import getAuthorizationHeader from './getAuthorizationHeader.js';
import getModalities from './getModalities.js';
import getName from './getName.js';
import getNumber from './getNumber.js';
import getString from './getString.js';
import getAccessToken from './getAccessToken.js';
import makeRequest from './makeRequest.js';
const DICOMWeb = {
get,
getAttribute,
getBulkData,
getJSON,
getAuthorizationHeader,
getModalities,
getName,
getNumber,
getString,
getAccessToken,
makeRequest
};
export { DICOMWeb };
OHIF.DICOMWeb = DICOMWeb;

View File

@ -1 +1,2 @@
import './object.js';
import './DICOMWeb/';

View File

@ -16,4 +16,4 @@ OHIF.utils.absoluteUrl = function(path) {
}
return absolutePath.replace(/\/\/+/g, '/');
};
};

View File

@ -11,6 +11,7 @@ const OHIF = {
viewer: {},
cornerstone: {},
user: {},
DICOMWeb: {}, // Temporarily added
};
// Expose the OHIF object to the client if it is on development mode

View File

@ -1,5 +1,6 @@
Npm.depends({
'twbs-pagination': '1.4.1'
'twbs-pagination': '1.4.1',
'isomorphic-base64': '1.0.2',
});
Package.describe({
@ -9,7 +10,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('standard-app-packages');
@ -44,5 +45,4 @@ Package.onUse(function(api) {
// Client and server imports
api.addFiles('both/index.js', ['client', 'server']);
});

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.5');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('ohif:cornerstone');

View File

@ -15,7 +15,7 @@ Npm.depends({
});
Package.onUse(function(api) {
api.versionsFrom('1.5');
api.versionsFrom('1.7');
api.use('ecmascript');

View File

@ -16,7 +16,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4.2.3');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('standard-app-packages');

View File

@ -1,22 +0,0 @@
Package.describe({
name: 'ohif:dicomweb-client',
summary: 'DICOM Services: DICOMWeb',
version: '0.0.1'
});
Npm.depends({
'url-parse': '1.4.1',
'isomorphic-fetch': '2.2.1',
'isomorphic-base64': '1.0.2'
});
Package.onUse(function(api) {
api.versionsFrom('1.6');
api.use('ecmascript');
// DICOMWeb functions
api.mainModule('src/index.js');
api.export('DICOMWeb');
});

View File

@ -1,33 +0,0 @@
import makeRequest from "./makeRequest";
/**
* Make a request to the URL given a set of options.
* This function will add optional timing, request, and respone logging.
*
* @param {String} url
* @param {Object} options
* @return {Promise<*>}
*/
const get = async function get(url, options) {
if (options.logRequests) {
console.log(url);
}
if (options.logTiming) {
console.time(url);
}
const result = await makeRequest(url, options);
if (options.logTiming) {
console.timeEnd(url);
}
if (options.logResponses) {
console.info(result);
}
return result;
}
export default get;

View File

@ -1,14 +0,0 @@
import { OHIF } from 'meteor/ohif:core';
export default function getAccessToken() {
if (!global.window || !window.sessionStorage || !sessionStorage) {
return;
}
const userAccessToken = OHIF.user.getAccessToken();
if (userAccessToken) {
return userAccessToken;
}
return sessionStorage.token;
}

View File

@ -1,133 +0,0 @@
import get from './get.js';
const ASCII = 'ascii';
function getMultipartContentInfo(headers) {
const multipartRegex = /^\s*multipart\/[^;]+/g;
const contentType = headers.get('content-type');
let dict = null;
if (typeof contentType === 'string' && multipartRegex.test(contentType)) {
let match;
let pairRegex = /;\s*([^=]+)=([^;\r\n]+)/g;
pairRegex.lastIndex = multipartRegex.lastIndex;
while ((match = pairRegex.exec(contentType)) !== null) {
let key = match[1]
let value = match[2];
if (dict === null) {
dict = {};
}
dict[key] = value;
}
}
return dict;
}
function parseContentHeader(data, offset) {
const endOfHeader = data.indexOf('\r\n\r\n', offset);
if (endOfHeader < 0 || endOfHeader <= offset || endOfHeader > data.length) {
throw new Error('End of content header cannot be determined...');
}
const header = {
start: offset,
end: endOfHeader + 4,
fields: {}
};
const headerRegex = /([\w\-]+):\s*([^\r\n]+)(?:\r\n)?/g;
const headerContentRegex = /\t([^\r\n]+)(?:\r\n)?/g;
const headerString = data.slice(offset, endOfHeader);
let match;
while ((match = headerRegex.exec(headerString)) !== null) {
const key = match[1].toLowerCase(), value = match[2];
while (headerString.charAt(headerRegex.lastIndex) === '\t') {
headerContentRegex.lastIndex = headerRegex.lastIndex;
if ((match = headerContentRegex.exec(headerString)) !== null) {
headerRegex.lastIndex = headerContentRegex.lastIndex;
value += ' ' + match[1];
continue;
}
break;
}
header.fields[key] = value;
}
return header;
}
function toBytes(input) {
const data = [];
for (let i = 0; i < input.length; i++){
data.push(input.charCodeAt(i));
}
return data;
}
function parseResponse(headers, data) {
const contentInfo = getMultipartContentInfo(headers);
if (!contentInfo || !contentInfo.boundary) {
throw new Error('Content boundary not specified...');
}
const boundary = contentInfo.boundary;
const delimiter = '--' + boundary + '\r\n';
let index = data.indexOf(delimiter, 0, ASCII);
if (index < 0) {
throw new Error('DICOMWeb: The specified boundary could not be found...');
}
let closingIndex
let closingDelimiter = '\r\n--' + boundary + '--';
index += delimiter.length;
if (data[index] !== 0x0D || data[index + 1] !== 0x0A) {
// multipart content headers are present, so let's parse them...
let contentHeader = parseContentHeader(data, index);
if (contentHeader && contentHeader.end > index) {
if (contentHeader.fields['content-length'] > 0) {
let endOfData = contentHeader.end + parseInt(contentHeader.fields['content-length'], 10);
if (endOfData === data.indexOf(closingDelimiter, endOfData)) {
// content-length is valid...
return data.slice(contentHeader.end, endOfData);
}
}
index = contentHeader.end;
}
} else {
// no headers, the content comes right after...
index += 2;
}
closingIndex = data.indexOf(closingDelimiter, index);
if (closingIndex < 0 || closingIndex < index) {
throw new Error('DICOMWeb: The end of the content could not be determined...')
}
return toBytes(data.slice(index, closingIndex));
}
const getBulkData = async function(url, options = {}) {
options.headers = options.headers || {};
options.headers.Accept = 'multipart/related; type=application/octet-stream';
const response = await get(url, options);
const data = await response.text();
return parseResponse(response.headers, data);
}
export default getBulkData;

View File

@ -1,16 +0,0 @@
import get from './get.js';
import getAttribute from "./getAttribute";
const getJSON = async function (url, options) {
options.headers = options.headers || {};
options.headers.Accept = 'application/json';
return get(url, options).then(response => {
if (!(response instanceof Response)) {
return '';
}
return response.json();
});
};
export default getJSON;

View File

@ -1,69 +0,0 @@
import { Meteor } from "meteor/meteor";
import URL from 'url-parse';
import 'isomorphic-fetch';
import { btoa } from 'isomorphic-base64';
import getAccessToken from './getAccessToken.js';
async function makeRequest(url, options) {
const parsed = new URL(url);
let requestOpt = {
method: 'GET',
headers: {}
};
const accessToken = getAccessToken();
if (accessToken) {
requestOpt.headers = {
Authorization: `Bearer ${accessToken}`
};
} else if (options.auth) {
requestOpt.headers = {
Authorization: `Basic ${btoa(options.auth)}`
};
}
if (options.headers) {
Object.keys(options.headers).forEach(key => {
requestOpt.headers[key] = options.headers[key];
});
}
if(options.method) {
requestOpt.method = options.method;
}
if (options.method === 'POST' && options.body) {
requestOpt.body = options.body;
}
return new Promise((resolve, reject) => {
// TODO: Kept getting weird build errors from RegExp
const isAbsolute = parsed.href.indexOf('http://') === 0 || parsed.href.indexOf('https://') === 0;
let url = parsed.href;
if (isAbsolute === false) {
url = Meteor.absoluteUrl(parsed.href);
}
fetch(url, requestOpt).then((response) => {
if (response.status >= 400) {
reject(new Error(response.status));
}
if (response.status === 204) {
resolve([]);
} else {
// TODO: Handle 204 no content
resolve(response);
}
}, (error) => {
console.error('There was an error in the DICOMWeb Server');
reject(new Error(error));
});
});
}
export default makeRequest;

View File

@ -9,7 +9,7 @@ Npm.depends({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('standard-app-packages');

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4.2.3');
api.versionsFrom('1.7');
// Meteor packages
api.use('ecmascript');

View File

@ -9,7 +9,7 @@ Npm.depends({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
// Meteor packages
api.use([

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('standard-app-packages');

View File

@ -9,7 +9,7 @@ Npm.depends({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('standard-app-packages');

View File

@ -18,7 +18,7 @@ export const retrieveMeasurements = (patientId, timepointIds) => {
export const storeMeasurements = (measurementData, timepointIds) => {
OHIF.log.info('storeMeasurements');
const server = OHIF.servers.getCurrentServer();
if (!server || server.type !== 'dicomWeb') {
return Promise.resolve({});
@ -28,6 +28,8 @@ export const storeMeasurements = (measurementData, timepointIds) => {
return stowSRFromMeasurements(measurementData).then( () => {
OHIF.studies.deleteStudyMetadataPromise(studyInstanceUid);
}, error => {
throw new Error(error);
});
};

View File

@ -1,60 +1,51 @@
import { dcmjs } from 'meteor/ohif:cornerstone';
import retrieveDataFromSR from './retrieveDataFromSR';
import retrieveDataFromMeasurements from './retrieveDataFromMeasurements';
import {
multipartEncode
} from './srUtils';
import DICOMwebClient from 'dicomweb-client';
const retrieveMeasurementFromSR = async (series) => {
const instance = series.getFirstInstance();
const options = {
method: 'GET',
responseType: 'arraybuffer',
};
const url = instance.getDataProperty('wadouri');
const server = OHIF.servers.getCurrentServer();
const url = WADOProxy.convertURL(server.wadoRoot, server);
try {
const result = await DICOMWeb.makeRequest(url, options);
const data = await result.arrayBuffer();
const measurementData = retrieveDataFromSR(data);
return Promise.resolve(measurementData);
} catch(error) {
return Promise.reject(error);
}
const config = {
url,
headers: OHIF.DICOMWeb.getAuthorizationHeader()
};
const dicomWeb = new DICOMwebClient.api.DICOMwebClient(config);
const instance = series.getFirstInstance();
const options = {
studyInstanceUID: instance.getStudyInstanceUID(),
seriesInstanceUID: instance.getSeriesInstanceUID(),
sopInstanceUID: instance.getSOPInstanceUID(),
};
return dicomWeb.retrieveInstance(options).then(retrieveDataFromSR);
};
/**
* Retrieve the DICOMWeb STOW-RS URL for the current server.
*
* See ftp://medical.nema.org/medical/dicom/current/output/html/part18.html#sect_6.6.1.1
*
* @return {string|null} The URL, rerouted to the proxy if the WADOProxy is enabled.
*/
function getStowRsUrl() {
const server = OHIF.servers.getCurrentServer();
const url = `${server.wadoRoot}/studies`;
return WADOProxy.convertURL(url, server);
}
const stowSRFromMeasurements = async (measurements) => {
const serverUrl = getStowRsUrl();
const server = OHIF.servers.getCurrentServer();
const url = WADOProxy.convertURL(server.wadoRoot, server);
const reportDataset = retrieveDataFromMeasurements(measurements);
const boundary = dcmjs.data.DicomMetaDictionary.uid();
const options = {
method: 'POST',
body: multipartEncode(reportDataset, boundary),
headers: {
'Content-Type': `multipart/related; type=application/dicom; boundary=${boundary}`
}
const denaturalizedMetaheader = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset._meta);
const dicomDict = new dcmjs.data.DicomDict(denaturalizedMetaheader);
dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset);
const part10Buffer = dicomDict.write();
const config = {
url,
headers: OHIF.DICOMWeb.getAuthorizationHeader()
};
try {
await DICOMWeb.makeRequest(serverUrl, options);
return Promise.resolve();
} catch(error) {
return Promise.reject(error);
}
const dicomWeb = new DICOMwebClient.api.DICOMwebClient(config);
const options = {
datasets: [part10Buffer]
};
return dicomWeb.storeInstances(options);
};
export {

View File

@ -55,7 +55,7 @@ const getLatestSRSeries = () => {
if (supportedSopClassUIDs.includes(sopClassUid)) {
if(!latestSeries) {
latestSeries = series;
} else if (series._data.seriesDate > latestSeries._data.seriesDate ||
} else if (series._data.seriesDate > latestSeries._data.seriesDate ||
(series._data.seriesDate === latestSeries._data.seriesDate && series._data.seriesTime > latestSeries._data.seriesTime)) {
latestSeries = series;
}
@ -70,36 +70,10 @@ const stringToArray = (string) => {
return Uint8Array.from(Array.from(string).map(letter => letter.charCodeAt(0)))
};
const multipartEncode = (dataset, boundary) => {
const denaturalizedMetaheader = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset._meta);
const dicomDict = new dcmjs.data.DicomDict(denaturalizedMetaheader);
dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset);
const part10Buffer = dicomDict.write();
const header = `\r\n--${boundary}\r\nContent-Type: application/dicom\r\n\r\n`;
const footer = `\r\n--${boundary}--`;
headerArray = stringToArray(header);
contentArray = new Uint8Array(part10Buffer);
footerArray = stringToArray(footer);
const multipartArray = new Uint8Array(headerArray.length + contentArray.length + footerArray.length);
multipartArray.set(headerArray, 0);
multipartArray.set(contentArray, headerArray.length);
multipartArray.set(footerArray, headerArray.length + contentArray.length);
return(multipartArray.buffer);
};
export {
codeMeaningEquals,
getAllDisplaySets,
getInstanceMetadata,
getLatestSRSeries,
multipartEncode,
toArray
}
}

View File

@ -4,8 +4,13 @@ Package.describe({
version: '0.0.1'
});
Npm.depends({
'dicomweb-client': '0.3.2',
'xhr2': '0.1.4'
});
Package.onUse(function(api) {
api.versionsFrom('1.6');
api.versionsFrom('1.7');
api.use('ecmascript');

View File

@ -11,7 +11,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('standard-app-packages');

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
// Meteor packages
api.use('ecmascript');

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
// Meteor packages
api.use([

View File

@ -64,15 +64,15 @@ export const DICOMWebServer = new SimpleSchema({
imageRendering: {
type: String,
label: 'Image rendering',
allowedValues: ['wadouri', 'wadors', 'orthanc'],
valuesLabels: ['WADO URI', 'WADO RS', 'ORTHANC'],
allowedValues: ['wadouri', 'wadors'],
valuesLabels: ['WADO URI', 'WADO RS'],
defaultValue: 'wadouri'
},
thumbnailRendering: {
type: String,
label: 'Thumbnail rendering',
allowedValues: ['wadouri', 'wadors', 'orthanc'],
valuesLabels: ['WADO URI', 'WADO RS', 'ORTHANC'],
allowedValues: ['wadouri', 'wadors'],
valuesLabels: ['WADO URI', 'WADO RS'],
defaultValue: 'wadouri'
},
qidoRoot: {

View File

@ -14,7 +14,6 @@ if (Meteor.settings &&
const endpoints = servers[serverType];
endpoints.forEach((endpoint) => {
const server = Object.assign({}, endpoint);
server.origin = 'json';
server.type = serverType;
Servers.insert(server);

View File

@ -5,14 +5,14 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('templating');
api.use('jquery');
api.use('stylus');
api.use('aldeed:simple-schema');
api.use('aldeed:collection2');
api.use('templating', 'client');
api.use('jquery', 'client');
api.use('stylus', 'client');
api.use('aldeed:simple-schema@1.5.4');
api.use('aldeed:collection2@2.10.0');
// Our custom packages
api.use('ohif:core');

View File

@ -2,7 +2,6 @@ import { Meteor } from 'meteor/meteor';
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
import { Servers } from 'meteor/ohif:servers/both/collections';
import { ServerConfiguration } from 'meteor/ohif:servers/both/schema/servers.js';
// Check the servers on meteor startup
if (Meteor.settings &&

View File

@ -1,18 +0,0 @@
/**
* 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('&');
};

View File

@ -1,2 +1 @@
import './encodeQueryData.js';
import './parseFloatArray.js';

View File

@ -1,15 +1,7 @@
import { OHIF } from 'meteor/ohif:core';
import { DICOMWeb } from 'meteor/ohif:dicomweb-client';
import DICOMwebClient from 'dicomweb-client';
/**
* 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';
}
const { DICOMWeb } = OHIF;
/**
* Parses data returned from a QIDO search and transforms it into
@ -76,20 +68,23 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
*/
OHIF.studies.services.QIDO.Instances = function(server, studyInstanceUid) {
var url = buildUrl(server, studyInstanceUid);
// TODO: Are we using this function anywhere?? Can we remove it?
try {
var result = DICOMWeb.getJSON(url, server.requestOptions);
const config = {
url: server.qidoRoot,
headers: OHIF.DICOMWeb.getAuthorizationHeader()
};
const dicomWeb = new DICOMwebClient.api.DICOMwebClient(config);
const queryParams = getQIDOQueryParams(filter, server.qidoSupportsIncludeField);
const options = {
studyInstanceUID: studyInstanceUid
};
return dicomWeb.searchForInstances(options).then(result => {
return {
wadoUriRoot: server.wadoUriRoot,
studyInstanceUid: studyInstanceUid,
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
};
} catch (error) {
OHIF.log.trace();
throw error;
}
});
};

View File

@ -1,5 +1,14 @@
import { OHIF } from 'meteor/ohif:core';
import { DICOMWeb } from 'meteor/ohif:dicomweb-client';
import DICOMwebClient from 'dicomweb-client';
const { DICOMWeb } = OHIF;
// TODO: Is there an easier way to do this?
if (Meteor.isServer) {
var XMLHttpRequest = require('xhr2');
global.XMLHttpRequest = XMLHttpRequest;
}
/**
* Creates a QIDO date string for a date range query
@ -27,7 +36,7 @@ function dateToString(date) {
* @param filter
* @returns {string} The URL with encoded filter query data
*/
function filterToQIDOURL(server, filter) {
function getQIDOQueryParams(filter, serverSupportsQIDOIncludeField) {
const commaSeparatedFields = [
'00081030', // Study Description
'00080060' //Modality
@ -41,7 +50,7 @@ function filterToQIDOURL(server, filter) {
StudyDescription: filter.studyDescription,
ModalitiesInStudy: filter.modalitiesInStudy,
limit: filter.limit,
includefield: server.qidoSupportsIncludeField ? 'all' : commaSeparatedFields
includefield: serverSupportsQIDOIncludeField ? commaSeparatedFields : 'all'
};
// build the StudyDate range parameter
@ -59,7 +68,16 @@ function filterToQIDOURL(server, filter) {
parameters.StudyInstanceUID = studyUids;
}
return server.qidoRoot + '/studies?' + encodeQueryData(parameters);
// 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;
}
/**
@ -98,13 +116,16 @@ function resultDataToStudies(resultData) {
}
OHIF.studies.services.QIDO.Studies = (server, filter) => {
const url = filterToQIDOURL(server, filter);
const config = {
url: server.qidoRoot,
headers: OHIF.DICOMWeb.getAuthorizationHeader()
};
return new Promise((resolve, reject) => {
DICOMWeb.getJSON(url, server.requestOptions).then(result => {
const studies = resultDataToStudies(result);
const dicomWeb = new DICOMwebClient.api.DICOMwebClient(config);
const queryParams = getQIDOQueryParams(filter, server.qidoSupportsIncludeField);
const options = {
queryParams
};
resolve(studies);
}, reject);
});
return dicomWeb.searchForStudies(options).then(resultDataToStudies);
};

View File

@ -1,7 +1,10 @@
import { OHIF } from 'meteor/ohif:core';
import { DICOMWeb } from 'meteor/ohif:dicomweb-client';
import DICOMwebClient from 'dicomweb-client';
import { parseFloatArray } from '../../lib/parseFloatArray';
const { DICOMWeb } = OHIF;
/**
* Simple cache schema for retrieved color palettes.
*/
@ -39,17 +42,6 @@ const paletteColorCache = {
}
};
/**
* Creates a URL for a WADO search
*
* @param server
* @param studyInstanceUid
* @returns {string}
*/
function buildUrl(server, studyInstanceUid) {
return server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
}
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
@ -58,6 +50,7 @@ function buildUrl(server, studyInstanceUid) {
* @returns {string}
*/
function buildInstanceWadoUrl(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
@ -102,23 +95,47 @@ function getSourceImageInstanceUid(instance) {
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const lut = [];
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
const uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server)
const bulkDataPromise = DICOMWeb.getBulkData(uri);
return bulkDataPromise.then(data => {
for (var i = 0; i < numLutEntries; i++) {
if(bits === 16) {
lut[i] = data[i * 65536] + data[i + 1];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server)
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 &&
!uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: OHIF.DICOMWeb.getAuthorizationHeader()
};
const dicomWeb = new DICOMwebClient.api.DICOMwebClient(config);
const options = {
BulkDataURI: uri
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + (byteArray[position + 1] * 256);
}
const arrayBufferToPaletteColorLUT = (arraybuffer) =>{
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = data[i];
lut[i] = byteArray[i];
}
}
return lut;
})
}
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT)
}
/**
@ -129,13 +146,15 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let entry = null;
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
return resolve(entry);
const entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
@ -146,7 +165,11 @@ async function getPaletteColors(server, instance, lutDescriptor) {
const promises = [r, g, b];
Promise.all(promises).then((args) => {
entry = { red: r, green: g, blue: b };
entry = {
red: args[0],
green: args[1],
blue: args[2]
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
@ -202,9 +225,6 @@ function getRadiopharmaceuticalInfo(instance) {
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
const seriesMap = {};
const seriesList = [];
if (!resultData.length) {
return;
}
@ -215,7 +235,9 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
}
const studyData = {
seriesList,
seriesList: [],
studyInstanceUid,
wadoUriRoot: server.wadoUriRoot,
patientName: DICOMWeb.getName(anInstance['00100010']),
patientId: DICOMWeb.getString(anInstance['00100020']),
patientAge: DICOMWeb.getNumber(anInstance['00101010']),
@ -230,9 +252,12 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
institutionName: DICOMWeb.getString(anInstance['00080080'])
};
const seriesMap = {};
await Promise.all(resultData.map(async function(instance) {
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
var series = seriesMap[seriesInstanceUid];
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesDescription: DICOMWeb.getString(instance['0008103E']),
@ -244,7 +269,7 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
instances: []
};
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
studyData.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
@ -331,22 +356,23 @@ async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
}
/**
* Retrieved Study MetaData from a DICOM server using a WADO call
* Retrieve Study MetaData from a DICOM server using a WADO call
*
* @param server
* @param studyInstanceUid
* @returns {Promise}
*/
OHIF.studies.services.WADO.RetrieveMetadata = async function(server, studyInstanceUid) {
const url = buildUrl(server, studyInstanceUid);
const config = {
url: server.wadoRoot,
headers: OHIF.DICOMWeb.getAuthorizationHeader()
};
const dicomWeb = new DICOMwebClient.api.DICOMwebClient(config);
const options = {
studyInstanceUID: studyInstanceUid
};
return new Promise((resolve, reject) => {
DICOMWeb.getJSON(url, server.requestOptions).then(result => {
resultDataToStudyMetadata(server, studyInstanceUid, result).then((study) => {
study.wadoUriRoot = server.wadoUriRoot;
study.studyInstanceUid = studyInstanceUid;
resolve(study);
}, reject);
}, reject);
return dicomWeb.retrieveStudyMetadata(options).then(result => {
return resultDataToStudyMetadata(server, studyInstanceUid, result);
});
};

View File

@ -5,11 +5,13 @@ Package.describe({
});
Npm.depends({
dimse: '0.0.2'
})
dimse: '0.0.2',
'dicomweb-client': '0.3.2',
'xhr2': '0.1.4'
});
Package.onUse(function(api) {
api.versionsFrom('1.6');
api.versionsFrom('1.7');
api.use([
'ecmascript',
@ -24,7 +26,6 @@ Package.onUse(function(api) {
'ohif:core',
'ohif:log',
'ohif:servers',
'ohif:dicomweb-client',
'ohif:viewerbase',
'ohif:wadoproxy'
]);

View File

@ -15,7 +15,6 @@ Template.studylistResult.helpers({
*/
studies() {
const instance = Template.instance();
let studies;
let sortOption = {
patientName: 1,
studyDate: 1
@ -32,7 +31,7 @@ Template.studylistResult.helpers({
const offset = rowsPerPage * currentPage;
const limit = offset + rowsPerPage;
studies = OHIF.studylist.collections.Studies.find({}, {
const studies = OHIF.studylist.collections.Studies.find({}, {
sort: sortOption
}).fetch();
@ -90,17 +89,6 @@ function getFilter(filter) {
return filter;
}
/**
* Search for a value in a string
*/
function isIndexOf(mainVal, searchVal) {
if (mainVal === undefined || mainVal === '' || mainVal.indexOf(searchVal) > -1){
return true;
}
return false;
}
/**
* Replace object if undefined
*/
@ -167,10 +155,21 @@ function search() {
// Loop through all identified studies
studies.forEach(study => {
// TODO: Why is this Modality filter different from QIDO?
if (modality !== "" && study.modalities.includes(modality)) {
return;
}
// Sometimes DICOM studies have incorrect Date entries with
// periods such as '1990.10.04'
let studyDate = study.studyDate;
if (studyDate && studyDate.includes('.')) {
studyDate = studyDate.replace('.', '');
}
// Search the rest of the parameters that aren't done via the server call
if (isIndexOf(study.modalities, modality) &&
(new Date(studyDateFrom).setHours(0, 0, 0, 0) <= convertStringToStudyDate(study.studyDate) || !studyDateFrom || studyDateFrom === '') &&
(convertStringToStudyDate(study.studyDate) <= new Date(studyDateTo).setHours(0, 0, 0, 0) || !studyDateTo || studyDateTo === '')) {
if ((new Date(studyDateFrom).setHours(0, 0, 0, 0) <= studyDate || !studyDateFrom ) &&
(studyDate <= new Date(studyDateTo).setHours(0, 0, 0, 0) || !studyDateTo || studyDateTo === '')) {
// Convert numberOfStudyRelatedInstance string into integer
study.numberOfStudyRelatedInstances = !isNaN(study.numberOfStudyRelatedInstances) ? parseInt(study.numberOfStudyRelatedInstances) : undefined;

View File

@ -10,7 +10,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('standard-app-packages');

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4.2.3');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('templating');

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4.2.3');
api.versionsFrom('1.7');
api.use('stylus');

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('standard-app-packages');

View File

@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use(['ecmascript',
'standard-app-packages',
@ -218,7 +218,7 @@ Package.onUse(function(api) {
});
Package.onTest(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
/*
* Really important dependencies to the project

View File

@ -10,7 +10,7 @@ Npm.depends({
});
Package.onUse(function(api) {
api.versionsFrom('1.4');
api.versionsFrom('1.7');
api.use('ecmascript');
api.use('clinical:router@2.0.19');
@ -22,7 +22,7 @@ Package.onUse(function(api) {
api.addFiles('both/convertURL.js', ['client', 'server']);
api.addFiles('both/initialize.js', ['client', 'server']);
api.addFiles('server/routes.js', 'server');
// Global exports
api.export('WADOProxy');
});

View File

@ -27,7 +27,6 @@ ohif:cornerstone-settings
ohif:viewerbase
ohif:metadata
ohif:study-list
ohif:dicomweb-client
ohif:measurement-table
aldeed:template-extension

View File

@ -1,4 +1,3 @@
accounts-base@1.4.2
aldeed:collection2@2.10.0
aldeed:collection2-core@1.2.0
aldeed:schema-deny@1.1.0
@ -28,7 +27,6 @@ cultofcoders:persistent-session@0.4.5
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.2.0
deps@1.0.12
diff-sequence@1.1.0
@ -54,7 +52,6 @@ iron:layout@1.0.12
jquery@1.11.11
launch-screen@1.1.1
livedata@1.0.18
localstorage@1.2.0
logging@1.1.20
mdg:validation-error@0.5.1
meteor@1.9.2
@ -81,7 +78,6 @@ ohif:core@0.0.1
ohif:cornerstone@0.0.1
ohif:cornerstone-settings@0.0.1
ohif:design@0.0.1
ohif:dicomweb-client@0.0.1
ohif:hanging-protocols@0.0.1
ohif:header@0.0.1
ohif:hotkeys@0.0.1
@ -102,13 +98,11 @@ ordered-dict@1.1.0
promise@0.11.1
raix:eventemitter@0.1.3
random@1.1.0
rate-limit@1.0.9
reactive-dict@1.2.0
reactive-var@1.0.11
reload@1.2.0
retry@1.1.0
routepolicy@1.0.13
service-configuration@1.0.11
session@1.1.7
shell-server@0.3.1
silentcicero:jszip@0.0.4

View File

@ -6,7 +6,7 @@
"wadoUriRoot": "http://localhost:8042/wado",
"qidoRoot": "http://localhost:8042/dicom-web",
"wadoRoot": "http://localhost:8042/dicom-web",
"qidoSupportsIncludeField": false,
"qidoSupportsIncludeField": true,
"imageRendering": "wadouri",
"thumbnailRendering": "wadouri",
"requestOptions": {