changeset 1371:f528849ee9f7 query-retrieve

DICOM Query/Retrieve available from Orthanc Explorer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 27 May 2015 17:33:13 +0200
parents 7b6f5115607f
children 1412ec22ee0b
files NEWS OrthancExplorer/explorer.html OrthancExplorer/query-retrieve.js OrthancServer/DicomProtocol/DicomUserConnection.cpp OrthancServer/DicomProtocol/DicomUserConnection.h OrthancServer/OrthancMoveRequestHandler.cpp
diffstat 6 files changed, 441 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed May 27 12:32:43 2015 +0200
+++ b/NEWS	Wed May 27 17:33:13 2015 +0200
@@ -4,6 +4,8 @@
 Major
 -----
 
+* DICOM Query/Retrieve available from Orthanc Explorer
+* C-Move SCU and C-Find SCU are accessible through the REST API
 * "?expand" flag for URIs "/patients", "/studies" and "/series"
 * "/tools/find" URI to search for DICOM resources from REST
 * Support of FreeBSD
--- a/OrthancExplorer/explorer.html	Wed May 27 12:32:43 2015 +0200
+++ b/OrthancExplorer/explorer.html	Wed May 27 17:33:13 2015 +0200
@@ -30,6 +30,7 @@
     <link rel="stylesheet" href="explorer.css" />
     <script src="file-upload.js"></script>
     <script src="explorer.js"></script>
+    <script src="query-retrieve.js"></script>
     <script src="../plugins/explorer.js"></script>
   </head>
   <body>
@@ -37,7 +38,10 @@
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Find a patient</h1>
         <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <ul id="all-patients" data-role="listview" data-inset="true" data-filter="true">
@@ -75,7 +79,10 @@
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Patient</h1>
         <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <div class="ui-grid-a">
@@ -129,7 +136,10 @@
           Study
         </h1>
         <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <div class="ui-grid-a">
@@ -178,7 +188,10 @@
         </h1>
 
         <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <div class="ui-grid-a">
@@ -228,7 +241,10 @@
           Instance
         </h1>
         <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <div class="ui-grid-a">
@@ -284,6 +300,90 @@
       </div>
     </div>
 
+    <div data-role="page" id="query-retrieve" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (1/3)</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+      </div>
+      <div data-role="content">
+        <form data-ajax="false">
+          <div data-role="fieldcontain">
+	    <label for="qr-server">DICOM server:</label>
+            <select name="qr-server" id="qr-server">
+            </select>
+	  </div>
+
+          <div data-role="fieldcontain" id="qr-fields">
+	    <fieldset data-role="controlgroup">
+	      <legend>Field of interest:</legend>
+	      <input type="radio" name="qr-field" id="qr-patient-id" value="PatientID" checked="checked" />
+	      <label for="qr-patient-id">Patient ID</label>
+	      <input type="radio" name="qr-field" id="qr-patient-name" value="PatientName" />
+	      <label for="qr-patient-name">Patient Name</label>
+	      <input type="radio" name="qr-field" id="qr-accession-number" value="AccessionNumber" />
+	      <label for="qr-accession-number">Accession Number</label>
+	      <input type="radio" name="qr-field" id="qr-study-description" value="StudyDescription" />
+	      <label for="qr-study-description">Study Description</label>
+	    </fieldset>
+	  </div>
+
+          <div data-role="fieldcontain">
+	    <label for="qr-value">Value for this field:</label>
+	    <input type="text" name="qr-value" id="qr-value" value="*"  />
+	  </div>
+
+          <div data-role="fieldcontain">
+	    <label for="qr-date">Study date:</label>
+            <select name="qr-date" id="qr-date">
+            </select>
+	  </div>
+
+          <div data-role="fieldcontain" id="qr-modalities">
+            <div data-role="fieldcontain">
+	      <fieldset data-role="controlgroup" data-type="horizontal">
+                <legend>Modalities:</legend>
+	        <input type="checkbox" name="CR" id="qr-cr" class="custom" /> <label for="qr-cr">CR</label>
+	        <input type="checkbox" name="CT" id="qr-ct" class="custom" /> <label for="qr-ct">CT</label>
+	        <input type="checkbox" name="MR" id="qr-mr" class="custom" /> <label for="qr-mr">MR</label>
+	        <input type="checkbox" name="NM" id="qr-nm" class="custom" /> <label for="qr-nm">NM</label>
+	        <input type="checkbox" name="PT" id="qr-pt" class="custom" /> <label for="qr-pt">PT</label>
+	        <input type="checkbox" name="US" id="qr-us" class="custom" /> <label for="qr-us">US</label>
+	        <input type="checkbox" name="XA" id="qr-xa" class="custom" /> <label for="qr-xa">XA</label>
+	      </fieldset>
+            </div>
+          </div>
+
+          <button id="qr-submit" type="submit" data-theme="b">Search studies</button>
+        </form>
+      </div>
+    </div>
+
+
+    <div data-role="page" id="query-retrieve-2" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (2/3)</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a>
+      </div>
+      <div data-role="content">
+        <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b">
+        </ul>
+      </div>
+    </div>
+
+
+    <div data-role="page" id="query-retrieve-3" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (3/3)</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a>
+      </div>
+      <div data-role="content">
+        <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b">
+        </ul>
+      </div>
+    </div>
+
 
     <div id="peer-store" style="display:none;" class="ui-body-c">
       <p align="center"><b>Sending to Orthanc peer...</b></p>
