1 /*=========================================================================
2 
3   Library:   CTK
4 
5   Copyright (c) Kitware Inc.
6 
7   Licensed under the Apache License, Version 2.0 (the "License");
8   you may not use this file except in compliance with the License.
9   You may obtain a copy of the License at
10 
11       http://www.apache.org/licenses/LICENSE-2.0.txt
12 
13   Unless required by applicable law or agreed to in writing, software
14   distributed under the License is distributed on an "AS IS" BASIS,
15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   See the License for the specific language governing permissions and
17   limitations under the License.
18 
19 =========================================================================*/
20 
21 // Qt includes
22 #include <QSqlQuery>
23 #include <QSqlRecord>
24 #include <QVariant>
25 #include <QDate>
26 #include <QStringList>
27 #include <QSet>
28 #include <QFile>
29 #include <QDirIterator>
30 #include <QFileInfo>
31 #include <QDebug>
32 
33 // ctkDICOMCore includes
34 #include "ctkDICOMQuery.h"
35 #include "ctkDICOMUtil.h"
36 #include "ctkLogger.h"
37 
38 // DCMTK includes
39 #include "dcmtk/dcmnet/dimse.h"
40 #include "dcmtk/dcmnet/diutil.h"
41 #include "dcmtk/dcmnet/scu.h"
42 
43 #include <dcmtk/dcmdata/dcfilefo.h>
44 #include <dcmtk/dcmdata/dcfilefo.h>
45 #include <dcmtk/dcmdata/dcdeftag.h>
46 #include <dcmtk/dcmdata/dcdatset.h>
47 #include <dcmtk/ofstd/ofcond.h>
48 #include <dcmtk/ofstd/ofstring.h>
49 #include <dcmtk/ofstd/oflist.h>
50 #include <dcmtk/ofstd/ofstd.h>        /* for class OFStandard */
51 #include <dcmtk/dcmdata/dcddirif.h>   /* for class DicomDirInterface */
52 
53 
54 static ctkLogger logger ( "org.commontk.dicom.DICOMQuery" );
55 
56 //------------------------------------------------------------------------------
57 // A customized implemenation so that Qt signals can be emitted
58 // when query results are obtained
59 class ctkDICOMQuerySCUPrivate : public DcmSCU
60 {
61 public:
62   ctkDICOMQuery *query;
ctkDICOMQuerySCUPrivate()63   ctkDICOMQuerySCUPrivate()
64     {
65     this->query = 0;
66     };
~ctkDICOMQuerySCUPrivate()67   ~ctkDICOMQuerySCUPrivate() {};
handleFINDResponse(const T_ASC_PresentationContextID presID,QRResponse * response,OFBool & waitForNextResponse)68   virtual OFCondition handleFINDResponse(const T_ASC_PresentationContextID  presID,
69                                          QRResponse *response,
70                                          OFBool &waitForNextResponse)
71     {
72       if (this->query)
73         {
74         logger.debug ( "FIND RESPONSE" );
75         emit this->query->debug("Got a find response!");
76         return this->DcmSCU::handleFINDResponse(presID, response, waitForNextResponse);
77         }
78       return DIMSE_NULLKEY;
79     };
80 };
81 
82 //------------------------------------------------------------------------------
83 class ctkDICOMQueryPrivate
84 {
85 public:
86   ctkDICOMQueryPrivate();
87   ~ctkDICOMQueryPrivate();
88 
89   /// Add a StudyInstanceUID to be queried
90   void addStudyInstanceUIDAndDataset(const QString& StudyInstanceUID, DcmDataset* dataset );
91 
92   QString                 CallingAETitle;
93   QString                 CalledAETitle;
94   QString                 Host;
95   int                     Port;
96   bool                    PreferCGET;
97   QMap<QString,QVariant>  Filters;
98   ctkDICOMQuerySCUPrivate SCU;
99   DcmDataset*             Query;
100   QStringList             StudyInstanceUIDList;
101   QList<DcmDataset*>      StudyDatasetList;
102   bool                    Canceled;
103 };
104 
105 //------------------------------------------------------------------------------
106 // ctkDICOMQueryPrivate methods
107 
108 //------------------------------------------------------------------------------
ctkDICOMQueryPrivate()109 ctkDICOMQueryPrivate::ctkDICOMQueryPrivate()
110 {
111   this->Query = new DcmDataset();
112   this->Port = 0;
113   this->Canceled = false;
114   this->PreferCGET = false;
115 }
116 
117 //------------------------------------------------------------------------------
~ctkDICOMQueryPrivate()118 ctkDICOMQueryPrivate::~ctkDICOMQueryPrivate()
119 {
120   delete this->Query;
121 }
122 
123 //------------------------------------------------------------------------------
addStudyInstanceUIDAndDataset(const QString & s,DcmDataset * dataset)124 void ctkDICOMQueryPrivate::addStudyInstanceUIDAndDataset( const QString& s, DcmDataset* dataset )
125 {
126   this->StudyInstanceUIDList.append ( s );
127   this->StudyDatasetList.append ( dataset );
128 }
129 
130 //------------------------------------------------------------------------------
131 // ctkDICOMQuery methods
132 
133 //------------------------------------------------------------------------------
ctkDICOMQuery(QObject * parentObject)134 ctkDICOMQuery::ctkDICOMQuery(QObject* parentObject)
135   : QObject(parentObject)
136   , d_ptr(new ctkDICOMQueryPrivate)
137 {
138   Q_D(ctkDICOMQuery);
139   d->SCU.query = this; // give the dcmtk level access to this for emitting signals
140 }
141 
142 //------------------------------------------------------------------------------
~ctkDICOMQuery()143 ctkDICOMQuery::~ctkDICOMQuery()
144 {
145 }
146 
147 /// Set methods for connectivity
148 //------------------------------------------------------------------------------
setCallingAETitle(const QString & callingAETitle)149 void ctkDICOMQuery::setCallingAETitle( const QString& callingAETitle )
150 {
151   Q_D(ctkDICOMQuery);
152   d->CallingAETitle = callingAETitle;
153 }
154 
155 //------------------------------------------------------------------------------
callingAETitle() const156 QString ctkDICOMQuery::callingAETitle() const
157 {
158   Q_D(const ctkDICOMQuery);
159   return d->CallingAETitle;
160 }
161 
162 //------------------------------------------------------------------------------
setCalledAETitle(const QString & calledAETitle)163 void ctkDICOMQuery::setCalledAETitle( const QString& calledAETitle )
164 {
165   Q_D(ctkDICOMQuery);
166   d->CalledAETitle = calledAETitle;
167 }
168 
169 //------------------------------------------------------------------------------
calledAETitle() const170 QString ctkDICOMQuery::calledAETitle()const
171 {
172   Q_D(const ctkDICOMQuery);
173   return d->CalledAETitle;
174 }
175 
176 //------------------------------------------------------------------------------
setHost(const QString & host)177 void ctkDICOMQuery::setHost( const QString& host )
178 {
179   Q_D(ctkDICOMQuery);
180   d->Host = host;
181 }
182 
183 //------------------------------------------------------------------------------
host() const184 QString ctkDICOMQuery::host() const
185 {
186   Q_D(const ctkDICOMQuery);
187   return d->Host;
188 }
189 
190 //------------------------------------------------------------------------------
setPort(int port)191 void ctkDICOMQuery::setPort ( int port )
192 {
193   Q_D(ctkDICOMQuery);
194   d->Port = port;
195 }
196 
197 //------------------------------------------------------------------------------
port() const198 int ctkDICOMQuery::port()const
199 {
200   Q_D(const ctkDICOMQuery);
201   return d->Port;
202 }
203 
204 //------------------------------------------------------------------------------
setPreferCGET(bool preferCGET)205 void ctkDICOMQuery::setPreferCGET ( bool preferCGET )
206 {
207   Q_D(ctkDICOMQuery);
208   d->PreferCGET = preferCGET;
209 }
210 
211 //------------------------------------------------------------------------------
preferCGET() const212 bool ctkDICOMQuery::preferCGET()const
213 {
214   Q_D(const ctkDICOMQuery);
215   return d->PreferCGET;
216 }
217 
218 //------------------------------------------------------------------------------
setFilters(const QMap<QString,QVariant> & filters)219 void ctkDICOMQuery::setFilters( const QMap<QString,QVariant>& filters )
220 {
221   Q_D(ctkDICOMQuery);
222   d->Filters = filters;
223 }
224 
225 //------------------------------------------------------------------------------
filters() const226 QMap<QString,QVariant> ctkDICOMQuery::filters()const
227 {
228   Q_D(const ctkDICOMQuery);
229   return d->Filters;
230 }
231 
232 //------------------------------------------------------------------------------
studyInstanceUIDQueried() const233 QStringList ctkDICOMQuery::studyInstanceUIDQueried()const
234 {
235   Q_D(const ctkDICOMQuery);
236   return d->StudyInstanceUIDList;
237 }
238 
239 //------------------------------------------------------------------------------
query(ctkDICOMDatabase & database)240 bool ctkDICOMQuery::query(ctkDICOMDatabase& database )
241 {
242   // turn on logging if needed for debug:
243   //ctk::setDICOMLogLevel(ctkErrorLogLevel::Debug);
244 
245   // ctkDICOMDatabase::setDatabase ( database );
246   Q_D(ctkDICOMQuery);
247   // In the following, we emit progress(int) after progress(QString), this
248   // is in case the connected object doesn't refresh its ui when the progress
249   // message is updated but only if the progress value is (e.g. QProgressDialog)
250   if ( database.database().isOpen() )
251     {
252     logger.debug ( "DB open in Query" );
253     emit progress("DB open in Query");
254     }
255   else
256     {
257     logger.debug ( "DB not open in Query" );
258     emit progress("DB not open in Query");
259     }
260   emit progress(0);
261   if (d->Canceled) {return false;}
262 
263   d->StudyInstanceUIDList.clear();
264   d->SCU.setAETitle ( OFString(this->callingAETitle().toStdString().c_str()) );
265   d->SCU.setPeerAETitle ( OFString(this->calledAETitle().toStdString().c_str()) );
266   d->SCU.setPeerHostName ( OFString(this->host().toStdString().c_str()) );
267   d->SCU.setPeerPort ( this->port() );
268 
269   logger.error ( "Setting Transfer Syntaxes" );
270   emit progress("Setting Transfer Syntaxes");
271   emit progress(10);
272   if (d->Canceled) {return false;}
273 
274   OFList<OFString> transferSyntaxes;
275   transferSyntaxes.push_back ( UID_LittleEndianExplicitTransferSyntax );
276   transferSyntaxes.push_back ( UID_BigEndianExplicitTransferSyntax );
277   transferSyntaxes.push_back ( UID_LittleEndianImplicitTransferSyntax );
278 
279   d->SCU.addPresentationContext ( UID_FINDStudyRootQueryRetrieveInformationModel, transferSyntaxes );
280   // d->SCU.addPresentationContext ( UID_VerificationSOPClass, transferSyntaxes );
281   if ( !d->SCU.initNetwork().good() )
282     {
283     logger.error( "Error initializing the network" );
284     emit progress("Error initializing the network");
285     emit progress(100);
286     return false;
287     }
288   logger.debug ( "Negotiating Association" );
289   emit progress("Negotiating Association");
290   emit progress(20);
291   if (d->Canceled) {return false;}
292 
293   OFCondition result = d->SCU.negotiateAssociation();
294   if (result.bad())
295     {
296     logger.error( "Error negotiating the association: " + QString(result.text()) );
297     emit progress("Error negotiating the association");
298     emit progress(100);
299     return false;
300     }
301 
302   // Clear the query
303   d->Query->clear();
304 
305   // Insert all keys that we like to receive values for
306   d->Query->insertEmptyElement ( DCM_PatientID );
307   d->Query->insertEmptyElement ( DCM_PatientName );
308   d->Query->insertEmptyElement ( DCM_PatientBirthDate );
309   d->Query->insertEmptyElement ( DCM_StudyID );
310   d->Query->insertEmptyElement ( DCM_StudyInstanceUID );
311   d->Query->insertEmptyElement ( DCM_StudyDescription );
312   d->Query->insertEmptyElement ( DCM_StudyDate );
313   d->Query->insertEmptyElement ( DCM_StudyTime );
314   d->Query->insertEmptyElement ( DCM_ModalitiesInStudy );
315   d->Query->insertEmptyElement ( DCM_AccessionNumber );
316   d->Query->insertEmptyElement ( DCM_NumberOfStudyRelatedInstances ); // Number of images in the series
317   d->Query->insertEmptyElement ( DCM_NumberOfStudyRelatedSeries ); // Number of series in the study
318 
319   // Make clear we define our search values in ISO Latin 1 (default would be ASCII)
320   d->Query->putAndInsertOFStringArray(DCM_SpecificCharacterSet, "ISO_IR 100");
321 
322   d->Query->putAndInsertString ( DCM_QueryRetrieveLevel, "STUDY" );
323 
324   /* Now, for all keys that the user provided for filtering on STUDY level,
325    * overwrite empty keys with value. For now, only Patient's Name, Patient ID,
326    * Study Description, Modalities in Study, and Study Date are used.
327    */
328   QString seriesDescription;
329   foreach( QString key, d->Filters.keys() )
330     {
331     if ( key == QString("Name") && !d->Filters[key].toString().isEmpty())
332       {
333       // make the filter a wildcard in dicom style
334       d->Query->putAndInsertString( DCM_PatientName,
335         (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data());
336       }
337     else if ( key == QString("Study") && !d->Filters[key].toString().isEmpty())
338       {
339       // make the filter a wildcard in dicom style
340       d->Query->putAndInsertString( DCM_StudyDescription,
341         (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data());
342       }
343     else if ( key == QString("ID") && !d->Filters[key].toString().isEmpty())
344       {
345       // make the filter a wildcard in dicom style
346       d->Query->putAndInsertString( DCM_PatientID,
347         (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data());
348       }
349     else if ( key == QString("Modalities") && !d->Filters[key].toString().isEmpty())
350       {
351       // make the filter be an "OR" of modalities using backslash (dicom-style)
352       QString modalitySearch("");
353       foreach (const QString& modality, d->Filters[key].toStringList())
354       {
355         modalitySearch += modality + QString("\\");
356       }
357       modalitySearch.chop(1); // remove final backslash
358       logger.debug("modalityInStudySearch " + modalitySearch);
359       d->Query->putAndInsertString( DCM_ModalitiesInStudy, modalitySearch.toLatin1().data() );
360       }
361     // Rememer Series Description for later series query if we go through the keys now
362     else if ( key == QString("Series") && !d->Filters[key].toString().isEmpty())
363       {
364       // make the filter a wildcard in dicom style
365       seriesDescription = "*" + d->Filters[key].toString() + "*";
366       }
367     else
368       {
369       logger.debug("Ignoring unknown search key: " + key);
370       }
371     }
372 
373   if ( d->Filters.keys().contains("StartDate") && d->Filters.keys().contains("EndDate") )
374     {
375     QString dateRange = d->Filters["StartDate"].toString() +
376                         QString("-") +
377                         d->Filters["EndDate"].toString();
378     d->Query->putAndInsertString ( DCM_StudyDate, dateRange.toLatin1().data() );
379     logger.debug("Query on study date " + dateRange);
380     }
381   emit progress(30);
382   if (d->Canceled) {return false;}
383 
384   OFList<QRResponse *> responses;
385 
386   Uint16 presentationContext = 0;
387   // Check for any accepted presentation context for FIND in study root (dont care about transfer syntax)
388   presentationContext = d->SCU.findPresentationContextID ( UID_FINDStudyRootQueryRetrieveInformationModel, "");
389   if ( presentationContext == 0 )
390     {
391     logger.error ( "Failed to find acceptable presentation context" );
392     emit progress("Failed to find acceptable presentation context");
393     }
394   else
395     {
396     logger.info ( "Found useful presentation context" );
397     emit progress("Found useful presentation context");
398     }
399   emit progress(40);
400   if (d->Canceled) {return false;}
401 
402   OFCondition status = d->SCU.sendFINDRequest ( presentationContext, d->Query, &responses );
403   if ( !status.good() )
404     {
405     logger.error ( "Find failed" );
406     emit progress("Find failed");
407     d->SCU.closeAssociation ( DCMSCU_RELEASE_ASSOCIATION );
408     emit progress(100);
409     return false;
410     }
411   logger.debug ( "Find succeded");
412   emit progress("Find succeded");
413   emit progress(50);
414   if (d->Canceled) {return false;}
415 
416   for ( OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++ )
417     {
418     DcmDataset *dataset = (*it)->m_dataset;
419     if ( dataset != NULL ) // the last response is always empty
420       {
421       database.insert ( dataset, false /* do not store to disk*/, false /* no thumbnail*/);
422       OFString StudyInstanceUID;
423       dataset->findAndGetOFString ( DCM_StudyInstanceUID, StudyInstanceUID );
424       d->addStudyInstanceUIDAndDataset ( StudyInstanceUID.c_str(), dataset );
425       emit progress(QString("Processing: ") + QString(StudyInstanceUID.c_str()));
426       emit progress(50);
427       if (d->Canceled) {return false;}
428       }
429     }
430 
431   /* Only ask for series attributes now. This requires kicking out the rest of former query. */
432   d->Query->clear();
433   d->Query->insertEmptyElement ( DCM_SeriesNumber );
434   d->Query->insertEmptyElement ( DCM_SeriesDescription );
435   d->Query->insertEmptyElement ( DCM_SeriesInstanceUID );
436   d->Query->insertEmptyElement ( DCM_SeriesDate );
437   d->Query->insertEmptyElement ( DCM_SeriesTime );
438   d->Query->insertEmptyElement ( DCM_Modality );
439   d->Query->insertEmptyElement ( DCM_NumberOfSeriesRelatedInstances ); // Number of images in the series
440 
441   /* Add user-defined filters */
442   d->Query->putAndInsertOFStringArray(DCM_SeriesDescription, seriesDescription.toLatin1().data());
443 
444   // Now search each within each Study that was identified
445   d->Query->putAndInsertString ( DCM_QueryRetrieveLevel, "SERIES" );
446   float progressRatio = 25. / d->StudyInstanceUIDList.count();
447   int i = 0;
448 
449   QListIterator<DcmDataset*> datasetIterator(d->StudyDatasetList);
450   foreach ( QString StudyInstanceUID, d->StudyInstanceUIDList )
451     {
452     DcmDataset *studyDataset = datasetIterator.next();
453     DcmElement *patientName, *patientID;
454     studyDataset->findAndGetElement(DCM_PatientName, patientName);
455     studyDataset->findAndGetElement(DCM_PatientID, patientID);
456 
457     logger.debug ( "Starting Series C-FIND for Study: " + StudyInstanceUID );
458     emit progress(QString("Starting Series C-FIND for Study: ") + StudyInstanceUID);
459     emit progress(50 + (progressRatio * i++));
460     if (d->Canceled) {return false;}
461 
462     d->Query->putAndInsertString ( DCM_StudyInstanceUID, StudyInstanceUID.toStdString().c_str() );
463     OFList<QRResponse *> responses;
464     status = d->SCU.sendFINDRequest ( presentationContext, d->Query, &responses );
465     if ( status.good() )
466       {
467       for ( OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++ )
468         {
469         DcmDataset *dataset = (*it)->m_dataset;
470         if ( dataset != NULL )
471           {
472           // add the patient elements not provided for the series level query
473           dataset->insert( patientName, true );
474           dataset->insert( patientID, true );
475           // insert series dataset
476           database.insert ( dataset, false /* do not store */, false /* no thumbnail */ );
477           }
478         }
479       logger.debug ( "Find succeded on Series level for Study: " + StudyInstanceUID );
480       emit progress(QString("Find succeded on Series level for Study: ") + StudyInstanceUID);
481       emit progress(50 + (progressRatio * i++));
482       if (d->Canceled) {return false;}
483       }
484     else
485       {
486       logger.error ( "Find on Series level failed for Study: " + StudyInstanceUID );
487       emit progress(QString("Find on Series level failed for Study: ") + StudyInstanceUID);
488       }
489     emit progress(50 + (progressRatio * i++));
490     if (d->Canceled) {return false;}
491     }
492   d->SCU.closeAssociation ( DCMSCU_RELEASE_ASSOCIATION );
493   emit progress(100);
494   return true;
495 }
496 
497 //----------------------------------------------------------------------------
cancel()498 void ctkDICOMQuery::cancel()
499 {
500   Q_D(ctkDICOMQuery);
501   d->Canceled = true;
502 }
503