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