@@ -295,6 +395,11 @@
       <p><img src="libs/images/ajax-loader2.gif" alt="" /></p>
     </div>
 
+    <div id="info-retrieve" style="display:none;" class="ui-body-c">
+      <p align="center"><b>Retrieving images from DICOM modality...</b></p>
+      <p><img src="libs/images/ajax-loader2.gif" alt="" /></p>
+    </div>
+
     <div id="dialog" style="display:none" >
     </div>
   </body>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancExplorer/query-retrieve.js	Wed May 27 17:33:13 2015 +0200
@@ -0,0 +1,265 @@
+function JavascriptDateToDicom(date)
+{
+  var 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);
+  other.setDate(today.getDate() + days);
+  return JavascriptDateToDicom(other);
+}
+
+
+$('#query-retrieve').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../modalities',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(modalities) {
+      var target = $('#qr-server');
+      $('option', target).remove();
+
+      for (var i = 0; i < modalities.length; i++) {
+        var option = $('<option>').text(modalities[i]);
+        target.append(option);
+      }
+
+      target.selectmenu('refresh');
+    }
+  });
+
+  var target = $('#qr-date');
+  $('option', target).remove();
+  target.append($('<option>').attr('value', '*').text('Any date'));
+  target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year'));
+  target.selectmenu('refresh');
+});
+
+
+$('#qr-submit').live('click', function() {
+  var query = {
+    'Level' : 'Study',
+    'Query' : {
+      'AccessionNumber' : '*',
+      'PatientBirthDate' : '*',
+      'PatientID' : '*',
+      'PatientName' : '*',
+      'PatientSex' : '*',
+      'SpecificCharacterSet' : 'ISO_IR 192',  // UTF-8
+      'StudyDate' : $('#qr-date').val(),
+      'StudyDescription' : '*'
+    }
+  };
+
+  var field = $('#qr-fields input:checked').val();
+  query['Query'][field] = $('#qr-value').val().toUpperCase();
+
+  var modalities = '';
+  $('#qr-modalities input:checked').each(function() {
+    var s = $(this).attr('name');
+    if (modalities == '*')
+      modalities = s;
+    else
+      modalities += '\\' + s;
+  });
+
+  if (modalities.length > 0) {
+    query['Query']['ModalitiesInStudy'] = modalities;
+  }
+
+
+  var server = $('#qr-server').val();
+  $.ajax({
+    url: '../modalities/' + server + '/query',
+    type: 'POST', 
+    data: JSON.stringify(query),
+    dataType: 'json',
+    async: false,
+    error: function() {
+      alert('Error during query (C-Find)');
+    },
+    success: function(result) {
+      window.location.assign('explorer.html#query-retrieve-2?server=' + server + '&uuid=' + result['ID']);
+    }
+  });
+
+  return false;
+});
+
+
+
+function Retrieve(url)
+{
+  $.ajax({
+    url: '../system',
+    dataType: 'json',
+    async: false,
+    success: function(system) {
+      $('<div>').simpledialog2({
+        mode: 'button',
+        headerText: 'Retrieve destination',
+        headerClose: true,
+        buttonPrompt: 'Enter Application Entity Title (AET):',
+        buttonInput: true,
+        buttonInputDefault: system['DicomAet'],
+        buttons : {
+          'OK': {
+            click: function () { 
+              var aet = $.mobile.sdLastInput;
+              if (aet.length == 0)
+                aet = system['DicomAet'];
+
+              $.ajax({
+                url: url,
+                type: 'POST',
+                async: true,  // Necessary to block UI
+                dataType: 'text',
+                data: aet,
+                beforeSend: function() {
+                  $.blockUI({ message: $('#info-retrieve') });
+                },
+                complete: function(s) {
+                  $.unblockUI();
+                },
+                error: function() {
+                  alert('Error during retrieve');
+                }
+              });
+            }
+          }
+        }
+      });
+    }
+  });
+}
+
+
+
+
+$('#query-retrieve-2').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var uri = '../queries/' + $.mobile.pageData.uuid + '/answers';
+
+    $.ajax({
+      url: uri,
+      dataType: 'json',
+      async: false,
+      success: function(answers) {
+        var target = $('#query-retrieve-2 ul');
+        $('li', target).remove();
+
+        for (var 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=' + $.mobile.pageData.server + '&uuid=' + study['StudyInstanceUID'];
+              var info = $('<a>').attr('href', series).html(
+                ('<h3>{0} - {1}</h3>' + 
+                 '<p>Accession number: <b>{2}</b></p>' +
+                 '<p>Birth date: <b>{3}</b></p>' +
+                 '<p>Patient sex: <b>{4}</b></p>' +
+                 '<p>Study description: <b>{5}</b></p>' +
+                 '<p>Study date: <b>{6}</b></p>').format(
+                   study['PatientID'],
+                   study['PatientName'],
+                   study['AccessionNumber'],
+                   FormatDicomDate(study['PatientBirthDate']),
+                   study['PatientSex'],
+                   study['StudyDescription'],
+                   FormatDicomDate(study['StudyDate'])));
+
+              var studyUri = uri + '/' + answers[i] + '/retrieve';
+              var retrieve = $('<a>').text('Retrieve').click(function() {
+                Retrieve(studyUri);
+              });
+
+              target.append($('<li>').append(info).append(retrieve));
+            }
+          });
+        }
+
+        target.listview('refresh');
+      }
+    });
+  }
+});
+
+
+$('#query-retrieve-3').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var query = {
+      'Level' : 'Series',
+      'Query' : {
+        'Modality' : '*',
+        'ProtocolName' : '*',
+        'SeriesDescription' : '*',
+        'SeriesInstanceUID' : '*',
+        'StudyInstanceUID' : $.mobile.pageData.uuid
+      }
+    };
+
+    $.ajax({
+      url: '../modalities/' + $.mobile.pageData.server + '/query',
+      type: 'POST', 
+      data: JSON.stringify(query),
+      dataType: 'json',
+      async: false,
+      error: function() {
+        alert('Error during query (C-Find)');
+      },
+      success: function(answer) {
+        var uri = '../queries/' + answer['ID'] + '/answers';
+
+        $.ajax({
+          url: uri,
+          dataType: 'json',
+          async: false,
+          success: function(answers) {
+            
+            var target = $('#query-retrieve-3 ul');
+            $('li', target).remove();
+
+            for (var i = 0; i < answers.length; i++) {
+              $.ajax({
+                url: uri + '/' + answers[i] + '/content?simplify',
+                dataType: 'json',
+                async: false,
+                success: function(series) {
+                  var info = $('<a>').html(
+                    ('<h3>{0}</h3>'  + 
+                     '<p>Modality: <b>{1}</b></p>' +
+                     '<p>Protocol name: <b>{2}</b></p>'
+                    ).format(
+                      series['SeriesDescription'],
+                      series['Modality'],
+                      series['ProtocolName']
+                    ));
+
+                  var seriesUri = uri + '/' + answers[i] + '/retrieve';
+                  var retrieve = $('<a>').text('Retrieve').click(function() {
+                    Retrieve(seriesUri);
+                  });
+
+                  target.append($('<li>').append(info).append(retrieve));
+                }
+              });
+            }
+
+            target.listview('refresh');
+          }
+        });
+      }
+    });
+  }
+});
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Wed May 27 12:32:43 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Wed May 27 17:33:13 2015 +0200
@@ -399,6 +399,13 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
+    if (level == ResourceType_Study)
+    {
+      allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+    }
+
+    allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+
     DicomArray query(fields);
     for (size_t i = 0; i < query.GetSize(); i++)
     {
@@ -533,6 +540,7 @@
 
 
   void DicomUserConnection::MoveInternal(const std::string& targetAet,
+                                         ResourceType level,
                                          const DicomMap& fields)
   {
     CheckIsOpen();
@@ -540,6 +548,39 @@
     const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
     std::auto_ptr<DcmDataset> dataset(ToDcmtkBridge::Convert(fields));
 
+    switch (level)
+    {
+      case ResourceType_Patient:
+        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT");
+        break;
+
+      case ResourceType_Study:
+        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY");
+        break;
+
+      case ResourceType_Series:
+        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES");
+        break;
+
+      case ResourceType_Instance:
+        if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
+            manufacturer_ == ModalityManufacturer_Dcm4Chee)
+        {
+          // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
+          // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
+          // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
+          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "IMAGE");
+        }
+        else
+        {
+          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "INSTANCE");
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
     // Figure out which of the accepted presentation contexts should be used
     int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
     if (presID == 0)
@@ -880,7 +921,7 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    MoveInternal(targetAet, move);
+    MoveInternal(targetAet, level, move);
   }
 
 
