Ver código fonte

Add support for CGET for DICOM retrieve

Use the ctkDcmSCU stand-in class (for DCMTK's DcmSCU)
to implement getting from compatible dicom peers.

Some renaming of methods and members in order to allow
the ctkDICOMRetrieve class to support both types of
transfer.
Steve Pieper 13 anos atrás
pai
commit
95d7f8ac17

+ 3 - 3
Applications/ctkDICOMRetrieve/ctkDICOMRetrieveMain.cpp

@@ -81,7 +81,7 @@ int main(int argc, char** argv)
   ctkDICOMRetrieve retrieve;
   retrieve.setCallingAETitle ( CallingAETitle );
   retrieve.setCalledAETitle ( CalledAETitle );
-  retrieve.setCalledPort ( CalledPort );
+  retrieve.setPort ( CalledPort );
   retrieve.setHost ( Host );
   retrieve.setMoveDestinationAETitle ( MoveDestinationAETitle );
 
@@ -94,12 +94,12 @@ int main(int argc, char** argv)
 
   QSharedPointer<ctkDICOMDatabase> dicomDatabase =  QSharedPointer<ctkDICOMDatabase> (new ctkDICOMDatabase);
   dicomDatabase->openDatabase( OutputDirectory.absoluteFilePath(QString("ctkDICOM.sql")) );
-  retrieve.setRetrieveDatabase( dicomDatabase );
+  retrieve.setDatabase( dicomDatabase );
 
   logger.info ( "Starting to retrieve" );
   try
     {
-    retrieve.retrieveStudy ( StudyUID );
+    retrieve.moveStudy ( StudyUID );
     }
   catch (std::exception e)
     {

+ 28 - 12
Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest1.cpp

@@ -41,14 +41,14 @@ int ctkDICOMRetrieveTest1( int argc, char * argv [] )
   if (!retrieve.callingAETitle().isEmpty() ||
       !retrieve.calledAETitle().isEmpty() ||
       !retrieve.host().isEmpty() ||
-      retrieve.calledPort() != 0 ||
+      retrieve.port() != 0 ||
       !retrieve.moveDestinationAETitle().isEmpty())
     {
     std::cerr << "ctkDICOMRetrieve::ctkDICOMRetrieve() failed: "
               << qPrintable(retrieve.callingAETitle()) << " "
               << qPrintable(retrieve.calledAETitle()) << " "
               << qPrintable(retrieve.host()) << " "
-              << retrieve.calledPort() << " "
+              << retrieve.port() << " "
               << qPrintable(retrieve.moveDestinationAETitle()) << std::endl;
     return EXIT_FAILURE;
     }
@@ -77,36 +77,52 @@ int ctkDICOMRetrieveTest1( int argc, char * argv [] )
     return EXIT_FAILURE;
     }
 
-  retrieve.setCalledPort(80);
-  if (retrieve.calledPort() != 80)
+  retrieve.setPort(80);
+  if (retrieve.port() != 80)
     {
     std::cerr << "ctkDICOMRetrieve::setCalledPort() failed: "
-              << retrieve.calledPort() << std::endl;
+              << retrieve.port() << std::endl;
     return EXIT_FAILURE;
     }
 
   QSharedPointer<ctkDICOMDatabase> dicomDatabase(new ctkDICOMDatabase);
-  retrieve.setRetrieveDatabase(dicomDatabase);
+  retrieve.setDatabase(dicomDatabase);
 
-  if (retrieve.retrieveDatabase() != dicomDatabase)
+  if (retrieve.database() != dicomDatabase)
     {
-    std::cerr << __LINE__ << ": ctkDICOMRetrieve::setRetrieveDatabase() failed."
+    std::cerr << __LINE__ << ": ctkDICOMRetrieve::setDatabase() failed."
               << std::endl;
     return EXIT_FAILURE;
     }
 
-  bool res = retrieve.retrieveSeries(QString(), QString());
+  bool res = retrieve.moveSeries(QString(), QString());
   if (res)
     {
-    std::cerr << __LINE__ << ": ctkDICOMRetrieve::retrieveSeries() should fail."
+    std::cerr << __LINE__ << ": ctkDICOMRetrieve::moveSeries() should fail."
               << std::endl;
     return EXIT_FAILURE;
     }
 
-  res = retrieve.retrieveStudy(QString());
+  res = retrieve.moveStudy(QString());
   if (res)
     {
-    std::cerr << __LINE__ << ": ctkDICOMRetrieve::retrieveStudy() should fail."
+    std::cerr << __LINE__ << ": ctkDICOMRetrieve::moveStudy() should fail."
+              << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  res = retrieve.getSeries(QString(), QString());
+  if (res)
+    {
+    std::cerr << __LINE__ << ": ctkDICOMRetrieve::getSeries() should fail."
+              << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  res = retrieve.getStudy(QString());
+  if (res)
+    {
+    std::cerr << __LINE__ << ": ctkDICOMRetrieve::getStudy() should fail."
               << std::endl;
     return EXIT_FAILURE;
     }

+ 3 - 3
Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest2.cpp

@@ -83,15 +83,15 @@ int ctkDICOMRetrieveTest2( int argc, char * argv [] )
   ctkDICOMRetrieve retrieve;
   retrieve.setCallingAETitle("CTK_AE");
   retrieve.setCalledAETitle("CTK_AE");
-  retrieve.setCalledPort(tester.dcmqrscpPort());
+  retrieve.setPort(tester.dcmqrscpPort());
   retrieve.setHost("localhost");
   retrieve.setMoveDestinationAETitle("CTK_CLIENT_AE");
 
-  retrieve.setRetrieveDatabase(retrieveDatabase);
+  retrieve.setDatabase(retrieveDatabase);
 
   foreach(const QString& study, query.studyInstanceUIDQueried())
     {
-    bool res = retrieve.retrieveStudy(study);
+    bool res = retrieve.moveStudy(study);
     if (!res)
       {
       std::cout << "ctkDICOMRetrieve::retrieveStudy() failed. "

+ 28 - 1
Libs/DICOM/Core/ctkDICOMQuery.cpp

@@ -55,6 +55,31 @@
 static ctkLogger logger ( "org.commontk.dicom.DICOMQuery" );
 
 //------------------------------------------------------------------------------
+// A customized implemenation so that Qt signals can be emitted
+// when query results are obtained
+class ctkDICOMQuerySCUPrivate : public ctkDcmSCU
+{
+public:
+  ctkDICOMQuery *query;
+  ctkDICOMQuerySCUPrivate()
+    {
+    this->query = 0;
+    };
+  ~ctkDICOMQuerySCUPrivate() {};
+  virtual OFCondition handleFINDResponse(const T_ASC_PresentationContextID  presID,
+                                         QRResponse *response,
+                                         OFBool &waitForNextResponse)
+    {
+      if (this->query)
+        {
+        logger.debug ( "FIND RESPONSE" );
+        emit this->query->queryResponseHandled("Got a find response!");
+        return this->ctkDcmSCU::handleFINDResponse(presID, response, waitForNextResponse);
+        }
+    };
+};
+
+//------------------------------------------------------------------------------
 class ctkDICOMQueryPrivate
 {
 public:
@@ -69,7 +94,7 @@ public:
   QString                 Host;
   int                     Port;
   QMap<QString,QVariant>  Filters;
-  ctkDcmSCU               SCU;
+  ctkDICOMQuerySCUPrivate SCU;
   DcmDataset*             Query;
   QStringList             StudyInstanceUIDList;
 };
@@ -104,6 +129,8 @@ ctkDICOMQuery::ctkDICOMQuery(QObject* parentObject)
   : QObject(parentObject)
   , d_ptr(new ctkDICOMQueryPrivate)
 {
+  Q_D(ctkDICOMQuery);
+  d->SCU.query = this; // give the dcmtk level access to this for emitting signals
 }
 
 //------------------------------------------------------------------------------

+ 6 - 0
Libs/DICOM/Core/ctkDICOMQuery.h

@@ -96,6 +96,10 @@ Q_SIGNALS:
   /// Signal is emitted inside the query() function. It sends the different step
   /// the function is at.
   void progress(const QString& message);
+  /// Signal is emitted from the private SCU class when dicom query responses
+  /// arrive
+  void queryResponseHandled( const QString message );
+
 
 protected:
   QScopedPointer<ctkDICOMQueryPrivate> d_ptr;
@@ -103,6 +107,8 @@ protected:
 private:
   Q_DECLARE_PRIVATE(ctkDICOMQuery);
   Q_DISABLE_COPY(ctkDICOMQuery);
+
+  friend class ctkDICOMQuerySCUPrivate;  // for access to queryResponseHandled
 };
 
 #endif

+ 313 - 62
Libs/DICOM/Core/ctkDICOMRetrieve.cpp

@@ -21,16 +21,6 @@
 #include <stdexcept>
 
 // Qt includes
-#include <QSqlQuery>
-#include <QSqlRecord>
-#include <QVariant>
-#include <QDate>
-#include <QStringList>
-#include <QSet>
-#include <QFile>
-#include <QDirIterator>
-#include <QFileInfo>
-#include <QDebug>
 
 // ctkDICOMCore includes
 #include "ctkDICOMRetrieve.h"
@@ -64,21 +54,99 @@
 static ctkLogger logger("org.commontk.dicom.DICOMRetrieve");
 
 //------------------------------------------------------------------------------
+// A customized local implemenation of the DcmSCU so that Qt signals can be emitted
+// when retrieve results are obtained
+class ctkDICOMRetrieveSCUPrivate : public ctkDcmSCU
+{
+public:
+  ctkDICOMRetrieve *retrieve;
+  ctkDICOMRetrieveSCUPrivate()
+    {
+    this->retrieve = 0;
+    };
+  ~ctkDICOMRetrieveSCUPrivate() {};
+
+  // called when a move reponse comes in: indicates that the
+  // move request is being handled by the remote server.
+  virtual OFCondition handleMOVEResponse(const T_ASC_PresentationContextID  presID,
+                                         RetrieveResponse *response,
+                                         OFBool &waitForNextResponse)
+    {
+      if (this->retrieve)
+        {
+        emit this->retrieve->moveResponseHandled("Got one!");
+        return this->ctkDcmSCU::handleMOVEResponse(
+                        presID, response, waitForNextResponse);
+        }
+      return false;
+    };
+
+  // called when a data set is coming in from a server in
+  // response to a CGET 
+  virtual OFCondition handleSTORERequest(const T_ASC_PresentationContextID presID,
+                                         DcmDataset *incomingObject,
+                                         OFBool& continueCGETSession,
+                                         Uint16& cStoreReturnStatus)
+    {
+      if (this->retrieve)
+        {
+        emit this->retrieve->storeRequested("Got a store request!");
+        if (this->retrieve && this->retrieve->database())
+          {
+          this->retrieve->database()->insert(incomingObject);
+          return ECC_Normal;
+          }
+        else
+          {
+          return this->ctkDcmSCU::handleSTORERequest(
+                          presID, incomingObject, continueCGETSession, cStoreReturnStatus);
+          }
+        }
+      return false;
+    };
+
+  // called when status information from remote server
+  // comes in from CGET
+  virtual OFCondition handleCGETResponse(const T_ASC_PresentationContextID presID,
+                                         RetrieveResponse* response,
+                                         OFBool& continueCGETSession)
+    {
+      if (this->retrieve)
+        {
+        emit this->retrieve->retrieveStatusChanged("Got a cget response!");
+        return this->ctkDcmSCU::handleCGETResponse(presID, response, continueCGETSession);
+        }
+      return false;
+    };
+};
+
+
+//------------------------------------------------------------------------------
 class ctkDICOMRetrievePrivate
 {
 public:
   ctkDICOMRetrievePrivate();
   ~ctkDICOMRetrievePrivate();
+  /// Keep the currently negotiated connection to the 
+  /// peer host open unless the connection parameters change
   bool          KeepAssociationOpen;
   bool          ConnectionParamsChanged;
-  QSharedPointer<ctkDICOMDatabase> RetrieveDatabase;
-  ctkDcmSCU        SCU;
+  bool          LastRetrieveType;
+  QSharedPointer<ctkDICOMDatabase> Database;
+  ctkDICOMRetrieveSCUPrivate        SCU;
   QString MoveDestinationAETitle;
   // do the retrieve, handling both series and study retrieves
-  enum RetrieveType { RetrieveSeries, RetrieveStudy };
-  bool retrieve ( const QString& studyInstanceUID,
+  enum RetrieveType { RetrieveNone, RetrieveSeries, RetrieveStudy };
+  bool initializeSCU(const QString& studyInstanceUID,
+                  const QString& seriesInstanceUID,
+                  const RetrieveType retrieveType,
+                  DcmDataset *retrieveParameters);
+  bool move ( const QString& studyInstanceUID,
                   const QString& seriesInstanceUID,
-                  const RetrieveType rType );
+                  const RetrieveType retrieveType );
+  bool get ( const QString& studyInstanceUID,
+                  const QString& seriesInstanceUID,
+                  const RetrieveType retrieveType );
 };
 
 //------------------------------------------------------------------------------
@@ -87,15 +155,37 @@ public:
 //------------------------------------------------------------------------------
 ctkDICOMRetrievePrivate::ctkDICOMRetrievePrivate()
 {
-  this->RetrieveDatabase = QSharedPointer<ctkDICOMDatabase> (0);
+  this->Database = QSharedPointer<ctkDICOMDatabase> (0);
   this->KeepAssociationOpen = true;
   this->ConnectionParamsChanged = false;
+  this->LastRetrieveType = RetrieveNone;
+
+  // Register the JPEG libraries in case we need them
+  // (registration only happens once, so it's okay to call repeatedly)
+  // register global JPEG decompression codecs
+  DJDecoderRegistration::registerCodecs();
+  // register global JPEG compression codecs
+  DJEncoderRegistration::registerCodecs();
+  // register RLE compression codec
+  DcmRLEEncoderRegistration::registerCodecs();
+  // register RLE decompression codec
+  DcmRLEDecoderRegistration::registerCodecs();
+
   logger.info ( "Setting Transfer Syntaxes" );
   OFList<OFString> transferSyntaxes;
   transferSyntaxes.push_back ( UID_LittleEndianExplicitTransferSyntax );
   transferSyntaxes.push_back ( UID_BigEndianExplicitTransferSyntax );
   transferSyntaxes.push_back ( UID_LittleEndianImplicitTransferSyntax );
-  this->SCU.addPresentationContext ( UID_MOVEStudyRootQueryRetrieveInformationModel, transferSyntaxes );
+  this->SCU.addPresentationContext ( 
+      UID_MOVEStudyRootQueryRetrieveInformationModel, transferSyntaxes );
+  this->SCU.addPresentationContext ( 
+      UID_GETStudyRootQueryRetrieveInformationModel, transferSyntaxes );
+
+  for (Uint16 i = 0; i < numberOfDcmLongSCUStorageSOPClassUIDs; i++)
+    {
+    this->SCU.addPresentationContext(dcmLongSCUStorageSOPClassUIDs[i], 
+        transferSyntaxes, ASC_SC_ROLE_SCP);
+    }
 }
 
 //------------------------------------------------------------------------------
@@ -106,28 +196,17 @@ ctkDICOMRetrievePrivate::~ctkDICOMRetrievePrivate()
 }
 
 //------------------------------------------------------------------------------
-bool ctkDICOMRetrievePrivate::retrieve ( const QString& studyInstanceUID,
+bool ctkDICOMRetrievePrivate::initializeSCU( const QString& studyInstanceUID,
                                          const QString& seriesInstanceUID,
-                                         const RetrieveType rType )
+                                         const RetrieveType retrieveType,
+                                         DcmDataset *retrieveParameters)
 {
-
-  if ( !this->RetrieveDatabase )
+  if ( !this->Database )
     {
-    logger.error ( "No RetrieveDatabase for retrieve transaction" );
+    logger.error ( "No Database for retrieve transaction" );
     return false;
     }
 
-  // Register the JPEG libraries in case we need them
-  // (registration only happens once, so it's okay to call repeatedly)
-  // register global JPEG decompression codecs
-  DJDecoderRegistration::registerCodecs();
-  // register global JPEG compression codecs
-  DJEncoderRegistration::registerCodecs();
-  // register RLE compression codec
-  DcmRLEEncoderRegistration::registerCodecs();
-  // register RLE decompression codec
-  DcmRLEDecoderRegistration::registerCodecs();
-
   // If we like to query another server than before, be sure to disconnect first
   if (this->SCU.isConnected() && this->ConnectionParamsChanged)
   {
@@ -155,24 +234,44 @@ bool ctkDICOMRetrievePrivate::retrieve ( const QString& studyInstanceUID,
   this->ConnectionParamsChanged = false;
   // Setup query about what to be received from the PACS
   logger.debug ( "Setting Retrieve Parameters" );
-  DcmDataset *retrieveParameters = new DcmDataset();
-  if ( rType == RetrieveSeries )
+  if ( retrieveType == RetrieveSeries )
     {
     retrieveParameters->putAndInsertString ( DCM_QueryRetrieveLevel, "SERIES" );
-    retrieveParameters->putAndInsertString ( DCM_SeriesInstanceUID, seriesInstanceUID.toStdString().c_str() );
+    retrieveParameters->putAndInsertString ( DCM_SeriesInstanceUID, 
+                                                seriesInstanceUID.toStdString().c_str() );
     // Always required to send all highler level unique keys, so add study here (we are in Study Root)
-    retrieveParameters->putAndInsertString ( DCM_StudyInstanceUID, studyInstanceUID.toStdString().c_str() );  //TODO
+    retrieveParameters->putAndInsertString ( DCM_StudyInstanceUID, 
+                                                studyInstanceUID.toStdString().c_str() );  //TODO
     }
   else
     {
     retrieveParameters->putAndInsertString ( DCM_QueryRetrieveLevel, "STUDY" );
-    retrieveParameters->putAndInsertString ( DCM_StudyInstanceUID, studyInstanceUID.toStdString().c_str() );
+    retrieveParameters->putAndInsertString ( DCM_StudyInstanceUID, 
+                                                studyInstanceUID.toStdString().c_str() );
     }
+  return true;
+}
+
+//------------------------------------------------------------------------------
+bool ctkDICOMRetrievePrivate::move ( const QString& studyInstanceUID,
+                                         const QString& seriesInstanceUID,
+                                         const RetrieveType retrieveType )
+{
+
+  DcmDataset *retrieveParameters = new DcmDataset();
+  if (! this->initializeSCU(studyInstanceUID, seriesInstanceUID, retrieveType, retrieveParameters) )
+    {
+    delete retrieveParameters;
+    return false;
+    }
+
 
   // Issue request
   logger.debug ( "Sending Move Request" );
   OFList<RetrieveResponse*> responses;
-  T_ASC_PresentationContextID presID = this->SCU.findPresentationContextID(UID_MOVEStudyRootQueryRetrieveInformationModel, "" /* don't care about transfer syntax */ );
+  T_ASC_PresentationContextID presID = this->SCU.findPresentationContextID(
+                                          UID_MOVEStudyRootQueryRetrieveInformationModel, 
+                                          "" /* don't care about transfer syntax */ );
   if (presID == 0)
     {
     logger.error ( "MOVE Request failed: No valid Study Root MOVE Presentation Context available" );
@@ -180,9 +279,15 @@ bool ctkDICOMRetrievePrivate::retrieve ( const QString& studyInstanceUID,
       {
       this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION);
       }
+    delete retrieveParameters;
     return false;
     }
-  OFCondition status = this->SCU.sendMOVERequest ( presID, this->MoveDestinationAETitle.toStdString().c_str(), retrieveParameters, &responses );
+
+  // do the actual move request
+  OFCondition status = this->SCU.sendMOVERequest ( 
+                          presID, this->MoveDestinationAETitle.toStdString().c_str(), 
+                          retrieveParameters, &responses );
+
   // Close association if we do not want to explicitly keep it open
   if (!this->KeepAssociationOpen)
     {
@@ -195,7 +300,8 @@ bool ctkDICOMRetrievePrivate::retrieve ( const QString& studyInstanceUID,
   if ( responses.begin() == responses.end() )
     {
     logger.error ( "No responses received at all! (at least one empty response always expected)" );
-    throw std::runtime_error( std::string("No responses received from server!") );
+    //throw std::runtime_error( std::string("No responses received from server!") );
+    return false;
     }
 
   /* The server is permitted to acknowledge every image that was received, or
@@ -209,13 +315,17 @@ bool ctkDICOMRetrievePrivate::retrieve ( const QString& studyInstanceUID,
   if ( responses.size() == 1 )
     {
     RetrieveResponse* rsp = *responses.begin();
-    logger.debug ( "MOVE response receveid with status: " + QString(DU_cmoveStatusString(rsp->m_status)) );
-    if ((rsp->m_status == STATUS_Success) || (rsp->m_status == STATUS_MOVE_Warning_SubOperationsCompleteOneOrMoreFailures))
+    logger.debug ( "MOVE response receveid with status: " + 
+                      QString(DU_cmoveStatusString(rsp->m_status)) );
+
+    if ( (rsp->m_status == STATUS_Success) 
+            || (rsp->m_status == STATUS_MOVE_Warning_SubOperationsCompleteOneOrMoreFailures))
       {
       if (rsp->m_numberOfCompletedSubops == 0)
         {
         logger.error ( "No images transferred by PACS!" );
-        throw std::runtime_error( std::string("No images transferred by PACS!") );
+        //throw std::runtime_error( std::string("No images transferred by PACS!") );
+        return false;
         }
       }
     else
@@ -230,7 +340,8 @@ bool ctkDICOMRetrievePrivate::retrieve ( const QString& studyInstanceUID,
         }
       statusDetail.prepend("MOVE request failed: ");
       logger.error(statusDetail);
-      throw std::runtime_error( statusDetail.toStdString() );
+      //throw std::runtime_error( statusDetail.toStdString() );
+      return false;
       }
     }
     // Select the last MOVE response to output meaningful status information
@@ -241,9 +352,120 @@ bool ctkDICOMRetrievePrivate::retrieve ( const QString& studyInstanceUID,
     it++;
     }
   logger.debug ( "MOVE responses report for study: " + studyInstanceUID +"\n"
-    + QString::number(static_cast<unsigned int>((*it)->m_numberOfCompletedSubops)) + " images transferred, and\n"
-    + QString::number(static_cast<unsigned int>((*it)->m_numberOfWarningSubops))   + " images transferred with warning, and\n"
-    + QString::number(static_cast<unsigned int>((*it)->m_numberOfFailedSubops))    + " images transfers failed");
+    + QString::number(static_cast<unsigned int>((*it)->m_numberOfCompletedSubops))
+        + " images transferred, and\n"
+    + QString::number(static_cast<unsigned int>((*it)->m_numberOfWarningSubops))
+        + " images transferred with warning, and\n"
+    + QString::number(static_cast<unsigned int>((*it)->m_numberOfFailedSubops))
+        + " images transfers failed");
+
+  return true;
+}
+
+//------------------------------------------------------------------------------
+bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID,
+                                         const QString& seriesInstanceUID,
+                                         const RetrieveType retrieveType )
+{
+  DcmDataset *retrieveParameters = new DcmDataset();
+  if (! this->initializeSCU(studyInstanceUID, seriesInstanceUID, retrieveType, retrieveParameters) )
+    {
+    delete retrieveParameters;
+    return false;
+    }
+
+  // Issue request
+  logger.debug ( "Sending Get Request" );
+  OFList<RetrieveResponse*> responses;
+  T_ASC_PresentationContextID presID = this->SCU.findPresentationContextID(
+                                          UID_GETStudyRootQueryRetrieveInformationModel, 
+                                          "" /* don't care about transfer syntax */ );
+  if (presID == 0)
+    {
+    logger.error ( "GET Request failed: No valid Study Root GET Presentation Context available" );
+    if (!this->KeepAssociationOpen)
+      {
+      this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION);
+      }
+    delete retrieveParameters;
+    return false;
+    }
+
+
+  // do the actual move request
+  OFCondition status = this->SCU.sendCGETRequest ( 
+                          presID, retrieveParameters, &responses );
+
+  // Close association if we do not want to explicitly keep it open
+  if (!this->KeepAssociationOpen)
+    {
+    this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION);
+    }
+  // Free some (little) memory
+  delete retrieveParameters;
+
+  // If we do not receive a single response, something is fishy
+  if ( responses.begin() == responses.end() )
+    {
+    logger.error ( "No responses received at all! (at least one empty response always expected)" );
+    //throw std::runtime_error( std::string("No responses received from server!") );
+    return false;
+    }
+
+  /* The server is permitted to acknowledge every image that was received, or
+   * to send a single move response.
+   * If there is only a single response, this can mean the following:
+   * 1) No images to transfer (Status Success and Number of Completed Subops = 0)
+   * 2) All images transferred (Status Success and Number of Completed Subops > 0)
+   * 3) Error code, i.e. no images transferred
+   * 4) Warning (one or more failures, i.e. some images transferred)
+   */
+  if ( responses.size() == 1 )
+    {
+    RetrieveResponse* rsp = *responses.begin();
+    logger.debug ( "GET response receveid with status: " + 
+                      QString(DU_cmoveStatusString(rsp->m_status)) );
+
+    if ( (rsp->m_status == STATUS_Success) 
+            || (rsp->m_status == STATUS_GET_Warning_SubOperationsCompleteOneOrMoreFailures))
+      {
+      if (rsp->m_numberOfCompletedSubops == 0)
+        {
+        logger.error ( "No images transferred by PACS!" );
+        //throw std::runtime_error( std::string("No images transferred by PACS!") );
+        return false;
+        }
+      }
+    else
+      {
+      logger.error("GET request failed, server does report error");
+      QString statusDetail("No details");
+      if (rsp->m_statusDetail != NULL)
+        {
+         std::ostringstream out;
+        rsp->m_statusDetail->print(out);
+        statusDetail = "Status Detail: " + statusDetail.fromStdString(out.str());
+        }
+      statusDetail.prepend("GET request failed: ");
+      logger.error(statusDetail);
+      //throw std::runtime_error( statusDetail.toStdString() );
+      return false;
+      }
+    }
+    // Select the last GET response to output meaningful status information
+    OFIterator<RetrieveResponse*> it = responses.begin();
+  Uint32 numResults = responses.size();
+  for (Uint32 i = 1; i < numResults; i++)
+    {
+    it++;
+    }
+  logger.debug ( "GET responses report for study: " + studyInstanceUID +"\n"
+    + QString::number(static_cast<unsigned int>((*it)->m_numberOfCompletedSubops))
+        + " images transferred, and\n"
+    + QString::number(static_cast<unsigned int>((*it)->m_numberOfWarningSubops))
+        + " images transferred with warning, and\n"
+    + QString::number(static_cast<unsigned int>((*it)->m_numberOfFailedSubops))
+        + " images transfers failed");
 
   return true;
 }
@@ -255,6 +477,8 @@ bool ctkDICOMRetrievePrivate::retrieve ( const QString& studyInstanceUID,
 ctkDICOMRetrieve::ctkDICOMRetrieve()
    : d_ptr(new ctkDICOMRetrievePrivate)
 {
+  Q_D(ctkDICOMRetrieve);
+  d->SCU.retrieve = this; // give the dcmtk level access to this for emitting signals
 }
 
 //------------------------------------------------------------------------------
@@ -318,7 +542,7 @@ QString ctkDICOMRetrieve::host()const
 }
 
 //------------------------------------------------------------------------------
-void ctkDICOMRetrieve::setCalledPort( int port )
+void ctkDICOMRetrieve::setPort( int port )
 {
   Q_D(ctkDICOMRetrieve);
   if (d->SCU.getPeerPort() != port)
@@ -329,7 +553,7 @@ void ctkDICOMRetrieve::setCalledPort( int port )
 }
 
 //------------------------------------------------------------------------------
-int ctkDICOMRetrieve::calledPort()const
+int ctkDICOMRetrieve::port()const
 {
   Q_D(const ctkDICOMRetrieve);
   return d->SCU.getPeerPort();
@@ -353,18 +577,17 @@ QString ctkDICOMRetrieve::moveDestinationAETitle()const
 }
 
 //------------------------------------------------------------------------------
-void ctkDICOMRetrieve::setRetrieveDatabase(QSharedPointer<ctkDICOMDatabase> dicomDatabase)
+void ctkDICOMRetrieve::setDatabase(QSharedPointer<ctkDICOMDatabase> dicomDatabase)
 {
   Q_D(ctkDICOMRetrieve);
-  d->RetrieveDatabase = dicomDatabase;
-  // (server parameters do not have to be changed)
+  d->Database = dicomDatabase;
 }
 
 //------------------------------------------------------------------------------
-QSharedPointer<ctkDICOMDatabase> ctkDICOMRetrieve::retrieveDatabase()const
+QSharedPointer<ctkDICOMDatabase> ctkDICOMRetrieve::database()const
 {
   Q_D(const ctkDICOMRetrieve);
-  return d->RetrieveDatabase;
+  return d->Database;
 }
 
 //------------------------------------------------------------------------------
@@ -382,7 +605,33 @@ bool ctkDICOMRetrieve::keepAssociationOpen()
 }
 
 //------------------------------------------------------------------------------
-bool ctkDICOMRetrieve::retrieveSeries(const QString& studyInstanceUID,
+bool ctkDICOMRetrieve::moveStudy(const QString& studyInstanceUID)
+{
+  if (studyInstanceUID.isEmpty())
+    {
+    logger.error("Cannot receive series: Study Instance UID empty.");
+    return false;
+    }
+  Q_D(ctkDICOMRetrieve);
+  logger.info ( "Starting moveStudy" );
+  return d->move ( studyInstanceUID, "", ctkDICOMRetrievePrivate::RetrieveStudy );
+}
+
+//------------------------------------------------------------------------------
+bool ctkDICOMRetrieve::getStudy(const QString& studyInstanceUID)
+{
+  if (studyInstanceUID.isEmpty())
+    {
+    logger.error("Cannot receive series: Study Instance UID empty.");
+    return false;
+    }
+  Q_D(ctkDICOMRetrieve);
+  logger.info ( "Starting getStudy" );
+  return d->get ( studyInstanceUID, "", ctkDICOMRetrievePrivate::RetrieveStudy );
+}
+
+//------------------------------------------------------------------------------
+bool ctkDICOMRetrieve::moveSeries(const QString& studyInstanceUID,
                                       const QString& seriesInstanceUID)
 {
   if (studyInstanceUID.isEmpty() || seriesInstanceUID.isEmpty())
@@ -391,19 +640,21 @@ bool ctkDICOMRetrieve::retrieveSeries(const QString& studyInstanceUID,
     return false;
     }
   Q_D(ctkDICOMRetrieve);
-  logger.info ( "Starting retrieveSeries" );
-  return d->retrieve ( studyInstanceUID, seriesInstanceUID, ctkDICOMRetrievePrivate::RetrieveSeries );
+  logger.info ( "Starting moveSeries" );
+  return d->move ( studyInstanceUID, seriesInstanceUID, ctkDICOMRetrievePrivate::RetrieveSeries );
 }
 
 //------------------------------------------------------------------------------
-bool ctkDICOMRetrieve::retrieveStudy( const QString& studyInstanceUID )
+bool ctkDICOMRetrieve::getSeries(const QString& studyInstanceUID,
+                                      const QString& seriesInstanceUID)
 {
-  if (studyInstanceUID.isEmpty())
+  if (studyInstanceUID.isEmpty() || seriesInstanceUID.isEmpty())
     {
     logger.error("Cannot receive series: Either Study or Series Instance UID empty.");
     return false;
     }
   Q_D(ctkDICOMRetrieve);
-  logger.info ( "Starting retrieveStudy" );
-  return d->retrieve ( studyInstanceUID, "", ctkDICOMRetrievePrivate::RetrieveStudy );
+  logger.info ( "Starting getSeries" );
+  return d->get ( studyInstanceUID, seriesInstanceUID, ctkDICOMRetrievePrivate::RetrieveSeries );
 }
+

+ 42 - 19
Libs/DICOM/Core/ctkDICOMRetrieve.h

@@ -39,52 +39,75 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMRetrieve : public QObject
   Q_PROPERTY(QString callingAETitle READ callingAETitle WRITE setCallingAETitle);
   Q_PROPERTY(QString calledAETitle READ calledAETitle WRITE setCallingAETitle);
   Q_PROPERTY(QString host READ host WRITE setHost);
-  Q_PROPERTY(int calledPort READ calledPort WRITE setCalledPort);
+  Q_PROPERTY(int port READ port WRITE setPort);
   Q_PROPERTY(QString moveDestinationAETitle READ moveDestinationAETitle WRITE setMoveDestinationAETitle)
+  Q_PROPERTY(bool keepAssociationOpen READ keepAssociationOpen WRITE setKeepAssociationOpen)
 
 public:
   explicit ctkDICOMRetrieve();
   virtual ~ctkDICOMRetrieve();
 
   /// Set methods for connectivity
-  /// CTK_AE
+  /// CTK_AE - the AE string by which the peer host might 
+  /// recognize your request
   void setCallingAETitle( const QString& callingAETitle );
   QString callingAETitle() const;
-  /// CTK_AE
+  /// CTK_AE - the AE of the serice of peer host that you are calling
+  /// which tells the host what you are requesting
   void setCalledAETitle( const QString& calledAETitle );
   QString calledAETitle() const;
-  /// localhost
+  /// peer hostname being connected to
   void setHost( const QString& host );
   QString host() const;
-  /// [0, 65365] 11112
-  void setCalledPort( int port );
-  int calledPort() const;
-  /// Typically CTK_CLIENT_AE
+  /// [0, 65365] port on peer host - e.g. 11112
+  void setPort( int port );
+  int port() const;
+  /// Typically CTK_STORE or similar - needs to be something that the
+  /// peer host knows about and is able to move data into
+  /// Only used when calling moveSeries or moveStudy
   void setMoveDestinationAETitle( const QString& moveDestinationAETitle );
   QString moveDestinationAETitle() const;
-
+  /// prefer to keep using the existing association to peer host when doing
+  /// multiple requests (default true)
   void setKeepAssociationOpen(const bool keepOpen);
   bool keepAssociationOpen();
-
-  /// method for database
-  void setRetrieveDatabase(QSharedPointer<ctkDICOMDatabase> dicomDatabase);
-  QSharedPointer<ctkDICOMDatabase> retrieveDatabase()const;
-
-  // Could be a slot...
-  bool retrieveSeries( const QString& studyInstanceUID,
+  /// where to insert new data sets obtained via get (must be set for
+  /// get to succeed
+  Q_INVOKABLE void setDatabase(QSharedPointer<ctkDICOMDatabase> dicomDatabase);
+  Q_INVOKABLE QSharedPointer<ctkDICOMDatabase> database()const;
+
+public slots:
+  /// Use CMOVE to ask peer host to store data to move destination
+  bool moveSeries( const QString& studyInstanceUID,
                        const QString& seriesInstanceUID );
-
-  bool retrieveStudy( const QString& studyInstanceUID );
+  /// Use CMOVE to ask peer host to store data to move destination
+  bool moveStudy( const QString& studyInstanceUID );
+  /// Use CGET to ask peer host to store data to us
+  bool getSeries( const QString& studyInstanceUID,
+                       const QString& seriesInstanceUID );
+  /// Use CGET to ask peer host to store data to us
+  bool getStudy( const QString& studyInstanceUID );
+
+signals:
+  //TODO: the signature of these signals will change
+  //from string to a more specific format when we decide
+  //what information to send
+  /// emitted when a move response has been received from dcmtk
+  void moveResponseHandled( const QString message );
+  /// emitted when a dataset is incoming from a CGET
+  void storeRequested( const QString message );
+  /// emitted when remote server sends us CGET responses
+  void retrieveStatusChanged( const QString message );
 
 protected:
   QScopedPointer<ctkDICOMRetrievePrivate> d_ptr;
 
 private:
-  void retrieve( QDir directory );
 
   Q_DECLARE_PRIVATE(ctkDICOMRetrieve);
   Q_DISABLE_COPY(ctkDICOMRetrieve);
 
+  friend class ctkDICOMRetrieveSCUPrivate;  // for access to status signals
 };
 
 

+ 25 - 20
Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp

@@ -104,22 +104,12 @@ void ctkDICOMQueryRetrieveWidgetPrivate::init()
   QObject::connect(this->CancelButton, SIGNAL(clicked()), q, SLOT(cancel()));
 
   this->results->setModel(&this->Model);
-  // TODO: use the checkable headerview when it becomes possible
-  // to select individual studies.  For now, assume that the 
-  // user will use the query terms to narrow down the transfer
-  /*
-  this->Model.setHeaderData(0, Qt::Horizontal, Qt::Unchecked, Qt::CheckStateRole);
-  QHeaderView* previousHeaderView = this->results->header();
-  ctkCheckableHeaderView* headerView =
-    new ctkCheckableHeaderView(Qt::Horizontal, this->results);
-  headerView->setClickable(previousHeaderView->isClickable());
-  headerView->setMovable(previousHeaderView->isMovable());
-  headerView->setHighlightSections(previousHeaderView->highlightSections());
-  headerView->checkableModelHelper()->setPropagateDepth(-1);
-  this->results->setHeader(headerView);
-  // headerView is hidden because it was created with a visisble parent widget 
-  headerView->setHidden(false);
-  */
+  this->results->setSelectionMode(QAbstractItemView::ExtendedSelection);
+  this->results->setSelectionBehavior(QAbstractItemView::SelectRows);
+
+  QObject::connect(this->results->selectionModel(), 
+    SIGNAL(selectionChanged (const QItemSelection &, const QItemSelection &)), 
+    q, SLOT(onSelectionChanged(const QItemSelection &, const QItemSelection &)));
 }
 
 //----------------------------------------------------------------------------
@@ -237,7 +227,6 @@ void ctkDICOMQueryRetrieveWidget::query()
   // checkable headers - allow user to select the patient/studies to retrieve
   d->Model.setDatabase(d->QueryResultDatabase.database());
 
-  d->RetrieveButton->setEnabled(d->Model.rowCount());
   progress.setValue(progress.maximum());
   d->ProgressDialog = 0;
 }
@@ -274,6 +263,8 @@ void ctkDICOMQueryRetrieveWidget::retrieve()
   progress.setMaximum(d->QueriesByStudyUID.keys().size());
   progress.open();
   progress.setValue(1);
+
+  // do the rerieval for each selected series
   foreach( QString studyUID, d->QueriesByStudyUID.keys() )
     {
     if (progress.wasCanceled())
@@ -287,10 +278,10 @@ void ctkDICOMQueryRetrieveWidget::retrieve()
 
     // Get information which server we want to get the study from and prepare request accordingly
     ctkDICOMQuery *query = d->QueriesByStudyUID[studyUID];
-    retrieve->setRetrieveDatabase( d->RetrieveDatabase );
+    retrieve->setDatabase( d->RetrieveDatabase );
     retrieve->setCallingAETitle( query->callingAETitle() );
     retrieve->setCalledAETitle( query->calledAETitle() );
-    retrieve->setCalledPort( query->port() );
+    retrieve->setPort( query->port() );
     retrieve->setHost( query->host() );
     // TODO: check the model item to see if it is checked
     // for now, assume all studies queried and shown to the user will be retrieved
@@ -300,7 +291,9 @@ void ctkDICOMQueryRetrieveWidget::retrieve()
     try
       {
       // perform the retrieve
-      retrieve->retrieveStudy ( studyUID );
+      // TODO: give the option to use MOVE instead of CGET
+      //retrieve->moveStudy ( studyUID );
+      retrieve->getStudy ( studyUID );
       }
     catch (std::exception e)
       {
@@ -386,3 +379,15 @@ void ctkDICOMQueryRetrieveWidget::updateRetrieveProgress(int value)
   d->ProgressDialog->setValue( value );
   logger.error(QString("setting value to %1").arg(value) );
 }
+
+//----------------------------------------------------------------------------
+void ctkDICOMQueryRetrieveWidget::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
+{
+  Q_UNUSED(selected);
+  Q_UNUSED(deselected);
+  Q_D(ctkDICOMQueryRetrieveWidget);
+
+  logger.debug("Selection change");
+  d->RetrieveButton->setEnabled(d->results->selectionModel()->hasSelection());
+}
+

+ 2 - 0
Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.h

@@ -25,6 +25,7 @@
 
 // Qt includes 
 #include <QWidget>
+#include <QItemSelection>
 
 
 // CTK includes
@@ -47,6 +48,7 @@ public Q_SLOTS:
   void query();
   void retrieve();
   void cancel();
+  void onSelectionChanged(const QItemSelection &, const QItemSelection &);
 
 Q_SIGNALS:
   /// Signal emit when studies have been retrieved (user clicked on the