Skip to content
Commits on Source (5)
repo: 3959d33612ccaadc0d4d707227fbed09ac35e5fe
node: 38b71a1006fd22750bb8f9ada9d2c6085bfe26de
branch: Orthanc-1.5.0
node: 870d19e7ea593126b5e7994cbc870847e09843fa
branch: Orthanc-1.5.1
latesttag: dcmtk-3.6.1
latesttagdistance: 554
changessincelatesttag: 613
latesttagdistance: 580
changessincelatesttag: 644
......@@ -195,17 +195,23 @@ namespace Orthanc
numberOfFrames_ = 1;
}
if ((bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
bitsAllocated_ != 24 && bitsAllocated_ != 32) ||
numberOfFrames_ == 0 ||
(planarConfiguration != 0 && planarConfiguration != 1))
if (bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
bitsAllocated_ != 24 && bitsAllocated_ != 32)
{
throw OrthancException(ErrorCode_NotImplemented);
throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast<std::string>(bitsAllocated_) + " bits allocated");
}
else if (numberOfFrames_ == 0)
{
throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported (no frames)");
}
else if (planarConfiguration != 0 && planarConfiguration != 1)
{
throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: planar configuration is " + boost::lexical_cast<std::string>(planarConfiguration));
}
if (samplesPerPixel_ == 0)
{
throw OrthancException(ErrorCode_NotImplemented);
throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0");
}
bytesPerValue_ = bitsAllocated_ / 8;
......
......@@ -982,6 +982,112 @@ namespace Orthanc
}
void DicomMap::FromDicomAsJson(const Json::Value& dicomAsJson)
{
Clear();
Json::Value::Members tags = dicomAsJson.getMemberNames();
for (Json::Value::Members::const_iterator
it = tags.begin(); it != tags.end(); ++it)
{
DicomTag tag(0, 0);
if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
{
throw OrthancException(ErrorCode_CorruptedFile);
}
const Json::Value& value = dicomAsJson[*it];
if (value.type() != Json::objectValue ||
!value.isMember("Type") ||
!value.isMember("Value") ||
value["Type"].type() != Json::stringValue)
{
throw OrthancException(ErrorCode_CorruptedFile);
}
if (value["Type"] == "String")
{
if (value["Value"].type() != Json::stringValue)
{
throw OrthancException(ErrorCode_CorruptedFile);
}
else
{
SetValue(tag, value["Value"].asString(), false /* not binary */);
}
}
}
}
void DicomMap::Merge(const DicomMap& other)
{
for (Map::const_iterator it = other.map_.begin();
it != other.map_.end(); ++it)
{
assert(it->second != NULL);
if (map_.find(it->first) == map_.end())
{
map_[it->first] = it->second->Clone();
}
}
}
void DicomMap::ExtractMainDicomTagsInternal(const DicomMap& other,
ResourceType level)
{
const DicomTag* tags = NULL;
size_t size = 0;
LoadMainDicomTags(tags, size, level);
assert(tags != NULL && size > 0);
for (size_t i = 0; i < size; i++)
{
Map::const_iterator found = other.map_.find(tags[i]);
if (found != other.map_.end() &&
map_.find(tags[i]) == map_.end())
{
assert(found->second != NULL);
map_[tags[i]] = found->second->Clone();
}
}
}
void DicomMap::ExtractMainDicomTags(const DicomMap& other)
{
Clear();
ExtractMainDicomTagsInternal(other, ResourceType_Patient);
ExtractMainDicomTagsInternal(other, ResourceType_Study);
ExtractMainDicomTagsInternal(other, ResourceType_Series);
ExtractMainDicomTagsInternal(other, ResourceType_Instance);
}
bool DicomMap::HasOnlyMainDicomTags() const
{
// TODO - Speed up possible by making this std::set a global variable
std::set<DicomTag> mainDicomTags;
GetMainDicomTags(mainDicomTags);
for (Map::const_iterator it = map_.begin(); it != map_.end(); ++it)
{
if (mainDicomTags.find(it->first) == mainDicomTags.end())
{
return false;
}
}
return true;
}
void DicomMap::Serialize(Json::Value& target) const
{
target = Json::objectValue;
......
......@@ -68,6 +68,9 @@ namespace Orthanc
static void GetMainDicomTagsInternal(std::set<DicomTag>& result, ResourceType level);
void ExtractMainDicomTagsInternal(const DicomMap& other,
ResourceType level);
public:
DicomMap()
{
......@@ -217,6 +220,14 @@ namespace Orthanc
bool ParseDouble(double& result,
const DicomTag& tag) const;
void FromDicomAsJson(const Json::Value& dicomAsJson);
void Merge(const DicomMap& other);
void ExtractMainDicomTags(const DicomMap& other);
bool HasOnlyMainDicomTags() const;
void Serialize(Json::Value& target) const;
void Unserialize(const Json::Value& source);
......
......@@ -163,6 +163,7 @@ namespace Orthanc
if (SystemToolbox::IsRegularFile(p.string()))
{
FilesystemHttpSender sender(p);
sender.SetContentType(SystemToolbox::AutodetectMimeType(p.string()));
output.Answer(sender); // TODO COMPRESSION
}
else if (listDirectoryContent_ &&
......
......@@ -115,8 +115,8 @@ SUPPORTED - Ubuntu 12.04.5 LTS
SUPPORTED - Ubuntu 14.04 LTS
----------------------------
SUPPORTED - Ubuntu 14.04 LTS and 16.04 LTS
------------------------------------------
# sudo apt-get install build-essential unzip cmake mercurial \
uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
......
......@@ -2,6 +2,24 @@ Pending changes in the mainline
===============================
Version 1.5.1 (2018-12-20)
==========================
General
-------
* Optimization: On C-FIND, avoid accessing the storage area whenever possible
* New configuration option:
- "StorageAccessOnFind" to rule the access to the storage area during C-FIND
Maintenance
-----------
* Removal of the "AllowFindSopClassesInStudy" old configuration option
* "/tools/create-dicom" is more tolerant wrt. invalid specific character set
Version 1.5.0 (2018-12-10)
==========================
......@@ -28,7 +46,7 @@ REST API
* New URI: "/studies/.../merge" to merge a study
* New URI: "/studies/.../split" to split a study
* POST-ing a DICOM file to "/instances" also answers the patient/study/series ID
* GET "/modalities/..." now returns a JSON object instead of a JSON array
* GET "/modalities/?expand" now returns a JSON object instead of a JSON array
* New "Details" field in HTTP answers on error (cf. "HttpDescribeErrors" option)
* New options to URI "/queries/.../answers": "?expand" and "?simplify"
* New URIs to launch new C-FIND to explore the hierarchy of a C-FIND answer:
......
......@@ -27,10 +27,10 @@ function DeepCopy(obj)
function ChangePage(page, options)
{
var first = true;
let first = true;
if (options) {
for (var key in options) {
var value = options[key];
for (let key in options) {
let value = options[key];
if (first) {
page += '?';
first = false;
......@@ -63,7 +63,7 @@ function Refresh()
$(document).ready(function() {
var $tree = $('#dicom-tree');
let $tree = $('#dicom-tree');
$tree.tree({
autoEscape: false
});
......@@ -124,7 +124,7 @@ function FormatDicomDate(s)
if (s == undefined)
return "No date";
var d = ParseDicomDate(s);
let d = ParseDicomDate(s);
if (d == null)
return '?';
else
......@@ -134,16 +134,16 @@ function FormatDicomDate(s)
function Sort(arr, fieldExtractor, isInteger, reverse)
{
var defaultValue;
let defaultValue;
if (isInteger)
defaultValue = 0;
else
defaultValue = '';
arr.sort(function(a, b) {
var ta = fieldExtractor(a);
var tb = fieldExtractor(b);
var order;
let ta = fieldExtractor(a);
let tb = fieldExtractor(b);
let order;
if (ta == undefined)
ta = defaultValue;
......@@ -226,11 +226,11 @@ function CompleteFormatting(node, link, isReverse, count)
function FormatMainDicomTags(target, tags, tagsToIgnore)
{
for (var i in tags)
for (let i in tags)
{
if (tagsToIgnore.indexOf(i) == -1)
{
var v = tags[i];
let v = tags[i];
if (i == "PatientBirthDate" ||
i == "StudyDate" ||
......@@ -254,7 +254,7 @@ function FormatMainDicomTags(target, tags, tagsToIgnore)
function FormatPatient(patient, link, isReverse)
{
var node = $('<div>').append($('<h3>').text(patient.MainDicomTags.PatientName));
let node = $('<div>').append($('<h3>').text(patient.MainDicomTags.PatientName));
FormatMainDicomTags(node, patient.MainDicomTags, [
"PatientName"
......@@ -268,7 +268,7 @@ function FormatPatient(patient, link, isReverse)
function FormatStudy(study, link, isReverse, includePatient)
{
var label;
let label;
if (includePatient) {
label = study.Label;
......@@ -276,7 +276,7 @@ function FormatStudy(study, link, isReverse, includePatient)
label = study.MainDicomTags.StudyDescription;
}
var node = $('<div>').append($('<h3>').text(label));
let node = $('<div>').append($('<h3>').text(label));
if (includePatient) {
FormatMainDicomTags(node, study.PatientMainDicomTags, [
......@@ -296,7 +296,7 @@ function FormatStudy(study, link, isReverse, includePatient)
function FormatSeries(series, link, isReverse)
{
var c;
let c;
if (series.ExpectedNumberOfInstances == null ||
series.Instances.length == series.ExpectedNumberOfInstances)
{
......@@ -307,7 +307,7 @@ function FormatSeries(series, link, isReverse)
c = series.Instances.length + '/' + series.ExpectedNumberOfInstances;
}
var node = $('<div>')
let node = $('<div>')
.append($('<h3>').text(series.MainDicomTags.SeriesDescription))
.append($('<p>').append($('<em>')
.text('Status: ')
......@@ -328,7 +328,7 @@ function FormatSeries(series, link, isReverse)
function FormatInstance(instance, link, isReverse)
{
var node = $('<div>').append($('<h3>').text('Instance: ' + instance.IndexInSeries));
let node = $('<div>').append($('<h3>').text('Instance: ' + instance.IndexInSeries));
FormatMainDicomTags(node, instance.MainDicomTags, [
"AcquisitionNumber",
......@@ -364,7 +364,7 @@ $('[data-role="page"]').live('pagebeforeshow', function() {
$('#lookup').live('pagebeforeshow', function() {
// NB: "GenerateDicomDate()" is defined in "query-retrieve.js"
var target = $('#lookup-study-date');
let target = $('#lookup-study-date');
$('option', target).remove();
target.append($('<option>').attr('value', '*').text('Any date'));
target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
......@@ -382,7 +382,7 @@ $('#lookup').live('pagebeforeshow', function() {
$('#lookup-submit').live('click', function() {
$('#lookup-result').hide();
var lookup = {
let lookup = {
'Level' : 'Study',
'Expand' : true,
'Limit' : LIMIT_RESOURCES + 1,
......@@ -432,12 +432,12 @@ $('#lookup-submit').live('click', function() {
$('#find-patients').live('pagebeforeshow', function() {
GetResource('/patients?expand&since=0&limit=' + (LIMIT_RESOURCES + 1), function(patients) {
var target = $('#all-patients');
let target = $('#all-patients');
$('li', target).remove();
SortOnDicomTag(patients, 'PatientName', false, false);
var count, showAlert;
let count, showAlert;
if (patients.length <= LIMIT_RESOURCES) {
count = patients.length;
showAlert = false;
......@@ -447,8 +447,8 @@ $('#find-patients').live('pagebeforeshow', function() {
showAlert = true;
}
for (var i = 0; i < count; i++) {
var p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID);
for (let i = 0; i < count; i++) {
let p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID);
target.append(p);
}
......@@ -467,14 +467,14 @@ $('#find-patients').live('pagebeforeshow', function() {
function FormatListOfStudies(targetId, alertId, countId, studies)
{
var target = $(targetId);
let target = $(targetId);
$('li', target).remove();
for (var i = 0; i < studies.length; i++) {
var patient = studies[i].PatientMainDicomTags.PatientName;
var study = studies[i].MainDicomTags.StudyDescription;
for (let i = 0; i < studies.length; i++) {
let patient = studies[i].PatientMainDicomTags.PatientName;
let study = studies[i].MainDicomTags.StudyDescription;
var s;
let s;
if (typeof patient === 'string') {
s = patient;
}
......@@ -493,7 +493,7 @@ function FormatListOfStudies(targetId, alertId, countId, studies)
Sort(studies, function(a) { return a.Label }, false, false);
var count, showAlert;
let count, showAlert;
if (studies.length <= LIMIT_RESOURCES) {
count = studies.length;
showAlert = false;
......@@ -503,8 +503,8 @@ function FormatListOfStudies(targetId, alertId, countId, studies)
showAlert = true;
}
for (var i = 0; i < count; i++) {
var p = FormatStudy(studies[i], '#study?uuid=' + studies[i].ID, false, true);
for (let i = 0; i < count; i++) {
let p = FormatStudy(studies[i], '#study?uuid=' + studies[i].ID, false, true);
target.append(p);
}
......@@ -547,7 +547,7 @@ function SetupAnonymizedOrModifiedFrom(buttonSelector, resource, resourceType, f
function RefreshPatient()
{
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
GetResource('/patients/' + pageData.uuid, function(patient) {
GetResource('/patients/' + pageData.uuid + '/studies', function(studies) {
......@@ -559,10 +559,10 @@ function RefreshPatient()
.append(FormatPatient(patient))
.listview('refresh');
var target = $('#list-studies');
let target = $('#list-studies');
$('li', target).remove();
for (var i = 0; i < studies.length; i++) {
for (let i = 0; i < studies.length; i++) {
if (i == 0 || studies[i].MainDicomTags.StudyDate != studies[i - 1].MainDicomTags.StudyDate)
{
target.append($('<li>')
......@@ -586,7 +586,7 @@ function RefreshPatient()
async: false,
cache: false,
success: function (s) {
var v = (s == '1') ? 'on' : 'off';
let v = (s == '1') ? 'on' : 'off';
$('#protection').val(v).slider('refresh');
}
});
......@@ -602,7 +602,7 @@ function RefreshPatient()
function RefreshStudy()
{
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
GetResource('/studies/' + pageData.uuid, function(study) {
GetResource('/patients/' + study.ParentPatient, function(patient) {
......@@ -621,9 +621,9 @@ function RefreshStudy()
SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', 'AnonymizedFrom');
SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', 'ModifiedFrom');
var target = $('#list-series');
let target = $('#list-series');
$('li', target).remove();
for (var i = 0; i < series.length; i++) {
for (let i = 0; i < series.length; i++) {
if (i == 0 || series[i].MainDicomTags.SeriesDate != series[i - 1].MainDicomTags.SeriesDate)
{
target.append($('<li>')
......@@ -647,7 +647,7 @@ function RefreshStudy()
function RefreshSeries()
{
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
GetResource('/series/' + pageData.uuid, function(series) {
GetResource('/studies/' + series.ParentStudy, function(study) {
......@@ -671,9 +671,9 @@ function RefreshSeries()
SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', 'AnonymizedFrom');
SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', 'ModifiedFrom');
var target = $('#list-instances');
let target = $('#list-instances');
$('li', target).remove();
for (var i = 0; i < instances.length; i++) {
for (let i = 0; i < instances.length; i++) {
target.append(FormatInstance(instances[i], '#instance?uuid=' + instances[i].ID));
}
target.listview('refresh');
......@@ -690,7 +690,7 @@ function RefreshSeries()
function EscapeHtml(value)
{
var ENTITY_MAP = {
let ENTITY_MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
......@@ -709,11 +709,11 @@ function EscapeHtml(value)
function ConvertForTree(dicom)
{
var result = [];
let result = [];
for (var i in dicom) {
for (let i in dicom) {
if (dicom[i] != null) {
var label = (i + '<span class="tag-name"> (<i>' +
let label = (i + '<span class="tag-name"> (<i>' +
EscapeHtml(dicom[i]["Name"]) +
'</i>)</span>: ');
......@@ -740,8 +740,8 @@ function ConvertForTree(dicom)
}
else if (dicom[i]["Type"] == 'Sequence')
{
var c = [];
for (var j = 0; j < dicom[i]["Value"].length; j++) {
let c = [];
for (let j = 0; j < dicom[i]["Value"].length; j++) {
c.push({
label: 'Item ' + j,
children: ConvertForTree(dicom[i]["Value"][j])
......@@ -763,7 +763,7 @@ function ConvertForTree(dicom)
function RefreshInstance()
{
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
GetResource('/instances/' + pageData.uuid, function(instance) {
GetResource('/series/' + instance.ParentSeries, function(series) {
......@@ -837,7 +837,7 @@ function DeleteResource(path)
dataType: 'json',
async: false,
success: function(s) {
var ancestor = s.RemainingAncestor;
let ancestor = s.RemainingAncestor;
if (ancestor == null)
$.mobile.changePage('#lookup');
else
......@@ -913,9 +913,9 @@ $('#instance-download-json').live('click', function(e) {
$('#instance-preview').live('click', function(e) {
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
var pdf = '../instances/' + pageData.uuid + '/pdf';
let pdf = '../instances/' + pageData.uuid + '/pdf';
$.ajax({
url: pdf,
cache: false,
......@@ -937,8 +937,8 @@ $('#instance-preview').live('click', function(e) {
{
// Viewing a multi-frame image
var images = [];
for (var i = 0; i < frames.length; i++) {
let images = [];
for (let i = 0; i < frames.length; i++) {
images.push([ '../instances/' + pageData.uuid + '/frames/' + i + '/preview' ]);
}
......@@ -959,14 +959,14 @@ $('#instance-preview').live('click', function(e) {
$('#series-preview').live('click', function(e) {
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
GetResource('/series/' + pageData.uuid, function(series) {
GetResource('/series/' + pageData.uuid + '/instances', function(instances) {
Sort(instances, function(x) { return x.IndexInSeries; }, true, false);
var images = [];
for (var i = 0; i < instances.length; i++) {
let images = [];
for (let i = 0; i < instances.length; i++) {
images.push([ '../instances/' + instances[i].ID + '/preview',
(i + 1).toString() + '/' + instances.length.toString() ])
}
......@@ -988,9 +988,9 @@ $('#series-preview').live('click', function(e) {
function ChooseDicomModality(callback)
{
var clickedModality = '';
var clickedPeer = '';
var items = $('<ul>')
let clickedModality = '';
let clickedPeer = '';
let items = $('<ul>')
.attr('data-divider-theme', 'd')
.attr('data-role', 'listview');
......@@ -1006,9 +1006,9 @@ function ChooseDicomModality(callback)
{
items.append('<li data-role="list-divider">DICOM modalities</li>');
for (var i = 0; i < modalities.length; i++) {
var name = modalities[i];
var item = $('<li>')
for (let i = 0; i < modalities.length; i++) {
let name = modalities[i];
let item = $('<li>')
.html('<a href="#" rel="close">' + name + '</a>')
.attr('name', name)
.click(function() {
......@@ -1030,9 +1030,9 @@ function ChooseDicomModality(callback)
{
items.append('<li data-role="list-divider">Orthanc peers</li>');
for (var i = 0; i < peers.length; i++) {
var name = peers[i];
var item = $('<li>')
for (let i = 0; i < peers.length; i++) {
let name = peers[i];
let item = $('<li>')
.html('<a href="#" rel="close">' + name + '</a>')
.attr('name', name)
.click(function() {
......@@ -1052,7 +1052,7 @@ function ChooseDicomModality(callback)
width: '100%',
blankContent: items,
callbackClose: function() {
var timer;
let timer;
function WaitForDialogToClose() {
if (!$('#dialog').is(':visible')) {
clearInterval(timer);
......@@ -1071,10 +1071,10 @@ function ChooseDicomModality(callback)
$('#instance-store,#series-store,#study-store,#patient-store').live('click', function(e) {
ChooseDicomModality(function(modality, peer) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
var url;
var loading;
let url;
let loading;
if (modality != '')
{
......@@ -1113,7 +1113,7 @@ $('#instance-store,#series-store,#study-store,#patient-store').live('click', fun
$('#show-tag-name').live('change', function(e) {
var checked = e.currentTarget.checked;
let checked = e.currentTarget.checked;
if (checked)
$('.tag-name').show();
else
......@@ -1155,7 +1155,7 @@ $('#series-media').live('click', function(e) {
$('#protection').live('change', function(e) {
var isProtected = e.target.value == "on";
let isProtected = e.target.value == "on";
$.ajax({
url: '../patients/' + $.mobile.pageData.uuid + '/protected',
type: 'PUT',
......@@ -1233,7 +1233,7 @@ $('#plugins').live('pagebeforeshow', function() {
async: false,
cache: false,
success: function(plugins) {
var target = $('#all-plugins');
let target = $('#all-plugins');
$('li', target).remove();
plugins.map(function(id) {
......@@ -1243,8 +1243,8 @@ $('#plugins').live('pagebeforeshow', function() {
async: false,
cache: false,
success: function(plugin) {
var li = $('<li>');
var item = li;
let li = $('<li>');
let item = li;
if ('RootUri' in plugin)
{
......@@ -1272,12 +1272,12 @@ $('#plugins').live('pagebeforeshow', function() {
function ParseJobTime(s)
{
var t = (s.substr(0, 4) + '-' +
let t = (s.substr(0, 4) + '-' +
s.substr(4, 2) + '-' +
s.substr(6, 5) + ':' +
s.substr(11, 2) + ':' +
s.substr(13));
var utc = new Date(t);
let utc = new Date(t);
// Convert from UTC to local time
return new Date(utc.getTime() - utc.getTimezoneOffset() * 60000);
......@@ -1311,18 +1311,18 @@ $('#jobs').live('pagebeforeshow', function() {
async: false,
cache: false,
success: function(jobs) {
var target = $('#all-jobs');
let target = $('#all-jobs');
$('li', target).remove();
var running = $('<li>')
let running = $('<li>')
.attr('data-role', 'list-divider')
.text('Currently running');
var pending = $('<li>')
let pending = $('<li>')
.attr('data-role', 'list-divider')
.text('Pending jobs');
var inactive = $('<li>')
let inactive = $('<li>')
.attr('data-role', 'list-divider')
.text('Inactive jobs');
......@@ -1331,8 +1331,8 @@ $('#jobs').live('pagebeforeshow', function() {
target.append(inactive);
jobs.map(function(job) {
var li = $('<li>');
var item = $('<a>');
let li = $('<li>');
let item = $('<a>');
li.append(item);
item.attr('href', '#job?uuid=' + job.ID);
item.append($('<h1>').text(job.Type));
......@@ -1369,7 +1369,7 @@ $('#jobs').live('pagebeforeshow', function() {
$('#job').live('pagebeforeshow', function() {
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
$.ajax({
url: '../jobs/' + pageData.uuid,
......@@ -1377,15 +1377,15 @@ $('#job').live('pagebeforeshow', function() {
async: false,
cache: false,
success: function(job) {
var target = $('#job-info');
let target = $('#job-info');
$('li', target).remove();
target.append($('<li>')
.attr('data-role', 'list-divider')
.text('General information about the job'));
var block = $('<li>');
for (var i in job) {
let block = $('<li>');
for (let i in job) {
if (i == 'CreationTime' ||
i == 'CompletionTime' ||
i == 'EstimatedTimeOfArrival') {
......@@ -1403,10 +1403,10 @@ $('#job').live('pagebeforeshow', function() {
.attr('data-role', 'list-divider')
.text('Detailed information'));
var block = $('<li>');
let block = $('<li>');
for (var item in job.Content) {
var value = job.Content[item];
for (let item in job.Content) {
let value = job.Content[item];
if (typeof value !== 'string') {
value = JSON.stringify(value);
}
......
......@@ -27,7 +27,7 @@ $(document).ready(function() {
$('#progress .label').text('Failure');
})
.bind('fileuploaddrop', function (e, data) {
var target = $('#upload-list');
let target = $('#upload-list');
$.each(data.files, function (index, file) {
target.append('<li class="pending-file">' + file.name + '</li>');
});
......@@ -36,7 +36,7 @@ $(document).ready(function() {
.bind('fileuploadsend', function (e, data) {
// Update the progress bar. Note: for some weird reason, the
// "fileuploadprogressall" does not work under Firefox.
var progress = parseInt(currentUpload / totalUploads * 100, 10);
let progress = parseInt(currentUpload / totalUploads * 100, 10);
currentUpload += 1;
$('#progress .label').text('Uploading: ' + progress + '%');
$('#progress .bar')
......@@ -62,7 +62,7 @@ $('#upload').live('pagehide', function() {
$('#upload-button').live('click', function() {
var pu = pendingUploads;
let pu = pendingUploads;
pendingUploads = [];
$('.pending-file').remove();
......@@ -77,7 +77,7 @@ $('#upload-button').live('click', function() {
//$('#upload-abort').removeClass('ui-disabled');
}
for (var i = 0; i < pu.length; i++) {
for (let i = 0; i < pu.length; i++) {
pu[i].submit();
}
});
......
function JavascriptDateToDicom(date)
{
var s = date.toISOString();
let s = date.toISOString();
return s.substring(0, 4) + s.substring(5, 7) + s.substring(8, 10);
}
function GenerateDicomDate(days)
{
var today = new Date();
var other = new Date(today);
let today = new Date();
let other = new Date(today);
other.setDate(today.getDate() + days);
return JavascriptDateToDicom(other);
}
......@@ -20,11 +20,11 @@ $('#query-retrieve').live('pagebeforeshow', function() {
async: false,
cache: false,
success: function(modalities) {
var target = $('#qr-server');
let target = $('#qr-server');
$('option', target).remove();
for (var i = 0; i < modalities.length; i++) {
var option = $('<option>').text(modalities[i]);
for (let i = 0; i < modalities.length; i++) {
let option = $('<option>').text(modalities[i]);
target.append(option);
}
......@@ -32,7 +32,7 @@ $('#query-retrieve').live('pagebeforeshow', function() {
}
});
var target = $('#qr-date');
let target = $('#qr-date');
$('option', target).remove();
target.append($('<option>').attr('value', '*').text('Any date'));
target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
......@@ -46,8 +46,8 @@ $('#query-retrieve').live('pagebeforeshow', function() {
$('#qr-echo').live('click', function() {
var server = $('#qr-server').val();
var message = 'Error: The C-Echo has failed!';
let server = $('#qr-server').val();
let message = 'Error: The C-Echo has failed!';
$.ajax({
url: '../modalities/' + server + '/echo',
......@@ -75,7 +75,7 @@ $('#qr-echo').live('click', function() {
$('#qr-submit').live('click', function() {
var query = {
let query = {
'Level' : 'Study',
'Query' : {
'AccessionNumber' : '*',
......@@ -88,12 +88,12 @@ $('#qr-submit').live('click', function() {
}
};
var field = $('#qr-fields input:checked').val();
let field = $('#qr-fields input:checked').val();
query['Query'][field] = $('#qr-value').val().toUpperCase();
var modalities = '';
let modalities = '';
$('#qr-modalities input:checked').each(function() {
var s = $(this).attr('name');
let s = $(this).attr('name');
if (modalities == '')
modalities = s;
else
......@@ -105,7 +105,7 @@ $('#qr-submit').live('click', function() {
}
var server = $('#qr-server').val();
let server = $('#qr-server').val();
$.ajax({
url: '../modalities/' + server + '/query',
type: 'POST',
......@@ -130,27 +130,27 @@ $('#qr-submit').live('click', function() {
$('#query-retrieve-2').live('pagebeforeshow', function() {
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
var uri = '../queries/' + pageData.uuid + '/answers';
let uri = '../queries/' + pageData.uuid + '/answers';
$.ajax({
url: uri,
dataType: 'json',
async: false,
success: function(answers) {
var target = $('#query-retrieve-2 ul');
let target = $('#query-retrieve-2 ul');
$('li', target).remove();
for (var i = 0; i < answers.length; i++) {
for (let i = 0; i < answers.length; i++) {
$.ajax({
url: uri + '/' + answers[i] + '/content?simplify',
dataType: 'json',
async: false,
success: function(study) {
var series = '#query-retrieve-3?server=' + pageData.server + '&uuid=' + study['StudyInstanceUID'];
let series = '#query-retrieve-3?server=' + pageData.server + '&uuid=' + study['StudyInstanceUID'];
var content = ($('<div>')
let content = ($('<div>')
.append($('<h3>').text(study['PatientID'] + ' - ' + study['PatientName']))
.append($('<p>').text('Accession number: ')
.append($('<b>').text(study['AccessionNumber'])))
......@@ -163,10 +163,10 @@ $('#query-retrieve-2').live('pagebeforeshow', function() {
.append($('<p>').text('Study date: ')
.append($('<b>').text(FormatDicomDate(study['StudyDate'])))));
var info = $('<a>').attr('href', series).html(content);
let info = $('<a>').attr('href', series).html(content);
var answerId = answers[i];
var retrieve = $('<a>').text('Retrieve all study').click(function() {
let answerId = answers[i];
let retrieve = $('<a>').text('Retrieve all study').click(function() {
ChangePage('query-retrieve-4', {
'query' : pageData.uuid,
'answer' : answerId,
......@@ -188,9 +188,9 @@ $('#query-retrieve-2').live('pagebeforeshow', function() {
$('#query-retrieve-3').live('pagebeforeshow', function() {
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
let pageData = DeepCopy($.mobile.pageData);
var query = {
let query = {
'Level' : 'Series',
'Query' : {
'Modality' : '*',
......@@ -211,8 +211,8 @@ $('#query-retrieve-3').live('pagebeforeshow', function() {
alert('Error during query (C-Find)');
},
success: function(answer) {
var queryUuid = answer['ID'];
var uri = '../queries/' + answer['ID'] + '/answers';
let queryUuid = answer['ID'];
let uri = '../queries/' + answer['ID'] + '/answers';
$.ajax({
url: uri,
......@@ -220,25 +220,25 @@ $('#query-retrieve-3').live('pagebeforeshow', function() {
async: false,
success: function(answers) {
var target = $('#query-retrieve-3 ul');
let target = $('#query-retrieve-3 ul');
$('li', target).remove();
for (var i = 0; i < answers.length; i++) {
for (let i = 0; i < answers.length; i++) {
$.ajax({
url: uri + '/' + answers[i] + '/content?simplify',
dataType: 'json',
async: false,
success: function(series) {
var content = ($('<div>')
let content = ($('<div>')
.append($('<h3>').text(series['SeriesDescription']))
.append($('<p>').text('Modality: ')
.append($('<b>').text(series['Modality'])))
.append($('<p>').text('ProtocolName: ')
.append($('<b>').text(series['ProtocolName']))));
var info = $('<a>').html(content);
let info = $('<a>').html(content);
var answerId = answers[i];
let answerId = answers[i];
info.click(function() {
ChangePage('query-retrieve-4', {
'query' : queryUuid,
......@@ -265,8 +265,8 @@ $('#query-retrieve-3').live('pagebeforeshow', function() {
$('#query-retrieve-4').live('pagebeforeshow', function() {
if ($.mobile.pageData) {
var pageData = DeepCopy($.mobile.pageData);
var uri = '../queries/' + pageData.query + '/answers/' + pageData.answer + '/retrieve';
let pageData = DeepCopy($.mobile.pageData);
let uri = '../queries/' + pageData.query + '/answers/' + pageData.answer + '/retrieve';
$.ajax({
url: '../system',
......@@ -279,7 +279,7 @@ $('#query-retrieve-4').live('pagebeforeshow', function() {
$('#retrieve-form').submit(function(event) {
event.preventDefault();
var aet = $('#retrieve-target').val();
let aet = $('#retrieve-target').val();
if (aet.length == 0) {
aet = system['DicomAet'];
}
......
......@@ -89,82 +89,6 @@ namespace Orthanc
}
static void ExtractTagFromMainDicomTags(std::set<std::string>& target,
ServerIndex& index,
const DicomTag& tag,
const std::list<std::string>& resources,
ResourceType level)
{
for (std::list<std::string>::const_iterator
it = resources.begin(); it != resources.end(); ++it)
{
DicomMap tags;
if (index.GetMainDicomTags(tags, *it, level, level) &&
tags.HasTag(tag))
{
target.insert(tags.GetValue(tag).GetContent());
}
}
}
static bool ExtractMetadata(std::set<std::string>& target,
ServerIndex& index,
MetadataType metadata,
const std::list<std::string>& resources)
{
for (std::list<std::string>::const_iterator
it = resources.begin(); it != resources.end(); ++it)
{
std::string value;
if (index.LookupMetadata(value, *it, metadata))
{
target.insert(value);
}
else
{
// This metadata is unavailable for some resource, give up
return false;
}
}
return true;
}
static void ExtractTagFromInstancesOnDisk(std::set<std::string>& target,
ServerContext& context,
const DicomTag& tag,
const std::list<std::string>& instances)
{
// WARNING: This function is slow, as it reads the JSON file
// summarizing each instance of interest from the hard drive.
std::string formatted = tag.Format();
for (std::list<std::string>::const_iterator
it = instances.begin(); it != instances.end(); ++it)
{
Json::Value dicom;
context.ReadDicomAsJson(dicom, *it);
if (dicom.isMember(formatted))
{
const Json::Value& source = dicom[formatted];
if (source.type() == Json::objectValue &&
source.isMember("Type") &&
source.isMember("Value") &&
source["Type"].asString() == "String" &&
source["Value"].type() == Json::stringValue)
{
target.insert(source["Value"].asString());
}
}
}
}
static void ComputePatientCounters(DicomMap& result,
ServerIndex& index,
const std::string& patient,
......@@ -230,7 +154,24 @@ namespace Orthanc
if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
{
std::set<std::string> values;
ExtractTagFromMainDicomTags(values, index, DICOM_TAG_MODALITY, series, ResourceType_Series);
for (std::list<std::string>::const_iterator
it = series.begin(); it != series.end(); ++it)
{
DicomMap tags;
if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series))
{
const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
if (value != NULL &&
!value->IsNull() &&
!value->IsBinary())
{
values.insert(value->GetContent());
}
}
}
StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values);
}
......@@ -253,27 +194,17 @@ namespace Orthanc
{
std::set<std::string> values;
if (ExtractMetadata(values, index, MetadataType_Instance_SopClassUid, instances))
{
// The metadata "SopClassUid" is available for each of these instances
StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
}
else
{
OrthancConfiguration::ReaderLock lock;
if (lock.GetConfiguration().GetBooleanParameter("AllowFindSopClassesInStudy", false))
for (std::list<std::string>::const_iterator
it = instances.begin(); it != instances.end(); ++it)
{
ExtractTagFromInstancesOnDisk(values, context, DICOM_TAG_SOP_CLASS_UID, instances);
StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
}
else
std::string value;
if (context.LookupOrReconstructMetadata(value, *it, MetadataType_Instance_SopClassUid))
{
result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, "", false);
LOG(WARNING) << "The handling of \"SOP Classes in Study\" (0008,0062) "
<< "in C-FIND requests is disabled";
values.insert(value);
}
}
StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
}
}
......@@ -365,11 +296,23 @@ namespace Orthanc
static void AddAnswer(DicomFindAnswers& answers,
const Json::Value& resource,
const DicomMap& mainDicomTags,
const Json::Value* dicomAsJson,
const DicomArray& query,
const std::list<DicomTag>& sequencesToReturn,
const DicomMap* counters)
{
DicomMap match;
if (dicomAsJson != NULL)
{
match.FromDicomAsJson(*dicomAsJson);
}
else
{
match.Assign(mainDicomTags);
}
DicomMap result;
for (size_t i = 0; i < query.GetSize(); i++)
......@@ -385,16 +328,18 @@ namespace Orthanc
}
else
{
std::string tag = query.GetElement(i).GetTag().Format();
std::string value;
if (resource.isMember(tag))
const DicomTag& tag = query.GetElement(i).GetTag();
const DicomValue* value = match.TestAndGetValue(tag);
if (value != NULL &&
!value->IsNull() &&
!value->IsBinary())
{
value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
result.SetValue(query.GetElement(i).GetTag(), value, false);
result.SetValue(tag, value->GetContent(), false);
}
else
{
result.SetValue(query.GetElement(i).GetTag(), "", false);
result.SetValue(tag, "", false);
}
}
}
......@@ -417,6 +362,11 @@ namespace Orthanc
{
answers.Add(result);
}
else if (dicomAsJson == NULL)
{
LOG(WARNING) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled";
answers.Add(result);
}
else
{
ParsedDicomFile dicom(result);
......@@ -424,7 +374,8 @@ namespace Orthanc
for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin();
tag != sequencesToReturn.end(); ++tag)
{
const Json::Value& source = resource[tag->Format()];
assert(dicomAsJson != NULL);
const Json::Value& source = (*dicomAsJson) [tag->Format()];
if (source.type() == Json::objectValue &&
source.isMember("Type") &&
......@@ -521,6 +472,76 @@ namespace Orthanc
}
class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor
{
private:
DicomFindAnswers& answers_;
ServerContext& context_;
ResourceType level_;
const DicomMap& query_;
DicomArray queryAsArray_;
const std::list<DicomTag>& sequencesToReturn_;
public:
LookupVisitor(DicomFindAnswers& answers,
ServerContext& context,
ResourceType level,
const DicomMap& query,
const std::list<DicomTag>& sequencesToReturn) :
answers_(answers),
context_(context),
level_(level),
query_(query),
queryAsArray_(query),
sequencesToReturn_(sequencesToReturn)
{
answers_.SetComplete(false);
}
virtual bool IsDicomAsJsonNeeded() const
{
// Ask the "DICOM-as-JSON" attachment only if sequences are to
// be returned OR if "query_" contains non-main DICOM tags!
DicomMap withoutSpecialTags;
withoutSpecialTags.Assign(query_);
// Check out "ComputeCounters()"
withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY);
withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY);
// Check out "AddAnswer()"
withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
return (!sequencesToReturn_.empty() ||
!withoutSpecialTags.HasOnlyMainDicomTags());
}
virtual void MarkAsComplete()
{
answers_.SetComplete(true);
}
virtual void Visit(const std::string& publicId,
const std::string& instanceId,
const DicomMap& mainDicomTags,
const Json::Value* dicomAsJson)
{
std::auto_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_));
AddAnswer(answers_, mainDicomTags, dicomAsJson,
queryAsArray_, sequencesToReturn_, counters.get());
}
};
void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
const DicomMap& input,
const std::list<DicomTag>& sequencesToReturn,
......@@ -623,8 +644,6 @@ namespace Orthanc
if (FilterQueryTag(value, level, tag, manufacturer))
{
// TODO - Move this to "ResourceLookup::AddDicomConstraint()"
ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
// DICOM specifies that searches must be case sensitive, except
......@@ -651,42 +670,9 @@ namespace Orthanc
size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
// TODO - Use ServerContext::Apply() at this point, in order to
// share the code with the "/tools/find" REST URI
std::vector<std::string> resources, instances;
context_.GetIndex().FindCandidates(resources, instances, lookup);
LOG(INFO) << "Number of candidate resources after fast DB filtering: " << resources.size();
assert(resources.size() == instances.size());
bool complete = true;
for (size_t i = 0; i < instances.size(); i++)
{
// TODO - Don't read the full JSON from the disk if only "main
// DICOM tags" are to be returned
Json::Value dicom;
context_.ReadDicomAsJson(dicom, instances[i]);
if (lookup.IsMatch(dicom))
{
if (limit != 0 &&
answers.GetSize() >= limit)
{
complete = false;
break;
}
else
{
std::auto_ptr<DicomMap> counters(ComputeCounters(context_, instances[i], level, *filteredInput));
AddAnswer(answers, dicom, query, sequencesToReturn, counters.get());
}
}
}
LOG(INFO) << "Number of matching resources: " << answers.GetSize();
answers.SetComplete(complete);
LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn);
context_.Apply(visitor, lookup, 0 /* "since" is not relevant to C-FIND */, limit);
}
......
......@@ -41,6 +41,8 @@ namespace Orthanc
class OrthancFindRequestHandler : public IFindRequestHandler
{
private:
class LookupVisitor;
ServerContext& context_;
unsigned int maxResults_;
unsigned int maxInstances_;
......
......@@ -440,6 +440,7 @@ namespace Orthanc
// Select one existing child instance of the parent resource, to
// retrieve all its tags
Json::Value siblingTags;
std::string siblingInstanceId;
{
// Retrieve all the instances of the parent resource
......@@ -452,7 +453,8 @@ namespace Orthanc
throw OrthancException(ErrorCode_InternalError);
}
context.ReadDicomAsJson(siblingTags, siblingInstances.front());
siblingInstanceId = siblingInstances.front();
context.ReadDicomAsJson(siblingTags, siblingInstanceId);
}
......@@ -463,11 +465,14 @@ namespace Orthanc
if (siblingTags.isMember(SPECIFIC_CHARACTER_SET))
{
Encoding encoding;
if (!siblingTags[SPECIFIC_CHARACTER_SET].isMember("Value") ||
siblingTags[SPECIFIC_CHARACTER_SET]["Value"].type() != Json::stringValue ||
!GetDicomEncoding(encoding, siblingTags[SPECIFIC_CHARACTER_SET]["Value"].asCString()))
{
throw OrthancException(ErrorCode_CreateDicomParentEncoding);
LOG(WARNING) << "Instance with an incorrect Specific Character Set, "
<< "using the default Orthanc encoding: " << siblingInstanceId;
encoding = GetDefaultDicomEncoding();
}
dicom.SetEncoding(encoding);
......
......@@ -1269,83 +1269,158 @@ namespace Orthanc
}
namespace
{
class FindVisitor : public ServerContext::ILookupVisitor
{
private:
bool isComplete_;
std::list<std::string> resources_;
public:
FindVisitor() :
isComplete_(false)
{
}
virtual bool IsDicomAsJsonNeeded() const
{
return false; // (*)
}
virtual void MarkAsComplete()
{
isComplete_ = true; // Unused information as of Orthanc 1.5.0
}
virtual void Visit(const std::string& publicId,
const std::string& instanceId /* unused */,
const DicomMap& mainDicomTags /* unused */,
const Json::Value* dicomAsJson /* unused (*) */)
{
resources_.push_back(publicId);
}
void Answer(RestApiOutput& output,
ServerIndex& index,
ResourceType level,
bool expand) const
{
AnswerListOfResources(output, index, resources_, level, expand);
}
};
}
static void Find(RestApiPostCall& call)
{
static const char* const KEY_CASE_SENSITIVE = "CaseSensitive";
static const char* const KEY_EXPAND = "Expand";
static const char* const KEY_LEVEL = "Level";
static const char* const KEY_LIMIT = "Limit";
static const char* const KEY_QUERY = "Query";
static const char* const KEY_SINCE = "Since";
ServerContext& context = OrthancRestApi::GetContext(call);
Json::Value request;
if (call.ParseJsonRequest(request) &&
request.type() == Json::objectValue &&
request.isMember("Level") &&
request.isMember("Query") &&
request["Level"].type() == Json::stringValue &&
request["Query"].type() == Json::objectValue &&
(!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue) &&
(!request.isMember("Limit") || request["Limit"].type() == Json::intValue) &&
(!request.isMember("Since") || request["Since"].type() == Json::intValue))
if (!call.ParseJsonRequest(request) ||
request.type() != Json::objectValue)
{
throw OrthancException(ErrorCode_BadRequest,
"The body must contain a JSON object");
}
else if (!request.isMember(KEY_LEVEL) ||
request[KEY_LEVEL].type() != Json::stringValue)
{
throw OrthancException(ErrorCode_BadRequest,
"Field \"" + std::string(KEY_LEVEL) + "\" is missing, or should be a string");
}
else if (!request.isMember(KEY_QUERY) &&
request[KEY_QUERY].type() != Json::objectValue)
{
throw OrthancException(ErrorCode_BadRequest,
"Field \"" + std::string(KEY_QUERY) + "\" is missing, or should be a JSON object");
}
else if (request.isMember(KEY_CASE_SENSITIVE) &&
request[KEY_CASE_SENSITIVE].type() != Json::booleanValue)
{
throw OrthancException(ErrorCode_BadRequest,
"Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean");
}
else if (request.isMember(KEY_LIMIT) &&
request[KEY_LIMIT].type() != Json::intValue)
{
throw OrthancException(ErrorCode_BadRequest,
"Field \"" + std::string(KEY_LIMIT) + "\" should be an integer");
}
else if (request.isMember(KEY_SINCE) &&
request[KEY_SINCE].type() != Json::intValue)
{
throw OrthancException(ErrorCode_BadRequest,
"Field \"" + std::string(KEY_SINCE) + "\" should be an integer");
}
else
{
bool expand = false;
if (request.isMember("Expand"))
if (request.isMember(KEY_EXPAND))
{
expand = request["Expand"].asBool();
expand = request[KEY_EXPAND].asBool();
}
bool caseSensitive = false;
if (request.isMember("CaseSensitive"))
if (request.isMember(KEY_CASE_SENSITIVE))
{
caseSensitive = request["CaseSensitive"].asBool();
caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
}
size_t limit = 0;
if (request.isMember("Limit"))
if (request.isMember(KEY_LIMIT))
{
int tmp = request["Limit"].asInt();
int tmp = request[KEY_LIMIT].asInt();
if (tmp < 0)
{
throw OrthancException(ErrorCode_ParameterOutOfRange);
throw OrthancException(ErrorCode_ParameterOutOfRange,
"Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer");
}
limit = static_cast<size_t>(tmp);
}
size_t since = 0;
if (request.isMember("Since"))
if (request.isMember(KEY_SINCE))
{
int tmp = request["Since"].asInt();
int tmp = request[KEY_SINCE].asInt();
if (tmp < 0)
{
throw OrthancException(ErrorCode_ParameterOutOfRange);
throw OrthancException(ErrorCode_ParameterOutOfRange,
"Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer");
}
since = static_cast<size_t>(tmp);
}
std::string level = request["Level"].asString();
std::string level = request[KEY_LEVEL].asString();
LookupResource query(StringToResourceType(level.c_str()));
Json::Value::Members members = request["Query"].getMemberNames();
Json::Value::Members members = request[KEY_QUERY].getMemberNames();
for (size_t i = 0; i < members.size(); i++)
{
if (request["Query"][members[i]].type() != Json::stringValue)
if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
{
throw OrthancException(ErrorCode_BadRequest);
throw OrthancException(ErrorCode_BadRequest,
"Tag \"" + members[i] + "\" should be associated with a string");
}
query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]),
request["Query"][members[i]].asString(),
request[KEY_QUERY][members[i]].asString(),
caseSensitive);
}
bool isComplete;
std::list<std::string> resources;
context.Apply(isComplete, resources, query, since, limit);
AnswerListOfResources(call.GetOutput(), context.GetIndex(),
resources, query.GetLevel(), expand);
}
else
{
throw OrthancException(ErrorCode_BadRequest);
FindVisitor visitor;
context.Apply(visitor, query, since, limit);
visitor.Answer(call.GetOutput(), context.GetIndex(), query.GetLevel(), expand);
}
}
......
......@@ -34,9 +34,10 @@
#include "../PrecompiledHeadersServer.h"
#include "LookupIdentifierQuery.h"
#include "../../Core/DicomParsing/FromDcmtkBridge.h"
#include "../../Core/OrthancException.h"
#include "../ServerToolbox.h"
#include "SetOfResources.h"
#include "../../Core/DicomParsing/FromDcmtkBridge.h"
#include <cassert>
......@@ -44,6 +45,28 @@
namespace Orthanc
{
LookupIdentifierQuery::SingleConstraint::
SingleConstraint(const DicomTag& tag,
IdentifierConstraintType type,
const std::string& value) :
tag_(tag),
type_(type),
value_(ServerToolbox::NormalizeIdentifier(value))
{
}
LookupIdentifierQuery::RangeConstraint::
RangeConstraint(const DicomTag& tag,
const std::string& start,
const std::string& end) :
tag_(tag),
start_(ServerToolbox::NormalizeIdentifier(start)),
end_(ServerToolbox::NormalizeIdentifier(end))
{
}
LookupIdentifierQuery::Disjunction::~Disjunction()
{
for (size_t i = 0; i < singleConstraints_.size(); i++)
......@@ -84,6 +107,12 @@ namespace Orthanc
}
bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag)
{
return ServerToolbox::IsIdentifier(tag, level_);
}
void LookupIdentifierQuery::AddConstraint(DicomTag tag,
IdentifierConstraintType type,
const std::string& value)
......
......@@ -33,7 +33,6 @@
#pragma once
#include "../ServerToolbox.h"
#include "../IDatabaseWrapper.h"
#include "SetOfResources.h"
......@@ -79,12 +78,7 @@ namespace Orthanc
public:
SingleConstraint(const DicomTag& tag,
IdentifierConstraintType type,
const std::string& value) :
tag_(tag),
type_(type),
value_(ServerToolbox::NormalizeIdentifier(value))
{
}
const std::string& value);
const DicomTag& GetTag() const
{
......@@ -113,12 +107,7 @@ namespace Orthanc
public:
RangeConstraint(const DicomTag& tag,
const std::string& start,
const std::string& end) :
tag_(tag),
start_(ServerToolbox::NormalizeIdentifier(start)),
end_(ServerToolbox::NormalizeIdentifier(end))
{
}
const std::string& end);
const DicomTag& GetTag() const
{
......@@ -189,10 +178,7 @@ namespace Orthanc
~LookupIdentifierQuery();
bool IsIdentifier(const DicomTag& tag)
{
return ServerToolbox::IsIdentifier(tag, level_);
}
bool IsIdentifier(const DicomTag& tag);
void AddConstraint(DicomTag tag,
IdentifierConstraintType type,
......
......@@ -42,6 +42,19 @@
namespace Orthanc
{
static bool DoesDicomMapMatch(const DicomMap& dicom,
const DicomTag& tag,
const IFindConstraint& constraint)
{
const DicomValue* value = dicom.TestAndGetValue(tag);
return (value != NULL &&
!value->IsNull() &&
!value->IsBinary() &&
constraint.Match(value->GetContent()));
}
LookupResource::Level::Level(ResourceType level) : level_(level)
{
const DicomTag* tags = NULL;
......@@ -112,11 +125,40 @@ namespace Orthanc
return true;
}
else
{
// This is not a main DICOM tag
return false;
}
}
bool LookupResource::Level::IsMatch(const DicomMap& dicom) const
{
for (Constraints::const_iterator it = identifiersConstraints_.begin();
it != identifiersConstraints_.end(); ++it)
{
assert(it->second != NULL);
if (!DoesDicomMapMatch(dicom, it->first, *it->second))
{
return false;
}
}
for (Constraints::const_iterator it = mainTagsConstraints_.begin();
it != mainTagsConstraints_.end(); ++it)
{
assert(it->second != NULL);
if (!DoesDicomMapMatch(dicom, it->first, *it->second))
{
return false;
}
}
return true;
}
LookupResource::LookupResource(ResourceType level) : level_(level)
{
......@@ -282,22 +324,22 @@ namespace Orthanc
bool LookupResource::IsMatch(const Json::Value& dicomAsJson) const
bool LookupResource::IsMatch(const DicomMap& dicom) const
{
for (Constraints::const_iterator it = unoptimizedConstraints_.begin();
it != unoptimizedConstraints_.end(); ++it)
for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it)
{
std::string tag = it->first.Format();
if (dicomAsJson.isMember(tag) &&
dicomAsJson[tag]["Type"] == "String")
{
std::string value = dicomAsJson[tag]["Value"].asString();
if (!it->second->Match(value))
if (!it->second->IsMatch(dicom))
{
return false;
}
}
else
for (Constraints::const_iterator it = unoptimizedConstraints_.begin();
it != unoptimizedConstraints_.end(); ++it)
{
assert(it->second != NULL);
if (!DoesDicomMapMatch(dicom, it->first, *it->second))
{
return false;
}
......
......@@ -64,13 +64,15 @@ namespace Orthanc
void Apply(SetOfResources& candidates,
IDatabaseWrapper& database) const;
bool IsMatch(const DicomMap& dicom) const;
};
typedef std::map<ResourceType, Level*> Levels;
ResourceType level_;
Levels levels_;
Constraints unoptimizedConstraints_;
Constraints unoptimizedConstraints_; // Constraints on non-main DICOM tags
std::auto_ptr<ListConstraint> modalitiesInStudy_;
bool AddInternal(ResourceType level,
......@@ -103,6 +105,11 @@ namespace Orthanc
void FindCandidates(std::list<int64_t>& result,
IDatabaseWrapper& database) const;
bool IsMatch(const Json::Value& dicomAsJson) const;
bool HasOnlyMainDicomTags() const
{
return unoptimizedConstraints_.empty();
}
bool IsMatch(const DicomMap& dicom) const;
};
}
......@@ -773,27 +773,91 @@ namespace Orthanc
}
void ServerContext::Apply(bool& isComplete,
std::list<std::string>& result,
void ServerContext::Apply(ILookupVisitor& visitor,
const ::Orthanc::LookupResource& lookup,
size_t since,
size_t limit)
{
result.clear();
isComplete = true;
LookupMode mode;
{
// New configuration option in 1.5.1
OrthancConfiguration::ReaderLock lock;
std::string value = lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always");
if (value == "Always")
{
mode = LookupMode_DiskOnLookupAndAnswer;
}
else if (value == "Never")
{
mode = LookupMode_DatabaseOnly;
}
else if (value == "Answers")
{
mode = LookupMode_DiskOnAnswer;
}
else
{
throw OrthancException(ErrorCode_ParameterOutOfRange,
"Configuration option \"StorageAccessOnFind\" "
"should be \"Always\", \"Never\" or \"Answers\": " + value);
}
}
std::vector<std::string> resources, instances;
GetIndex().FindCandidates(resources, instances, lookup);
LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size();
assert(resources.size() == instances.size());
size_t countResults = 0;
size_t skipped = 0;
bool complete = true;
const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded();
for (size_t i = 0; i < instances.size(); i++)
{
// TODO - Don't read the full JSON from the disk if only "main
// DICOM tags" are to be returned
Json::Value dicom;
ReadDicomAsJson(dicom, instances[i]);
// Optimization in Orthanc 1.5.1 - Don't read the full JSON from
// the disk if only "main DICOM tags" are to be returned
std::auto_ptr<Json::Value> dicomAsJson;
bool hasOnlyMainDicomTags;
DicomMap dicom;
if (mode == LookupMode_DatabaseOnly ||
mode == LookupMode_DiskOnAnswer ||
lookup.HasOnlyMainDicomTags())
{
// Case (1): The main DICOM tags, as stored in the database,
// are sufficient to look for match
if (!GetIndex().GetAllMainDicomTags(dicom, instances[i]))
{
// The instance has been removed during the execution of the
// lookup, ignore it
continue;
}
hasOnlyMainDicomTags = true;
}
else
{
// Case (2): Need to read the "DICOM-as-JSON" attachment from
// the storage area
dicomAsJson.reset(new Json::Value);
ReadDicomAsJson(*dicomAsJson, instances[i]);
dicom.FromDicomAsJson(*dicomAsJson);
// This map contains the entire JSON, i.e. more than the main DICOM tags
hasOnlyMainDicomTags = false;
}
if (lookup.IsMatch(dicom))
{
......@@ -802,16 +866,119 @@ namespace Orthanc
skipped++;
}
else if (limit != 0 &&
result.size() >= limit)
countResults >= limit)
{
// Too many results, don't mark as complete
complete = false;
break;
}
else
{
if ((mode == LookupMode_DiskOnLookupAndAnswer ||
mode == LookupMode_DiskOnAnswer) &&
dicomAsJson.get() == NULL &&
isDicomAsJsonNeeded)
{
dicomAsJson.reset(new Json::Value);
ReadDicomAsJson(*dicomAsJson, instances[i]);
}
if (hasOnlyMainDicomTags)
{
// This is Case (1): The variable "dicom" only contains the main DICOM tags
visitor.Visit(resources[i], instances[i], dicom, dicomAsJson.get());
}
else
{
// Remove the non-main DICOM tags from "dicom" if Case (2)
// was used, for consistency with Case (1)
DicomMap mainDicomTags;
mainDicomTags.ExtractMainDicomTags(dicom);
visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get());
}
countResults ++;
}
}
}
if (complete)
{
visitor.MarkAsComplete();
}
LOG(INFO) << "Number of matching resources: " << countResults;
}
bool ServerContext::LookupOrReconstructMetadata(std::string& target,
const std::string& publicId,
MetadataType metadata)
{
// This is a backwards-compatibility function, that can
// reconstruct metadata that were not generated by an older
// release of Orthanc
if (metadata == MetadataType_Instance_SopClassUid ||
metadata == MetadataType_Instance_TransferSyntax)
{
isComplete = false;
return; // too many results
if (index_.LookupMetadata(target, publicId, metadata))
{
return true;
}
else
{
result.push_back(resources[i]);
// These metadata are mandatory in DICOM instances, and were
// introduced in Orthanc 1.2.0. The fact that
// "LookupMetadata()" has failed indicates that this database
// comes from an older release of Orthanc.
DicomTag tag(0, 0);
switch (metadata)
{
case MetadataType_Instance_SopClassUid:
tag = DICOM_TAG_SOP_CLASS_UID;
break;
case MetadataType_Instance_TransferSyntax:
tag = DICOM_TAG_TRANSFER_SYNTAX_UID;
break;
default:
throw OrthancException(ErrorCode_InternalError);
}
Json::Value dicomAsJson;
ReadDicomAsJson(dicomAsJson, publicId);
DicomMap tags;
tags.FromDicomAsJson(dicomAsJson);
const DicomValue* value = tags.TestAndGetValue(tag);
if (value != NULL &&
!value->IsNull() &&
!value->IsBinary())
{
target = value->GetContent();
// Store for reuse
index_.SetMetadata(publicId, metadata, target);
return true;
}
else
{
// Should never happen
return false;
}
}
}
else
{
// No backward
return index_.LookupMetadata(target, publicId, metadata);
}
}
......
......@@ -38,6 +38,7 @@
#include "LuaScripting.h"
#include "OrthancHttpHandler.h"
#include "ServerIndex.h"
#include "Search/LookupResource.h"
#include "../Core/Cache/MemoryCache.h"
#include "../Core/Cache/SharedArchive.h"
......@@ -62,7 +63,34 @@ namespace Orthanc
**/
class ServerContext : private JobsRegistry::IObserver
{
public:
class ILookupVisitor : public boost::noncopyable
{
public:
virtual ~ILookupVisitor()
{
}
virtual bool IsDicomAsJsonNeeded() const = 0;
virtual void MarkAsComplete() = 0;
virtual void Visit(const std::string& publicId,
const std::string& instanceId,
const DicomMap& mainDicomTags,
const Json::Value* dicomAsJson) = 0;
};
private:
enum LookupMode
{
LookupMode_DatabaseOnly,
LookupMode_DiskOnAnswer,
LookupMode_DiskOnLookupAndAnswer
};
class LuaServerListener : public IServerListener
{
private:
......@@ -334,12 +362,15 @@ namespace Orthanc
void Stop();
void Apply(bool& isComplete,
std::list<std::string>& result,
void Apply(ILookupVisitor& visitor,
const ::Orthanc::LookupResource& lookup,
size_t since,
size_t limit);
bool LookupOrReconstructMetadata(std::string& target,
const std::string& publicId,
MetadataType type);
/**
* Management of the plugins
......