@@ -889,7 +930,7 @@
   {
     DicomMap query;
     query.SetValue(DICOM_TAG_PATIENT_ID, patientId);
-    MoveInternal(targetAet, query);
+    MoveInternal(targetAet, ResourceType_Patient, query);
   }
 
   void DicomUserConnection::MoveStudy(const std::string& targetAet,
@@ -897,7 +938,7 @@
   {
     DicomMap query;
     query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
-    MoveInternal(targetAet, query);
+    MoveInternal(targetAet, ResourceType_Study, query);
   }
 
   void DicomUserConnection::MoveSeries(const std::string& targetAet,
@@ -907,7 +948,7 @@
     DicomMap query;
     query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
     query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid);
-    MoveInternal(targetAet, query);
+    MoveInternal(targetAet, ResourceType_Series, query);
   }
 
   void DicomUserConnection::MoveInstance(const std::string& targetAet,
@@ -919,7 +960,7 @@
     query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
     query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid);
     query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid);
-    MoveInternal(targetAet, query);
+    MoveInternal(targetAet, ResourceType_Instance, query);
   }
 
 
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h	Wed May 27 12:32:43 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.h	Wed May 27 17:33:13 2015 +0200
@@ -65,6 +65,7 @@
     void SetupPresentationContexts(const std::string& preferredTransferSyntax);
 
     void MoveInternal(const std::string& targetAet,
+                      ResourceType level,
                       const DicomMap& fields);
 
     void ResetStorageSOPClasses();
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Wed May 27 12:32:43 2015 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Wed May 27 17:33:13 2015 +0200
@@ -36,6 +36,8 @@
 #include <glog/logging.h>
 
 #include "OrthancInitialization.h"
+#include "FromDcmtkBridge.h"
+#include "../Core/DicomFormat/DicomArray.h"
 
 namespace Orthanc
 {
@@ -132,6 +134,21 @@
   {
     LOG(WARNING) << "Move-SCU request received for AET \"" << aet << "\"";
 
+    {
+      DicomArray query(input);
+      for (size_t i = 0; i < query.GetSize(); i++)
+      {
+        if (!query.GetElement(i).GetValue().IsNull())
+        {
+          LOG(INFO) << "  " << query.GetElement(i).GetTag()
+                    << "  " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag())
+                    << " = " << query.GetElement(i).GetValue().AsString();
+        }
+      }
+    }
+
+
+
     /**
      * Retrieve the query level.
      **/