diff --git a/Applications/ctkDICOMVisualBrowser/CMakeLists.txt b/Applications/ctkDICOMVisualBrowser/CMakeLists.txt new file mode 100644 index 0000000000..448ffa5e37 --- /dev/null +++ b/Applications/ctkDICOMVisualBrowser/CMakeLists.txt @@ -0,0 +1,40 @@ +project(ctkDICOMVisualBrowser) + +# +# See CTK/CMake/ctkMacroBuildApp.cmake for details +# + +# Source files +set(KIT_SRCS + ctkDICOMVisualBrowserMain.cpp + ) + +# Headers that should run through moc +set(KIT_MOC_SRCS + ) + +# UI files +set(KIT_UI_FORMS +) + +# Resources +set(KIT_resources +) + +# Target libraries - See CMake/ctkFunctionGetTargetLibraries.cmake +# The following macro will read the target libraries from the file 'target_libraries.cmake' +ctkFunctionGetTargetLibraries(KIT_target_libraries) + +ctkMacroBuildApp( + NAME ${PROJECT_NAME} + SRCS ${KIT_SRCS} + MOC_SRCS ${KIT_MOC_SRCS} + UI_FORMS ${KIT_UI_FORMS} + TARGET_LIBRARIES ${KIT_target_libraries} + RESOURCES ${KIT_resources} + ) + +# Testing +if(BUILD_TESTING) + add_subdirectory(Testing) +endif() diff --git a/Applications/ctkDICOMVisualBrowser/Testing/CMakeLists.txt b/Applications/ctkDICOMVisualBrowser/Testing/CMakeLists.txt new file mode 100644 index 0000000000..cdeb442a1d --- /dev/null +++ b/Applications/ctkDICOMVisualBrowser/Testing/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(Cpp) diff --git a/Applications/ctkDICOMVisualBrowser/Testing/Cpp/CMakeLists.txt b/Applications/ctkDICOMVisualBrowser/Testing/Cpp/CMakeLists.txt new file mode 100644 index 0000000000..4524b0b494 --- /dev/null +++ b/Applications/ctkDICOMVisualBrowser/Testing/Cpp/CMakeLists.txt @@ -0,0 +1,21 @@ +set(KIT ${PROJECT_NAME}) + +create_test_sourcelist(Tests ${KIT}CppTests.cpp + ctkDICOMVisualBrowserTest1.cpp + ) + +SET (TestsToRun ${Tests}) +REMOVE (TestsToRun ${KIT}CppTests.cpp) + +# Target libraries - See CMake/ctkFunctionGetTargetLibraries.cmake +# The following macro will read the target libraries from the file '/target_libraries.cmake' +ctkFunctionGetTargetLibraries(KIT_target_libraries ${${KIT}_SOURCE_DIR}) + +ctk_add_executable_utf8(${KIT}CppTests ${Tests}) +target_link_libraries(${KIT}CppTests ${KIT_target_libraries}) + +# +# Add Tests +# + +SIMPLE_TEST(ctkDICOMVisualBrowserTest1 $) diff --git a/Applications/ctkDICOMVisualBrowser/Testing/Cpp/ctkDICOMVisualBrowserTest1.cpp b/Applications/ctkDICOMVisualBrowser/Testing/Cpp/ctkDICOMVisualBrowserTest1.cpp new file mode 100644 index 0000000000..b7f267ef51 --- /dev/null +++ b/Applications/ctkDICOMVisualBrowser/Testing/Cpp/ctkDICOMVisualBrowserTest1.cpp @@ -0,0 +1,58 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +=========================================================================*/ + +// Qt includes +#include +#include + +// STD includes +#include +#include + +int ctkDICOMVisualBrowserTest1(int argc, char * argv []) +{ + QCoreApplication app(argc, argv); + if (app.arguments().count() != 2) + { + std::cerr << "Line " << __LINE__ << " - Failed to run " << argv[0] << "\n" + << "Usage:\n" + << " " << argv[0] << " /path/to/ctkDICOM"; + return EXIT_FAILURE; + } + QString command = app.arguments().at(1); + QProcess process; + process.start(command, /* arguments= */ QStringList()); + bool res = process.waitForStarted(); + if (!res) + { + std::cerr << '\"' << qPrintable(command) << '\"' + << " didn't start correctly" << std::endl; + return res ? EXIT_SUCCESS : EXIT_FAILURE; + } + process.kill(); + res = process.waitForFinished(); + if (!res) + { + std::cerr << '\"' << qPrintable(command) << '\"' + << " failed to terminate" << std::endl; + return res ? EXIT_SUCCESS : EXIT_FAILURE; + } + return res ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/Applications/ctkDICOMVisualBrowser/ctkDICOMVisualBrowserMain.cpp b/Applications/ctkDICOMVisualBrowser/ctkDICOMVisualBrowserMain.cpp new file mode 100644 index 0000000000..3e00123f54 --- /dev/null +++ b/Applications/ctkDICOMVisualBrowser/ctkDICOMVisualBrowserMain.cpp @@ -0,0 +1,99 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Isomics Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include +#include +#include +#include +#include + +// CTK widget includes +#include +#include +#include + +// STD includes +#include + +int main(int argc, char** argv) +{ + QApplication app(argc, argv); + + app.setOrganizationName("commontk"); + app.setOrganizationDomain("commontk.org"); + app.setApplicationName("ctkDICOM"); + + // set up Qt resource files + QResource::registerResource("./Resources/ctkDICOM.qrc"); + + QWidget mainWidget; + mainWidget.setObjectName(QString::fromUtf8("MainWidget")); + mainWidget.setWindowTitle(QString::fromUtf8("DICOM Visual Browser")); + + QVBoxLayout mainLayout; + mainLayout.setObjectName(QString::fromUtf8("mainLayout")); + mainLayout.setContentsMargins(1, 1, 1, 1); + + QHBoxLayout topLayout; + topLayout.setObjectName(QString::fromUtf8("topLayout")); + topLayout.setContentsMargins(1, 1, 1, 1); + + QLabel databaseNameLabel; + databaseNameLabel.setObjectName(QString::fromUtf8("DatabaseNameLabel")); + databaseNameLabel.setMaximumSize(QSize(100, 30)); + topLayout.addWidget(&databaseNameLabel); + + ctkDirectoryButton directoryButton; + directoryButton.setObjectName(QString::fromUtf8("DirectoryButton")); + directoryButton.setMinimumSize(QSize(200, 30)); + if (argc > 1) + { + directoryButton.setDirectory(argv[1]); + } + topLayout.addWidget(&directoryButton); + + mainLayout.addLayout(&topLayout); + + ctkDICOMVisualBrowserWidget DICOMVisualBrowser; + DICOMVisualBrowser.setObjectName(QString::fromUtf8("DICOMVisualBrowser")); + DICOMVisualBrowser.setDatabaseDirectorySettingsKey("DatabaseDirectory"); + DICOMVisualBrowser.setMinimumSize(QSize(1000, 1000)); + // set up the database + if (argc > 1) + { + DICOMVisualBrowser.setDatabaseDirectory(argv[1]); + } + + DICOMVisualBrowser.serverSettingsGroupBox()->setChecked(true); + QObject::connect(&directoryButton, SIGNAL(directoryChanged(const QString&)), + &DICOMVisualBrowser, SLOT(setDatabaseDirectory(const QString&))); + + mainLayout.addWidget(&DICOMVisualBrowser); + + mainWidget.setLayout(&mainLayout); + + mainWidget.show(); + + return app.exec(); +} diff --git a/Applications/ctkDICOMVisualBrowser/target_libraries.cmake b/Applications/ctkDICOMVisualBrowser/target_libraries.cmake new file mode 100644 index 0000000000..0b3ea7caa4 --- /dev/null +++ b/Applications/ctkDICOMVisualBrowser/target_libraries.cmake @@ -0,0 +1,9 @@ +# +# See CMake/ctkFunctionGetTargetLibraries.cmake +# +# This file should list the libraries required to build the current CTK application. +# + +set(target_libraries + CTKDICOMWidgets + ) diff --git a/CMake/ctkMacroSetupQt.cmake b/CMake/ctkMacroSetupQt.cmake index cfd93cf345..9777ad4297 100644 --- a/CMake/ctkMacroSetupQt.cmake +++ b/CMake/ctkMacroSetupQt.cmake @@ -29,6 +29,12 @@ macro(ctkMacroSetupQt) # See https://github.com/commontk/CTK/wiki/Maintenance#updates-of-required-qt-components + if(CTK_LIB_Widgets + OR CTK_LIB_DICOM/Widgets + ) + list(APPEND CTK_QT5_COMPONENTS Svg) + endif() + if(CTK_LIB_Widgets OR CTK_LIB_Scripting/Python/Core_PYTHONQT_WRAP_QTXML ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f3982f4f7..48c4ef83b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -550,6 +550,10 @@ ctk_app_option(ctkDICOM2 "Build the new DICOM example application (experimental)" OFF CTK_ENABLE_DICOM AND CTK_BUILD_EXAMPLES) +ctk_app_option(ctkDICOMVisualBrowser + "Build the new DICOM example application (experimental)" OFF + CTK_ENABLE_DICOM AND CTK_BUILD_EXAMPLES) + ctk_app_option(ctkDICOMIndexer "Build the DICOM example application" OFF CTK_ENABLE_DICOM AND CTK_BUILD_EXAMPLES) diff --git a/Libs/Core/CMakeLists.txt b/Libs/Core/CMakeLists.txt index 1873e89be5..de35547864 100644 --- a/Libs/Core/CMakeLists.txt +++ b/Libs/Core/CMakeLists.txt @@ -22,6 +22,8 @@ set(KIT_SRCS ctkAbstractFactory.tpp ctkAbstractFileBasedFactory.h ctkAbstractFileBasedFactory.tpp + ctkAbstractJob.cpp + ctkAbstractJob.h ctkAbstractObjectFactory.h ctkAbstractObjectFactory.tpp ctkAbstractPluginFactory.h @@ -30,6 +32,8 @@ set(KIT_SRCS ctkAbstractQObjectFactory.tpp ctkAbstractLibraryFactory.h ctkAbstractLibraryFactory.tpp + ctkAbstractWorker.cpp + ctkAbstractWorker.h ctkBackTrace.cpp ctkBooleanMapper.cpp ctkBooleanMapper.h @@ -65,6 +69,9 @@ set(KIT_SRCS ctkFileLogger.cpp ctkFileLogger.h ctkHighPrecisionTimer.cpp + ctkJobScheduler.cpp + ctkJobScheduler.h + ctkJobScheduler_p.h ctkLinearValueProxy.cpp ctkLinearValueProxy.h ctkLogger.cpp @@ -100,6 +107,8 @@ endif() # Headers that should run through moc set(KIT_MOC_SRCS + ctkAbstractJob.h + ctkAbstractWorker.h ctkBooleanMapper.h ctkCallback.h ctkCommandLineParser.h @@ -111,6 +120,8 @@ set(KIT_MOC_SRCS ctkErrorLogQtMessageHandler.h ctkErrorLogTerminalOutput.h ctkFileLogger.h + ctkJobScheduler.h + ctkJobScheduler_p.h ctkLinearValueProxy.h ctkLogger.h ctkModelTester.h @@ -122,6 +133,15 @@ set(KIT_MOC_SRCS ctkWorkflowTransitions.h ) +# Abstract class should not be wrapped ! +set_source_files_properties( + ctkAbstractJob.h + ctkAbstractWorker.h + ctkJobScheduler.h + ctkJobScheduler_p.h + WRAP_EXCLUDE + ) + # UI files set(KIT_UI_FORMS ) diff --git a/Libs/Core/ctkAbstractJob.cpp b/Libs/Core/ctkAbstractJob.cpp new file mode 100644 index 0000000000..87d72a6b80 --- /dev/null +++ b/Libs/Core/ctkAbstractJob.cpp @@ -0,0 +1,157 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#include "ctkAbstractJob.h" + +// Qt includes +#include + +// -------------------------------------------------------------------------- +ctkAbstractJob::ctkAbstractJob() +{ + this->Status = JobStatus::Initialized; + this->Persistent = false; + this->JobUID = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); + this->RetryCounter = 0; + this->RetryDelay = 100; + this->MaximumNumberOfRetry = 3; + this->MaximumConcurrentJobsPerType = 20; + this->Priority = QThread::Priority::LowPriority; +} + +//---------------------------------------------------------------------------- +ctkAbstractJob::~ctkAbstractJob() +{ +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setJobUID(const QString &jobUID) +{ + this->JobUID = jobUID; +} + +//---------------------------------------------------------------------------- +QString ctkAbstractJob::className() const +{ + if (!this->metaObject()) + { + return ""; + } + return this->metaObject()->className(); +} + +//---------------------------------------------------------------------------- +QString ctkAbstractJob::jobUID() const +{ + return this->JobUID; +} + +//---------------------------------------------------------------------------- +ctkAbstractJob::JobStatus ctkAbstractJob::status() const +{ + return this->Status; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setStatus(JobStatus status) +{ + this->Status = status; +} + +//---------------------------------------------------------------------------- +bool ctkAbstractJob::isPersistent() const +{ + return this->Persistent; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setIsPersistent(bool persistent) +{ + this->Persistent = persistent; +} + +//---------------------------------------------------------------------------- +int ctkAbstractJob::retryCounter() const +{ + return this->RetryCounter; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setRetryCounter(int retryCounter) +{ + this->RetryCounter = retryCounter; +} + +//---------------------------------------------------------------------------- +int ctkAbstractJob::maximumConcurrentJobsPerType() const +{ + return this->MaximumConcurrentJobsPerType; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setMaximumConcurrentJobsPerType(int maximumConcurrentJobsPerType) +{ + this->MaximumConcurrentJobsPerType = maximumConcurrentJobsPerType; +} + +//---------------------------------------------------------------------------- +int ctkAbstractJob::maximumNumberOfRetry() const +{ + return this->MaximumNumberOfRetry; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setMaximumNumberOfRetry(int maximumNumberOfRetry) +{ + this->MaximumNumberOfRetry = maximumNumberOfRetry; +} + +//---------------------------------------------------------------------------- +int ctkAbstractJob::retryDelay() const +{ + return this->RetryDelay; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setRetryDelay(int retryDelay) +{ + this->RetryDelay = retryDelay; +} + +//---------------------------------------------------------------------------- +QThread::Priority ctkAbstractJob::priority() const +{ + return this->Priority; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setPriority(const QThread::Priority &priority) +{ + this->Priority = priority; +} + +//---------------------------------------------------------------------------- +QVariant ctkAbstractJob::toVariant() +{ + return QVariant::fromValue(ctkJobDetail(*this)); +} diff --git a/Libs/Core/ctkAbstractJob.h b/Libs/Core/ctkAbstractJob.h new file mode 100644 index 0000000000..f6dd6a9141 --- /dev/null +++ b/Libs/Core/ctkAbstractJob.h @@ -0,0 +1,171 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkAbstractJob_h +#define __ctkAbstractJob_h + +// Qt includes +#include +#include +#include + +// CTK includes +#include "ctkCoreExport.h" + +class ctkAbstractWorker; + +//------------------------------------------------------------------------------ +/// \ingroup Core +class CTK_CORE_EXPORT ctkAbstractJob : public QObject +{ + Q_OBJECT + Q_ENUMS(JobStatus); + Q_PROPERTY(QString jobUID READ jobUID WRITE setJobUID); + Q_PROPERTY(QString className READ className); + Q_PROPERTY(JobStatus status READ status WRITE setStatus); + Q_PROPERTY(bool persistent READ isPersistent WRITE setIsPersistent); + Q_PROPERTY(bool retryCounter READ retryCounter WRITE setRetryCounter); + Q_PROPERTY(int maximumNumberOfRetry READ maximumNumberOfRetry WRITE setMaximumNumberOfRetry); + Q_PROPERTY(int retryDelay READ retryDelay WRITE setRetryDelay); + Q_PROPERTY(bool maximumConcurrentJobsPerType READ maximumConcurrentJobsPerType WRITE setMaximumConcurrentJobsPerType); + Q_PROPERTY(QThread::Priority priority READ priority WRITE setPriority); + +public: + explicit ctkAbstractJob(); + virtual ~ctkAbstractJob(); + + ///@{ + /// Job UID + QString jobUID() const; + virtual void setJobUID(const QString& jobUID); + ///@} + + /// Class name + QString className() const; + + ///@{ + /// Status + enum JobStatus { + Initialized = 0, + Queued, + Running, + Stopped, + Finished, + }; + JobStatus status() const; + virtual void setStatus(JobStatus status); + ///@} + + ///@{ + /// Persistent + bool isPersistent() const; + void setIsPersistent(bool persistent); + ///@} + + ///@{ + /// Number of retries: current counter of how many times + /// the task has been relunched on fails + int retryCounter() const; + void setRetryCounter(int retryCounter); + ///@} + + ///@{ + /// Set the maximum concurrent jobs per job type. + /// Default value is 20. + int maximumConcurrentJobsPerType() const; + void setMaximumConcurrentJobsPerType(int maximumConcurrentJobsPerType); + ///@} + + ///@{ + /// Maximum number of retries that the Job pool will try on each failed Job + /// default: 3 + int maximumNumberOfRetry() const; + void setMaximumNumberOfRetry(int maximumNumberOfRetry); + ///@} + + ///@{ + /// Retry delay in millisec + /// default: 100 msec + int retryDelay() const; + void setRetryDelay(int retryDelay); + ///@} + + ///@{ + /// Priority + QThread::Priority priority() const; + void setPriority(const QThread::Priority& priority); + ///@} + + /// Generate worker for job + Q_INVOKABLE virtual ctkAbstractWorker* createWorker() = 0; + + /// Create a copy of this job + Q_INVOKABLE virtual ctkAbstractJob* clone() const = 0; + + /// Logger report string formatting for specific job + Q_INVOKABLE virtual QString loggerReport(const QString& status) const = 0; + + /// Return the QVariant value of this job. + /// + /// The value is set using the ctkJobDetail metatype and is used to pass + /// information between threads using Qt signals. + /// \sa ctkJobDetail + Q_INVOKABLE virtual QVariant toVariant(); + +Q_SIGNALS: + void started(); + void finished(); + void canceled(); + void failed(); + +protected: + QString JobUID; + JobStatus Status; + bool Persistent; + int RetryDelay; + int RetryCounter; + int MaximumNumberOfRetry; + int MaximumConcurrentJobsPerType; + QThread::Priority Priority; + +private: + Q_DISABLE_COPY(ctkAbstractJob) +}; + +//------------------------------------------------------------------------------ +/// \ingroup Core +struct CTK_CORE_EXPORT ctkJobDetail { + explicit ctkJobDetail(){} + explicit ctkJobDetail(const ctkAbstractJob& job) + { + this->JobClass = job.className(); + this->JobUID = job.jobUID(); + } + virtual ~ctkJobDetail() = default; + + QString JobClass; + QString JobUID; +}; +Q_DECLARE_METATYPE(ctkJobDetail); + +#endif // ctkAbstractJob_h diff --git a/Libs/Core/ctkAbstractWorker.cpp b/Libs/Core/ctkAbstractWorker.cpp new file mode 100644 index 0000000000..fdddcd6505 --- /dev/null +++ b/Libs/Core/ctkAbstractWorker.cpp @@ -0,0 +1,140 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include + +// CTK includes +#include "ctkAbstractJob.h" +#include "ctkJobScheduler.h" +#include "ctkAbstractWorker.h" + +// -------------------------------------------------------------------------- +ctkAbstractWorker::ctkAbstractWorker() +{ + this->setAutoDelete(false); +} + +//---------------------------------------------------------------------------- +ctkAbstractWorker::~ctkAbstractWorker() = default; + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +ctkAbstractJob* ctkAbstractWorker::job() const +{ + return this->Job.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkAbstractWorker::jobShared() const +{ + return this->Job; +} + +//---------------------------------------------------------------------------- +void ctkAbstractWorker::setJob(ctkAbstractJob &job) +{ + this->setJob(QSharedPointer(&job, skipDelete)); +} + +//---------------------------------------------------------------------------- +void ctkAbstractWorker::setJob(QSharedPointer job) +{ + this->Job = job; +} + +//---------------------------------------------------------------------------- +ctkJobScheduler *ctkAbstractWorker::scheduler() const +{ + return this->Scheduler.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkAbstractWorker::schedulerShared() const +{ + return this->Scheduler; +} + +//---------------------------------------------------------------------------- +void ctkAbstractWorker::setScheduler(ctkJobScheduler &scheduler) +{ + this->Scheduler = QSharedPointer(&scheduler, skipDelete); +} + +//---------------------------------------------------------------------------- +void ctkAbstractWorker::setScheduler(QSharedPointer scheduler) +{ + this->Scheduler = scheduler; +} + +//---------------------------------------------------------------------------- +void ctkAbstractWorker::startNextJob() +{ + if (!this->Scheduler || !this->Job) + { + return; + } + + ctkAbstractJob* newJob = this->Job->clone(); + newJob->setRetryCounter(newJob->retryCounter() + 1); + this->Scheduler->addJob(newJob); +} + +//---------------------------------------------------------------------------- +void ctkAbstractWorker::onJobCanceled() +{ + if (!this->Job) + { + return; + } + + if (this->Job->retryCounter() < this->Job->maximumNumberOfRetry() && + this->Job->status() != ctkAbstractJob::JobStatus::Stopped) + { + QTimer timer; + timer.setSingleShot(true); + QEventLoop loop; + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.start(this->Job->retryDelay()); + + this->startNextJob(); + + emit this->Job->finished(); + } + else if (this->Job->status() != ctkAbstractJob::JobStatus::Stopped) + { + emit this->Job->failed(); + } + else + { + emit this->Job->finished(); + } +} diff --git a/Libs/Core/ctkAbstractWorker.h b/Libs/Core/ctkAbstractWorker.h new file mode 100644 index 0000000000..d73abb26d1 --- /dev/null +++ b/Libs/Core/ctkAbstractWorker.h @@ -0,0 +1,82 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkAbstractWorker_h +#define __ctkAbstractWorker_h + +// Qt includes +#include +#include + +// CTK includes +#include "ctkCoreExport.h" + +class ctkAbstractJob; +class ctkJobScheduler; + +//------------------------------------------------------------------------------ +/// \ingroup Core +class CTK_CORE_EXPORT ctkAbstractWorker : public QObject, public QRunnable +{ + Q_OBJECT + +public: + explicit ctkAbstractWorker(); + virtual ~ctkAbstractWorker(); + + /// Execute worker + virtual void run() = 0; + + /// Cancel worker + virtual void cancel() = 0; + + ///@{ + /// Job + Q_INVOKABLE ctkAbstractJob* job() const; + QSharedPointer jobShared() const; + Q_INVOKABLE void setJob(ctkAbstractJob& job); + virtual void setJob(QSharedPointer job); + ///@} + + ///@{ + /// Scheduler + Q_INVOKABLE ctkJobScheduler* scheduler() const; + QSharedPointer schedulerShared() const; + Q_INVOKABLE void setScheduler(ctkJobScheduler& scheduler); + void setScheduler(QSharedPointer scheduler); + ///@} + +public slots: + virtual void startNextJob(); + virtual void onJobCanceled(); + +protected: + QSharedPointer Job; + QSharedPointer Scheduler; + +private: + Q_DISABLE_COPY(ctkAbstractWorker) +}; + + +#endif // ctkAbstractWorker_h diff --git a/Libs/Core/ctkCorePythonQtDecorators.h b/Libs/Core/ctkCorePythonQtDecorators.h index a1b4ac6322..b55faab737 100644 --- a/Libs/Core/ctkCorePythonQtDecorators.h +++ b/Libs/Core/ctkCorePythonQtDecorators.h @@ -25,6 +25,7 @@ #include // CTK includes +#include // For ctkJobDetail #include #include #include @@ -48,6 +49,7 @@ class ctkCorePythonQtDecorators : public QObject { PythonQt::self()->registerClass(&ctkBooleanMapper::staticMetaObject, "CTKCore"); PythonQt::self()->registerCPPClass("ctkErrorLogContext", 0, "CTKCore"); + PythonQt::self()->registerCPPClass("ctkJobDetail", 0, "CTKCore"); PythonQt::self()->registerCPPClass("ctkWorkflowStep", 0, "CTKCore"); PythonQt::self()->registerClass(&ctkWorkflowInterstepTransition::staticMetaObject, "CTKCore"); } @@ -233,6 +235,31 @@ public Q_SLOTS: return context->Message; } + // + // ctkJobDetail + // + ctkJobDetail* new_ctkJobDetail() + { + return new ctkJobDetail(); + } + + void setJobClass(ctkJobDetail* td, const QString& jobClass) + { + td->JobClass = jobClass; + } + QString jobClass(ctkJobDetail* td) + { + return td->JobClass; + } + + void setJobUID(ctkJobDetail* td, const QString& jobUID) + { + td->JobUID = jobUID; + } + QString JobUID(ctkJobDetail* td) + { + return td->JobUID; + } }; //----------------------------------------------------------------------------- diff --git a/Libs/Core/ctkJobScheduler.cpp b/Libs/Core/ctkJobScheduler.cpp new file mode 100644 index 0000000000..141e813314 --- /dev/null +++ b/Libs/Core/ctkJobScheduler.cpp @@ -0,0 +1,480 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include +#include + +// CTK includes +#include "ctkAbstractJob.h" +#include "ctkJobScheduler.h" +#include "ctkJobScheduler_p.h" +#include "ctkAbstractWorker.h" +#include "ctkLogger.h" + +static ctkLogger logger("org.commontk.core.AbstractScheduler"); + +// -------------------------------------------------------------------------- +// ctkJobSchedulerPrivate methods + +// -------------------------------------------------------------------------- +ctkJobSchedulerPrivate::ctkJobSchedulerPrivate(ctkJobScheduler& object) + : q_ptr(&object) +{ +} + +// -------------------------------------------------------------------------- +ctkJobSchedulerPrivate::~ctkJobSchedulerPrivate() = default; + +//--------------------------------------------------------------------------- +void ctkJobSchedulerPrivate::init() +{ + Q_Q(ctkJobScheduler); + + QObject::connect(q, SIGNAL(queueJobs()), + q, SLOT(onQueueJobsInThreadPool()), + Qt::QueuedConnection); + + this->ThreadPool = QSharedPointer(new QThreadPool()); + this->ThreadPool->setMaxThreadCount(20); +} + +//------------------------------------------------------------------------------ +void ctkJobSchedulerPrivate::insertJob(QSharedPointer job) +{ + Q_Q(ctkJobScheduler); + + if (!job) + { + return; + } + + logger.debug(QString("ctkJobScheduler: creating job object %1 of type %2 in thread %3.\n") + .arg(job->jobUID()) + .arg(job->className()) + .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); + + QObject::connect(job.data(), SIGNAL(started()), q, SLOT(onJobStarted())); + QObject::connect(job.data(), SIGNAL(canceled()), q, SLOT(onJobCanceled())); + QObject::connect(job.data(), SIGNAL(failed()), q, SLOT(onJobFailed())); + QObject::connect(job.data(), SIGNAL(finished()), q, SLOT(onJobFinished())); + QObject::connect(job.data(), SIGNAL(progressJobDetail(QVariant)), + q, SIGNAL(progressJobDetail(QVariant))); + + QMutexLocker ml(&this->mMutex); + this->JobsQueue.insert(job->jobUID(), job); + emit q->queueJobs(); +} + +//------------------------------------------------------------------------------ +void ctkJobSchedulerPrivate::removeJob(const QString& jobUID) +{ + Q_Q(ctkJobScheduler); + + logger.debug(QString("ctkJobScheduler: deleting job object %1 in thread %2.\n") + .arg(jobUID) + .arg(QString::number(reinterpret_cast(QThread::currentThreadId()), 16))); + + QSharedPointer job = this->JobsQueue.value(jobUID); + if (!job) + { + return; + } + + QObject::disconnect(job.data(), SIGNAL(started()), q, SLOT(onJobStarted())); + QObject::disconnect(job.data(), SIGNAL(canceled()), q, SLOT(onJobCanceled())); + QObject::disconnect(job.data(), SIGNAL(failed()), q, SLOT(onJobFailed())); + QObject::disconnect(job.data(), SIGNAL(finished()), q, SLOT(onJobFinished())); + QObject::disconnect(job.data(), SIGNAL(progressJobDetail(QVariant)), q, SIGNAL(progressJobDetail(QVariant))); + + this->JobsQueue.remove(jobUID); + emit q->queueJobs(); +} + +//------------------------------------------------------------------------------ +int ctkJobSchedulerPrivate::getSameTypeJobsInThreadPoolQueueOrRunning(QSharedPointer job) +{ + int count = 0; + foreach (QSharedPointer queuedJob, this->JobsQueue) + { + if (queuedJob->jobUID() == job->jobUID()) + { + continue; + } + + if ((queuedJob->status() == ctkAbstractJob::JobStatus::Queued || + queuedJob->status() == ctkAbstractJob::JobStatus::Running) && + queuedJob->className() == job->className()) + { + count++; + } + } + + return count; +} + +//------------------------------------------------------------------------------ +QString ctkJobSchedulerPrivate::generateUniqueJobUID() +{ + return QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); +} + +//--------------------------------------------------------------------------- +// ctkJobScheduler methods + +// -------------------------------------------------------------------------- +ctkJobScheduler::ctkJobScheduler(QObject* parent) + : QObject(parent) + , d_ptr(new ctkJobSchedulerPrivate(*this)) +{ + Q_D(ctkJobScheduler); + d->init(); +} + +// -------------------------------------------------------------------------- +ctkJobScheduler::ctkJobScheduler(ctkJobSchedulerPrivate* pimpl, QObject* parent) + : Superclass(parent) + , d_ptr(pimpl) +{ + // derived classes must call init manually. Calling init() here may results in + // actions on a derived public class not yet finished to be created +} + +// -------------------------------------------------------------------------- +ctkJobScheduler::~ctkJobScheduler() = default; + +//---------------------------------------------------------------------------- +int ctkJobScheduler::numberOfJobs() +{ + Q_D(ctkJobScheduler); + QMutexLocker ml(&d->mMutex); + return d->JobsQueue.count(); +} + +//---------------------------------------------------------------------------- +int ctkJobScheduler::numberOfPersistentJobs() +{ + Q_D(ctkJobScheduler); + int cont = 0; + QMutexLocker ml(&d->mMutex); + foreach (QSharedPointer job, d->JobsQueue) + { + if (job->isPersistent()) + { + cont++; + } + } + return cont; +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::addJob(ctkAbstractJob* job) +{ + Q_D(ctkJobScheduler); + + QSharedPointer jobShared = QSharedPointer(job); + d->insertJob(jobShared); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::deleteJob(const QString& jobUID) +{ + Q_D(ctkJobScheduler); + d->removeJob(jobUID); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::deleteWorker(const QString& jobUID) +{ + Q_D(ctkJobScheduler); + + QMap>::iterator it = d->Workers.find(jobUID); + if (it == d->Workers.end()) + { + return; + } + + d->Workers.remove(jobUID); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkJobScheduler::getJobSharedByUID(const QString& jobUID) +{ + Q_D(ctkJobScheduler); + + QMutexLocker ml(&d->mMutex); + QMap>::iterator it = d->JobsQueue.find(jobUID); + if (it == d->JobsQueue.end()) + { + return nullptr; + } + + return d->JobsQueue.value(jobUID); +} + +//---------------------------------------------------------------------------- +ctkAbstractJob* ctkJobScheduler::getJobByUID(const QString& jobUID) +{ + QSharedPointer job = this->getJobSharedByUID(jobUID); + if (!job) + { + return nullptr; + } + + return job.data(); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::waitForFinish() +{ + Q_D(ctkJobScheduler); + + int numberOfPersistentJobs = this->numberOfPersistentJobs(); + while (this->numberOfJobs() > numberOfPersistentJobs) + { + QCoreApplication::processEvents(); + d->ThreadPool->waitForDone(300); + } +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::waitForDone(int msec) +{ + Q_D(ctkJobScheduler); + + QCoreApplication::processEvents(); + d->ThreadPool->waitForDone(msec); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::stopAllJobs(bool stopPersistentJobs) +{ + Q_D(ctkJobScheduler); + + QMutexLocker ml(&d->mMutex); + + // Stops jobs without a worker (in waiting) + foreach (QSharedPointer job, d->JobsQueue) + { + if (job->isPersistent() && !stopPersistentJobs) + { + continue; + } + + if (job->status() != ctkAbstractJob::JobStatus::Initialized) + { + continue; + } + + job->setStatus(ctkAbstractJob::JobStatus::Stopped); + this->deleteJob(job->jobUID()); + } + + // Stops queued and running jobs + foreach (QSharedPointer worker, d->Workers) + { + QSharedPointer job = worker->jobShared(); + if (job->isPersistent() && !stopPersistentJobs) + { + continue; + } + + if (job->status() != ctkAbstractJob::JobStatus::Running && + job->status() != ctkAbstractJob::JobStatus::Queued) + { + continue; + } + + job->setStatus(ctkAbstractJob::JobStatus::Stopped); + worker->cancel(); + } +} + +//---------------------------------------------------------------------------- +int ctkJobScheduler::maximumThreadCount() const +{ + Q_D(const ctkJobScheduler); + return d->ThreadPool->maxThreadCount(); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::setMaximumThreadCount(int maximumThreadCount) +{ + Q_D(ctkJobScheduler); + d->ThreadPool->setMaxThreadCount(maximumThreadCount); +} + +//---------------------------------------------------------------------------- +int ctkJobScheduler::maximumNumberOfRetry() const +{ + Q_D(const ctkJobScheduler); + return d->MaximumNumberOfRetry; +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::setMaximumNumberOfRetry(int maximumNumberOfRetry) +{ + Q_D(ctkJobScheduler); + d->MaximumNumberOfRetry = maximumNumberOfRetry; +} + +//---------------------------------------------------------------------------- +int ctkJobScheduler::retryDelay() const +{ + Q_D(const ctkJobScheduler); + return d->RetryDelay; +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::setRetryDelay(int retryDelay) +{ + Q_D(ctkJobScheduler); + d->RetryDelay = retryDelay; +} + +//---------------------------------------------------------------------------- +QThreadPool* ctkJobScheduler::threadPool() const +{ + Q_D(const ctkJobScheduler); + return d->ThreadPool.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkJobScheduler::threadPoolShared() const +{ + Q_D(const ctkJobScheduler); + return d->ThreadPool; +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::onJobStarted() +{ + ctkAbstractJob* job = qobject_cast(this->sender()); + if (!job) + { + return; + } + + logger.debug(job->loggerReport("started")); + emit this->jobStarted(job->toVariant()); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::onJobCanceled() +{ + ctkAbstractJob* job = qobject_cast(this->sender()); + if (!job) + { + return; + } + logger.debug(job->loggerReport("canceled")); + emit this->jobCanceled(job->toVariant()); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::onJobFailed() +{ + ctkAbstractJob* job = qobject_cast(this->sender()); + if (!job) + { + return; + } + + logger.debug(job->loggerReport("failed")); + + QVariant data = job->toVariant(); + QString jobUID = job->jobUID(); + this->deleteWorker(jobUID); + this->deleteJob(jobUID); + + emit this->jobFailed(data); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::onJobFinished() +{ + ctkAbstractJob* job = qobject_cast(this->sender()); + if (!job) + { + return; + } + + logger.debug(job->loggerReport("finished")); + + QVariant data = job->toVariant(); + QString jobUID = job->jobUID(); + this->deleteWorker(jobUID); + this->deleteJob(jobUID); + + emit this->jobFinished(data); +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::onQueueJobsInThreadPool() +{ + Q_D(ctkJobScheduler); + + QMutexLocker ml(&d->mMutex); + foreach (QThread::Priority priority, (QList() + << QThread::Priority::HighestPriority + << QThread::Priority::HighPriority + << QThread::Priority::NormalPriority + << QThread::Priority::LowPriority + << QThread::Priority::LowestPriority)) + { + foreach (QSharedPointer job, d->JobsQueue) + { + if (job->priority() != priority) + { + continue; + } + + if (job->status() != ctkAbstractJob::JobStatus::Initialized) + { + continue; + } + + int numberOfRunningJobsWithSameType = d->getSameTypeJobsInThreadPoolQueueOrRunning(job); + if (numberOfRunningJobsWithSameType >= job->maximumConcurrentJobsPerType()) + { + continue; + } + + logger.debug(QString("ctkDICOMScheduler: creating worker for job %1 in thread %2.\n") + .arg(job->jobUID()) + .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); + + job->setStatus(ctkAbstractJob::JobStatus::Queued); + + QSharedPointer worker = + QSharedPointer(job->createWorker()); + worker->setScheduler(*this); + + d->Workers.insert(job->jobUID(), worker); + d->ThreadPool->start(worker.data(), job->priority()); + } + } +} diff --git a/Libs/Core/ctkJobScheduler.h b/Libs/Core/ctkJobScheduler.h new file mode 100644 index 0000000000..4d077698b6 --- /dev/null +++ b/Libs/Core/ctkJobScheduler.h @@ -0,0 +1,119 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkJobScheduler_h +#define __ctkJobScheduler_h + +// Qt includes +#include +#include +#include +class QThreadPool; + +// CTK includes +#include "ctkCoreExport.h" +class ctkAbstractJob; +class ctkJobSchedulerPrivate; + +//------------------------------------------------------------------------------ +/// \ingroup Core +class CTK_CORE_EXPORT ctkJobScheduler : public QObject +{ + Q_OBJECT +public: + typedef QObject Superclass; + explicit ctkJobScheduler(QObject* parent = 0); + virtual ~ctkJobScheduler(); + + ///@{ + /// Jobs managment + Q_INVOKABLE int numberOfJobs(); + Q_INVOKABLE int numberOfPersistentJobs(); + + Q_INVOKABLE void addJob(ctkAbstractJob* job); + + Q_INVOKABLE virtual void deleteJob(const QString& jobUID); + Q_INVOKABLE virtual void deleteWorker(const QString& jobUID); + + QSharedPointer getJobSharedByUID(const QString& jobUID); + Q_INVOKABLE ctkAbstractJob* getJobByUID(const QString& jobUID); + + Q_INVOKABLE void waitForFinish(); + Q_INVOKABLE void waitForDone(int msec); + + Q_INVOKABLE void stopAllJobs(bool stopPersistentJobs = false); + ///@} + + ///@{ + /// Maximum number of concurrent QThreads spawned by the threadPool in the Job pool + /// default: 20 + int maximumThreadCount() const; + void setMaximumThreadCount(int maximumThreadCount); + ///@} + + ///@{ + /// Maximum number of retries that the Job pool will try on each failed Job + /// default: 3 + int maximumNumberOfRetry() const; + void setMaximumNumberOfRetry(int maximumNumberOfRetry); + ///@} + + ///@{ + /// Retry delay in millisec + /// default: 100 msec + int retryDelay() const; + void setRetryDelay(int retryDelay); + ///@} + + /// Return the threadPool. + Q_INVOKABLE QThreadPool* threadPool() const; + + /// Return threadPool as a shared pointer + /// (not Python-wrappable). + QSharedPointer threadPoolShared() const; + +Q_SIGNALS: + void jobStarted(QVariant data); + void jobFinished(QVariant data); + void jobCanceled(QVariant data); + void jobFailed(QVariant data); + void queueJobs(); + void progressJobDetail(QVariant data); + +public Q_SLOTS: + virtual void onJobStarted(); + virtual void onJobFinished(); + virtual void onJobCanceled(); + virtual void onJobFailed(); + virtual void onQueueJobsInThreadPool(); + +protected: + QScopedPointer d_ptr; + ctkJobScheduler(ctkJobSchedulerPrivate* pimpl, QObject* parent); + +private: + Q_DECLARE_PRIVATE(ctkJobScheduler); + Q_DISABLE_COPY(ctkJobScheduler) +}; + +#endif // ctkJobScheduler_h diff --git a/Libs/Core/ctkJobScheduler_p.h b/Libs/Core/ctkJobScheduler_p.h new file mode 100644 index 0000000000..0c3553a0ef --- /dev/null +++ b/Libs/Core/ctkJobScheduler_p.h @@ -0,0 +1,68 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +=========================================================================*/ + +#ifndef __ctkJobSchedulerPrivate_h +#define __ctkJobSchedulerPrivate_h + +// Qt includes +#include +#include +class QThreadPool; + +// ctkCore includes +#include "ctkCoreExport.h" +class ctkAbstractJob; +class ctkAbstractWorker; + +// ctkDICOMCore includes +#include "ctkJobScheduler.h" + +//------------------------------------------------------------------------------ +class CTK_CORE_EXPORT ctkJobSchedulerPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkJobScheduler) + +protected: + ctkJobScheduler* const q_ptr; + +public: + ctkJobSchedulerPrivate(ctkJobScheduler& object); + virtual ~ctkJobSchedulerPrivate(); + + /// Convenient setup methods + virtual void init(); + + virtual void insertJob(QSharedPointer job); + virtual void removeJob(const QString& jobUID); + int getSameTypeJobsInThreadPoolQueueOrRunning(QSharedPointer job); + QString generateUniqueJobUID(); + + QMutex mMutex; + + int RetryDelay{100}; + int MaximumNumberOfRetry{3}; + + QSharedPointer ThreadPool; + QMap> JobsQueue; + QMap> Workers; +}; + +#endif diff --git a/Libs/DICOM/Core/CMakeLists.txt b/Libs/DICOM/Core/CMakeLists.txt index 80a5422544..fbf02cd2e2 100644 --- a/Libs/DICOM/Core/CMakeLists.txt +++ b/Libs/DICOM/Core/CMakeLists.txt @@ -16,21 +16,59 @@ set(KIT_SRCS ctkDICOMItem.h ctkDICOMDisplayedFieldGenerator.cpp ctkDICOMDisplayedFieldGenerator.h + ctkDICOMEcho.cpp + ctkDICOMEcho.h ctkDICOMFilterProxyModel.cpp ctkDICOMFilterProxyModel.h ctkDICOMIndexer.cpp ctkDICOMIndexer.h ctkDICOMIndexer_p.h + ctkDICOMInserter.cpp + ctkDICOMInserter.h + ctkDICOMInserterJob.cpp + ctkDICOMInserterJob.h + ctkDICOMInserterWorker.cpp + ctkDICOMInserterWorker.h + ctkDICOMInserterWorker_p.h ctkDICOMItem.cpp ctkDICOMItem.h + ctkDICOMJob.cpp + ctkDICOMJob.h + ctkDICOMJobResponseSet.cpp + ctkDICOMJobResponseSet.h ctkDICOMModel.cpp ctkDICOMModel.h ctkDICOMPersonName.cpp ctkDICOMPersonName.h ctkDICOMQuery.cpp ctkDICOMQuery.h + ctkDICOMQueryJob.cpp + ctkDICOMQueryJob.h + ctkDICOMQueryJob_p.h + ctkDICOMQueryWorker.cpp + ctkDICOMQueryWorker.h + ctkDICOMQueryWorker_p.h ctkDICOMRetrieve.cpp ctkDICOMRetrieve.h + ctkDICOMRetrieveJob.cpp + ctkDICOMRetrieveJob.h + ctkDICOMRetrieveJob_p.h + ctkDICOMRetrieveWorker.cpp + ctkDICOMRetrieveWorker.h + ctkDICOMRetrieveWorker_p.h + ctkDICOMScheduler.cpp + ctkDICOMScheduler.h + ctkDICOMScheduler_p.h + ctkDICOMServer.cpp + ctkDICOMServer.h + ctkDICOMStorageListener.cpp + ctkDICOMStorageListener.h + ctkDICOMStorageListenerJob.cpp + ctkDICOMStorageListenerJob.h + ctkDICOMStorageListenerJob_p.h + ctkDICOMStorageListenerWorker.cpp + ctkDICOMStorageListenerWorker.h + ctkDICOMStorageListenerWorker_p.h ctkDICOMTester.cpp ctkDICOMTester.h ctkDICOMUtil.cpp @@ -66,12 +104,35 @@ set(KIT_MOC_SRCS ctkDICOMDisplayedFieldGenerator.h ctkDICOMDisplayedFieldGenerator_p.h ctkDICOMDisplayedFieldGeneratorRuleFactory.h + ctkDICOMEcho.h ctkDICOMIndexer.h ctkDICOMIndexer_p.h + ctkDICOMInserter.h + ctkDICOMInserterJob.h + ctkDICOMInserterWorker.h + ctkDICOMInserterWorker_p.h + ctkDICOMJob.h + ctkDICOMJobResponseSet.h ctkDICOMFilterProxyModel.h ctkDICOMModel.h ctkDICOMQuery.h + ctkDICOMQueryJob.h + ctkDICOMQueryJob_p.h + ctkDICOMQueryWorker.h + ctkDICOMQueryWorker_p.h ctkDICOMRetrieve.h + ctkDICOMRetrieveJob.h + ctkDICOMRetrieveJob_p.h + ctkDICOMRetrieveWorker.h + ctkDICOMRetrieveWorker_p.h + ctkDICOMScheduler.h + ctkDICOMScheduler_p.h + ctkDICOMServer.h + ctkDICOMStorageListener.h + ctkDICOMStorageListenerJob.h + ctkDICOMStorageListenerJob_p.h + ctkDICOMStorageListenerWorker.h + ctkDICOMStorageListenerWorker_p.h ctkDICOMTester.h ) @@ -93,7 +154,6 @@ list(APPEND KIT_target_libraries Qt${CTK_QT_VERSION}::Sql) # create a dcm query/retrieve service config file that points to the build dir set (DCMQRSCP_STORE_DIR ${CMAKE_CURRENT_BINARY_DIR}/Testing) configure_file( Resources/dcmqrscp.cfg.in dcmqrscp.cfg ) -set (DCMQRSCP_CONFIG ${CMAKE_CURRENT_BINARY_DIR}/dcmqrscp.cfg) ctkMacroBuildLib( NAME ${PROJECT_NAME} @@ -108,7 +168,6 @@ ctkMacroBuildLib( if(DEFINED DCMTK_HAVE_CONFIG_H_OPTIONAL AND NOT DCMTK_HAVE_CONFIG_H_OPTIONAL) # Workaround Debian packaging issue - See FindDCMTK.cmake for more details - set_target_properties(${PROJECT_NAME} PROPERTIES COMPILE_DEFINITIONS ${DCMTK_DEFINITIONS}) set_target_properties(${PROJECT_NAME} PROPERTIES INTERFACE_COMPILE_DEFINITIONS ${DCMTK_DEFINITIONS}) endif() diff --git a/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc b/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc index 977d25846b..8ec68f4abd 100644 --- a/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc +++ b/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc @@ -1,6 +1,7 @@ - - - dicom-schema.sql - dicom-qr-schema.sql - + + + dicom-schema.sql + dicom-qr-schema.sql + storescp.cfg + diff --git a/Libs/DICOM/Core/Resources/dicom-schema.sql b/Libs/DICOM/Core/Resources/dicom-schema.sql index 06b352f9f4..61af7adf3b 100644 --- a/Libs/DICOM/Core/Resources/dicom-schema.sql +++ b/Libs/DICOM/Core/Resources/dicom-schema.sql @@ -74,6 +74,7 @@ CREATE TABLE 'Series' ( 'BodyPartExamined' VARCHAR(255) NULL , 'FrameOfReferenceUID' VARCHAR(64) NULL , 'AcquisitionNumber' INT NULL , + 'ContrastAgent' VARCHAR(255) NULL , 'ScanningSequence' VARCHAR(45) NULL , 'EchoNumber' INT NULL , diff --git a/Libs/DICOM/Core/Resources/storescp.cfg b/Libs/DICOM/Core/Resources/storescp.cfg new file mode 100644 index 0000000000..07660fc6ea --- /dev/null +++ b/Libs/DICOM/Core/Resources/storescp.cfg @@ -0,0 +1,503 @@ +# +# Copyright (C) 2003-2021, OFFIS e.V. +# All rights reserved. See COPYRIGHT file for details. +# +# This software and supporting documentation were developed by +# +# OFFIS e.V. +# R&D Division Health +# Escherweg 2 +# D-26121 Oldenburg, Germany +# +# Module: dcmnet +# +# Author: Marco Eichelberg, Joerg Riesmeier +# +# Purpose: Sample configuration file for storescp +# + +# ============================================================================ +[[TransferSyntaxes]] +# ============================================================================ + +[Uncompressed] +TransferSyntax1 = LocalEndianExplicit +TransferSyntax2 = OppositeEndianExplicit +TransferSyntax3 = LittleEndianImplicit + +[UncompressedOrZlib] +TransferSyntax1 = DeflatedLittleEndianExplicit +TransferSyntax2 = LocalEndianExplicit +TransferSyntax3 = OppositeEndianExplicit +TransferSyntax4 = LittleEndianImplicit + +[AnyTransferSyntax] +TransferSyntax1 = JPEG2000 +TransferSyntax2 = JPEG2000LosslessOnly +TransferSyntax3 = JPEGExtended:Process2+4 +TransferSyntax4 = JPEGBaseline +TransferSyntax5 = JPEGLossless:Non-hierarchical-1stOrderPrediction +TransferSyntax6 = JPEGLSLossy +TransferSyntax7 = JPEGLSLossless +TransferSyntax8 = RLELossless +TransferSyntax9 = MPEG2MainProfile@MainLevel +TransferSyntax10 = MPEG2MainProfile@HighLevel +TransferSyntax11 = MPEG4HighProfile/Level4.1 +TransferSyntax12 = MPEG4BDcompatibleHighProfile/Level4.1 +TransferSyntax13 = MPEG4HighProfile/Level4.2For2DVideo +TransferSyntax14 = MPEG4HighProfile/Level4.2For3DVideo +TransferSyntax15 = MPEG4StereoHighProfile/Level4.2 +TransferSyntax16 = HEVCMainProfile/Level5.1 +TransferSyntax17 = HEVCMain10Profile/Level5.1 +TransferSyntax18 = DeflatedLittleEndianExplicit +TransferSyntax19 = LocalEndianExplicit +TransferSyntax20 = OppositeEndianExplicit +TransferSyntax21 = LittleEndianImplicit + +# ============================================================================ +[[PresentationContexts]] +# ============================================================================ + +[GenericStorageSCP] +# +# Don't forget to support the Verification SOP Class. +# +PresentationContext1 = VerificationSOPClass\Uncompressed +# +# Accept image SOP classes with virtually any transfer syntax we know. +# Accept non-image SOP classes uncompressed or with zlib compression only. +# +PresentationContext2 = BreastTomosynthesisImageStorage\AnyTransferSyntax +PresentationContext3 = ComputedRadiographyImageStorage\AnyTransferSyntax +PresentationContext4 = CornealTopographyMapStorage\AnyTransferSyntax +PresentationContext5 = CTImageStorage\AnyTransferSyntax +PresentationContext6 = DigitalIntraOralXRayImageStorageForPresentation\AnyTransferSyntax +PresentationContext7 = DigitalIntraOralXRayImageStorageForProcessing\AnyTransferSyntax +PresentationContext8 = DigitalMammographyXRayImageStorageForPresentation\AnyTransferSyntax +PresentationContext9 = DigitalMammographyXRayImageStorageForProcessing\AnyTransferSyntax +PresentationContext10 = DigitalXRayImageStorageForPresentation\AnyTransferSyntax +PresentationContext11 = DigitalXRayImageStorageForProcessing\AnyTransferSyntax +PresentationContext12 = EnhancedCTImageStorage\AnyTransferSyntax +PresentationContext13 = EnhancedMRColorImageStorage\AnyTransferSyntax +PresentationContext14 = EnhancedMRImageStorage\AnyTransferSyntax +PresentationContext15 = EnhancedPETImageStorage\AnyTransferSyntax +PresentationContext16 = EnhancedUSVolumeStorage\AnyTransferSyntax +PresentationContext17 = EnhancedXAImageStorage\AnyTransferSyntax +PresentationContext18 = EnhancedXRFImageStorage\AnyTransferSyntax +PresentationContext19 = IntravascularOpticalCoherenceTomographyImageStorageForPresentation\AnyTransferSyntax +PresentationContext20 = IntravascularOpticalCoherenceTomographyImageStorageForProcessing\AnyTransferSyntax +PresentationContext21 = MRImageStorage\AnyTransferSyntax +PresentationContext22 = MultiframeGrayscaleByteSecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext23 = MultiframeGrayscaleWordSecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext24 = MultiframeSingleBitSecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext25 = MultiframeTrueColorSecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext26 = NuclearMedicineImageStorage\AnyTransferSyntax +PresentationContext27 = OphthalmicPhotography16BitImageStorage\AnyTransferSyntax +PresentationContext28 = OphthalmicPhotography8BitImageStorage\AnyTransferSyntax +PresentationContext29 = OphthalmicThicknessMapStorage\AnyTransferSyntax +PresentationContext30 = OphthalmicTomographyImageStorage\AnyTransferSyntax +PresentationContext31 = PositronEmissionTomographyImageStorage\AnyTransferSyntax +PresentationContext32 = RTImageStorage\AnyTransferSyntax +PresentationContext33 = SecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext34 = UltrasoundImageStorage\AnyTransferSyntax +PresentationContext35 = UltrasoundMultiframeImageStorage\AnyTransferSyntax +PresentationContext36 = VideoEndoscopicImageStorage\AnyTransferSyntax +PresentationContext37 = VideoMicroscopicImageStorage\AnyTransferSyntax +PresentationContext38 = VideoPhotographicImageStorage\AnyTransferSyntax +PresentationContext39 = VLEndoscopicImageStorage\AnyTransferSyntax +PresentationContext40 = VLMicroscopicImageStorage\AnyTransferSyntax +PresentationContext41 = VLPhotographicImageStorage\AnyTransferSyntax +PresentationContext42 = VLSlideCoordinatesMicroscopicImageStorage\AnyTransferSyntax +PresentationContext43 = VLWholeSlideMicroscopyImageStorage\AnyTransferSyntax +PresentationContext44 = XRay3DAngiographicImageStorage\AnyTransferSyntax +PresentationContext45 = XRay3DCraniofacialImageStorage\AnyTransferSyntax +PresentationContext46 = XRayAngiographicImageStorage\AnyTransferSyntax +PresentationContext47 = XRayRadiofluoroscopicImageStorage\AnyTransferSyntax +# retired +PresentationContext48 = RETIRED_HardcopyColorImageStorage\AnyTransferSyntax +PresentationContext49 = RETIRED_HardcopyGrayscaleImageStorage\AnyTransferSyntax +PresentationContext50 = RETIRED_NuclearMedicineImageStorage\AnyTransferSyntax +PresentationContext51 = RETIRED_UltrasoundImageStorage\AnyTransferSyntax +PresentationContext52 = RETIRED_UltrasoundMultiframeImageStorage\AnyTransferSyntax +PresentationContext53 = RETIRED_VLImageStorage\AnyTransferSyntax +PresentationContext54 = RETIRED_VLMultiframeImageStorage\AnyTransferSyntax +PresentationContext55 = RETIRED_XRayAngiographicBiPlaneImageStorage\AnyTransferSyntax +# +# the following presentation contexts are for non-image SOP classes +# +PresentationContext56 = AmbulatoryECGWaveformStorage\UncompressedOrZlib +PresentationContext57 = ArterialPulseWaveformStorage\UncompressedOrZlib +PresentationContext58 = AutorefractionMeasurementsStorage\UncompressedOrZlib +PresentationContext59 = BasicStructuredDisplayStorage\UncompressedOrZlib +PresentationContext60 = BasicTextSRStorage\UncompressedOrZlib +PresentationContext61 = BasicVoiceAudioWaveformStorage\UncompressedOrZlib +PresentationContext62 = BlendingSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext63 = CardiacElectrophysiologyWaveformStorage\UncompressedOrZlib +PresentationContext64 = ChestCADSRStorage\UncompressedOrZlib +PresentationContext65 = ColonCADSRStorage\UncompressedOrZlib +PresentationContext66 = ColorSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext67 = Comprehensive3DSRStorage\UncompressedOrZlib +PresentationContext68 = ComprehensiveSRStorage\UncompressedOrZlib +PresentationContext69 = DeformableSpatialRegistrationStorage\UncompressedOrZlib +PresentationContext70 = EncapsulatedCDAStorage\UncompressedOrZlib +PresentationContext71 = EncapsulatedPDFStorage\UncompressedOrZlib +PresentationContext72 = EnhancedSRStorage\UncompressedOrZlib +PresentationContext73 = GeneralAudioWaveformStorage\UncompressedOrZlib +PresentationContext74 = GeneralECGWaveformStorage\UncompressedOrZlib +PresentationContext75 = GenericImplantTemplateStorage\UncompressedOrZlib +PresentationContext76 = GrayscaleSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext77 = HemodynamicWaveformStorage\UncompressedOrZlib +PresentationContext78 = ImplantAssemblyTemplateStorage\UncompressedOrZlib +PresentationContext79 = ImplantationPlanSRDocumentStorage\UncompressedOrZlib +PresentationContext80 = ImplantTemplateGroupStorage\UncompressedOrZlib +PresentationContext81 = IntraocularLensCalculationsStorage\UncompressedOrZlib +PresentationContext82 = KeratometryMeasurementsStorage\UncompressedOrZlib +PresentationContext83 = KeyObjectSelectionDocumentStorage\UncompressedOrZlib +PresentationContext84 = LensometryMeasurementsStorage\UncompressedOrZlib +PresentationContext85 = MacularGridThicknessAndVolumeReportStorage\UncompressedOrZlib +PresentationContext86 = MammographyCADSRStorage\UncompressedOrZlib +PresentationContext87 = MRSpectroscopyStorage\UncompressedOrZlib +PresentationContext88 = OphthalmicAxialMeasurementsStorage\UncompressedOrZlib +PresentationContext89 = OphthalmicVisualFieldStaticPerimetryMeasurementsStorage\UncompressedOrZlib +PresentationContext90 = ProcedureLogStorage\UncompressedOrZlib +PresentationContext91 = PseudoColorSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext92 = RawDataStorage\UncompressedOrZlib +PresentationContext93 = RealWorldValueMappingStorage\UncompressedOrZlib +PresentationContext94 = RespiratoryWaveformStorage\UncompressedOrZlib +PresentationContext95 = RTBeamsDeliveryInstructionStorage\UncompressedOrZlib +PresentationContext96 = RTBeamsTreatmentRecordStorage\UncompressedOrZlib +PresentationContext97 = RTBrachyTreatmentRecordStorage\UncompressedOrZlib +PresentationContext98 = RTDoseStorage\UncompressedOrZlib +PresentationContext99 = RTIonBeamsTreatmentRecordStorage\UncompressedOrZlib +PresentationContext100 = RTIonPlanStorage\UncompressedOrZlib +PresentationContext101 = RTPlanStorage\UncompressedOrZlib +PresentationContext102 = RTStructureSetStorage\UncompressedOrZlib +PresentationContext103 = RTTreatmentSummaryRecordStorage\UncompressedOrZlib +PresentationContext104 = SegmentationStorage\UncompressedOrZlib +PresentationContext105 = SpatialFiducialsStorage\UncompressedOrZlib +PresentationContext106 = SpatialRegistrationStorage\UncompressedOrZlib +PresentationContext107 = SpectaclePrescriptionReportStorage\UncompressedOrZlib +PresentationContext108 = StereometricRelationshipStorage\UncompressedOrZlib +PresentationContext109 = SubjectiveRefractionMeasurementsStorage\UncompressedOrZlib +PresentationContext110 = SurfaceScanMeshStorage\UncompressedOrZlib +PresentationContext111 = SurfaceScanPointCloudStorage\UncompressedOrZlib +PresentationContext112 = SurfaceSegmentationStorage\UncompressedOrZlib +PresentationContext113 = TwelveLeadECGWaveformStorage\UncompressedOrZlib +PresentationContext114 = VisualAcuityMeasurementsStorage\UncompressedOrZlib +PresentationContext115 = XAXRFGrayscaleSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext116 = XRayRadiationDoseSRStorage\UncompressedOrZlib +# retired +PresentationContext117 = RETIRED_StandaloneCurveStorage\UncompressedOrZlib +PresentationContext118 = RETIRED_StandaloneModalityLUTStorage\UncompressedOrZlib +PresentationContext119 = RETIRED_StandaloneOverlayStorage\UncompressedOrZlib +PresentationContext120 = RETIRED_StandalonePETCurveStorage\UncompressedOrZlib +PresentationContext121 = RETIRED_StandaloneVOILUTStorage\UncompressedOrZlib +PresentationContext122 = RETIRED_StoredPrintStorage\UncompressedOrZlib +# draft +PresentationContext123 = DRAFT_RTBeamsDeliveryInstructionStorage\UncompressedOrZlib +PresentationContext124 = DRAFT_SRAudioStorage\UncompressedOrZlib +PresentationContext125 = DRAFT_SRComprehensiveStorage\UncompressedOrZlib +PresentationContext126 = DRAFT_SRDetailStorage\UncompressedOrZlib +PresentationContext127 = DRAFT_SRTextStorage\UncompressedOrZlib +PresentationContext128 = DRAFT_WaveformStorage\UncompressedOrZlib +# +# the following SOP classes are missing in the above list: +# +# - AcquisitionContextSRStorage +# - AdvancedBlendingPresentationStateStorage +# - BodyPositionWaveformStorage +# - BreastProjectionXRayImageStorageForPresentation +# - BreastProjectionXRayImageStorageForProcessing +# - CArmPhotonElectronRadiationRecordStorage +# - CArmPhotonElectronRadiationStorage +# - ColorPaletteStorage +# - CompositingPlanarMPRVolumetricPresentationStateStorage +# - ContentAssessmentResultsStorage +# - CTDefinedProcedureProtocolStorage +# - CTPerformedProcedureProtocolStorage +# - DermoscopicPhotographyImageStorage +# - ElectromyogramWaveformStorage +# - ElectrooculogramWaveformStorage +# - EncapsulatedMTLStorage +# - EncapsulatedOBJStorage +# - EncapsulatedSTLStorage +# - EnhancedXRayRadiationDoseSRStorage +# - ExtensibleSRStorage +# - GrayscalePlanarMPRVolumetricPresentationStateStorage +# - HangingProtocolStorage +# - LegacyConvertedEnhancedCTImageStorage +# - LegacyConvertedEnhancedMRImageStorage +# - LegacyConvertedEnhancedPETImageStorage +# - MicroscopyBulkSimpleAnnotationsStorage +# - MultichannelRespiratoryWaveformStorage +# - MultipleVolumeRenderingVolumetricPresentationStateStorage +# - OphthalmicOpticalCoherenceTomographyBscanVolumeAnalysisStorage +# - OphthalmicOpticalCoherenceTomographyEnFaceImageStorage +# - ParametricMapStorage +# - PatientRadiationDoseSRStorage +# - PerformedImagingAgentAdministrationSRStorage +# - PlannedImagingAgentAdministrationSRStorage +# - ProtocolApprovalStorage +# - RadiopharmaceuticalRadiationDoseSRStorage +# - RoboticArmRadiationStorage +# - RoboticRadiationRecordStorage +# - RoutineScalpElectroencephalogramWaveformStorage +# - RTBrachyApplicationSetupDeliveryInstructionStorage +# - RTPhysicianIntentStorage +# - RTRadiationRecordSetStorage +# - RTRadiationSalvageRecordStorage +# - RTRadiationSetDeliveryInstructionStorage +# - RTRadiationSetStorage +# - RTSegmentAnnotationStorage +# - RTTreatmentPreparationStorage +# - SegmentedVolumeRenderingVolumetricPresentationStateStorage +# - SimplifiedAdultEchoSRStorage +# - SleepElectroencephalogramWaveformStorage +# - TomotherapeuticRadiationRecordStorage +# - TomotherapeuticRadiationStorage +# - TractographyResultsStorage +# - VolumeRenderingVolumetricPresentationStateStorage +# - WideFieldOphthalmicPhotographyStereographicProjectionImageStorage +# - WideFieldOphthalmicPhotography3DCoordinatesImageStorage +# - XADefinedProcedureProtocolStorage +# - XAPerformedProcedureProtocolStorage +# +# - DICOS_2DAITStorage +# - DICOS_3DAITStorage +# - DICOS_CTImageStorage +# - DICOS_DigitalXRayImageStorageForPresentation +# - DICOS_DigitalXRayImageStorageForProcessing +# - DICOS_QuadrupoleResonanceStorage +# - DICOS_ThreatDetectionReportStorage +# +# - DICONDE_EddyCurrentImageStorage +# - DICONDE_EddyCurrentMultiframeImageStorage + +# ---------------------------------------------------------------------------- + +[AllDICOMStorageSCP] +# +# Same as "GenericStorageSCP" but limited to non-retired and non-draft SOP Classes. +# This allows for accepting (almost) all DICOM Storage SOP Classes that are currently +# defined in the standard (an exception is made for some very new DICOM objects because +# of the limitation of 128 Presentation Contexts for SCPs, see DCMTK Feature #540). +# +PresentationContext1 = VerificationSOPClass\Uncompressed +# +# DICOM images +# +PresentationContext2 = BreastProjectionXRayImageStorageForPresentation\AnyTransferSyntax +PresentationContext3 = BreastProjectionXRayImageStorageForProcessing\AnyTransferSyntax +PresentationContext4 = BreastTomosynthesisImageStorage\AnyTransferSyntax +PresentationContext5 = ComputedRadiographyImageStorage\AnyTransferSyntax +PresentationContext6 = CornealTopographyMapStorage\AnyTransferSyntax +PresentationContext7 = CTImageStorage\AnyTransferSyntax +PresentationContext8 = DigitalIntraOralXRayImageStorageForPresentation\AnyTransferSyntax +PresentationContext9 = DigitalIntraOralXRayImageStorageForProcessing\AnyTransferSyntax +PresentationContext10 = DigitalMammographyXRayImageStorageForPresentation\AnyTransferSyntax +PresentationContext11 = DigitalMammographyXRayImageStorageForProcessing\AnyTransferSyntax +PresentationContext12 = DigitalXRayImageStorageForPresentation\AnyTransferSyntax +PresentationContext13 = DigitalXRayImageStorageForProcessing\AnyTransferSyntax +PresentationContext14 = EnhancedCTImageStorage\AnyTransferSyntax +PresentationContext15 = EnhancedMRColorImageStorage\AnyTransferSyntax +PresentationContext16 = EnhancedMRImageStorage\AnyTransferSyntax +PresentationContext17 = EnhancedPETImageStorage\AnyTransferSyntax +PresentationContext18 = EnhancedUSVolumeStorage\AnyTransferSyntax +PresentationContext19 = EnhancedXAImageStorage\AnyTransferSyntax +PresentationContext20 = EnhancedXRFImageStorage\AnyTransferSyntax +PresentationContext21 = IntravascularOpticalCoherenceTomographyImageStorageForPresentation\AnyTransferSyntax +PresentationContext22 = IntravascularOpticalCoherenceTomographyImageStorageForProcessing\AnyTransferSyntax +PresentationContext23 = LegacyConvertedEnhancedCTImageStorage\AnyTransferSyntax +PresentationContext24 = LegacyConvertedEnhancedMRImageStorage\AnyTransferSyntax +PresentationContext25 = LegacyConvertedEnhancedPETImageStorage\AnyTransferSyntax +PresentationContext26 = MRImageStorage\AnyTransferSyntax +PresentationContext27 = MultiframeGrayscaleByteSecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext28 = MultiframeGrayscaleWordSecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext29 = MultiframeSingleBitSecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext30 = MultiframeTrueColorSecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext31 = NuclearMedicineImageStorage\AnyTransferSyntax +PresentationContext32 = OphthalmicPhotography16BitImageStorage\AnyTransferSyntax +PresentationContext33 = OphthalmicPhotography8BitImageStorage\AnyTransferSyntax +PresentationContext34 = OphthalmicThicknessMapStorage\AnyTransferSyntax +PresentationContext35 = OphthalmicTomographyImageStorage\AnyTransferSyntax +PresentationContext36 = ParametricMapStorage\AnyTransferSyntax +PresentationContext37 = PositronEmissionTomographyImageStorage\AnyTransferSyntax +PresentationContext38 = RTImageStorage\AnyTransferSyntax +PresentationContext39 = SecondaryCaptureImageStorage\AnyTransferSyntax +PresentationContext40 = UltrasoundImageStorage\AnyTransferSyntax +PresentationContext41 = UltrasoundMultiframeImageStorage\AnyTransferSyntax +PresentationContext42 = VideoEndoscopicImageStorage\AnyTransferSyntax +PresentationContext43 = VideoMicroscopicImageStorage\AnyTransferSyntax +PresentationContext44 = VideoPhotographicImageStorage\AnyTransferSyntax +PresentationContext45 = VLEndoscopicImageStorage\AnyTransferSyntax +PresentationContext46 = VLMicroscopicImageStorage\AnyTransferSyntax +PresentationContext47 = VLPhotographicImageStorage\AnyTransferSyntax +PresentationContext48 = VLSlideCoordinatesMicroscopicImageStorage\AnyTransferSyntax +PresentationContext49 = VLWholeSlideMicroscopyImageStorage\AnyTransferSyntax +PresentationContext50 = WideFieldOphthalmicPhotographyStereographicProjectionImageStorage\AnyTransferSyntax +PresentationContext51 = WideFieldOphthalmicPhotography3DCoordinatesImageStorage\AnyTransferSyntax +PresentationContext52 = XRay3DAngiographicImageStorage\AnyTransferSyntax +PresentationContext53 = XRay3DCraniofacialImageStorage\AnyTransferSyntax +PresentationContext54 = XRayAngiographicImageStorage\AnyTransferSyntax +PresentationContext55 = XRayRadiofluoroscopicImageStorage\AnyTransferSyntax +# +# all other DICOM objects +# +PresentationContext56 = AcquisitionContextSRStorage\UncompressedOrZlib +PresentationContext57 = AmbulatoryECGWaveformStorage\UncompressedOrZlib +PresentationContext58 = ArterialPulseWaveformStorage\UncompressedOrZlib +PresentationContext59 = AutorefractionMeasurementsStorage\UncompressedOrZlib +PresentationContext60 = BasicStructuredDisplayStorage\UncompressedOrZlib +PresentationContext61 = BasicTextSRStorage\UncompressedOrZlib +PresentationContext62 = BasicVoiceAudioWaveformStorage\UncompressedOrZlib +PresentationContext63 = BlendingSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext64 = CardiacElectrophysiologyWaveformStorage\UncompressedOrZlib +PresentationContext65 = ChestCADSRStorage\UncompressedOrZlib +PresentationContext66 = ColonCADSRStorage\UncompressedOrZlib +PresentationContext67 = ColorSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext68 = CompositingPlanarMPRVolumetricPresentationStateStorage\UncompressedOrZlib +PresentationContext69 = Comprehensive3DSRStorage\UncompressedOrZlib +PresentationContext70 = ComprehensiveSRStorage\UncompressedOrZlib +PresentationContext71 = ContentAssessmentResultsStorage\UncompressedOrZlib +PresentationContext72 = CTDefinedProcedureProtocolStorage\UncompressedOrZlib +PresentationContext73 = CTPerformedProcedureProtocolStorage\UncompressedOrZlib +PresentationContext74 = DeformableSpatialRegistrationStorage\UncompressedOrZlib +PresentationContext75 = EncapsulatedCDAStorage\UncompressedOrZlib +PresentationContext76 = EncapsulatedPDFStorage\UncompressedOrZlib +PresentationContext77 = EnhancedSRStorage\UncompressedOrZlib +PresentationContext78 = ExtensibleSRStorage\UncompressedOrZlib +PresentationContext79 = GeneralAudioWaveformStorage\UncompressedOrZlib +PresentationContext80 = GeneralECGWaveformStorage\UncompressedOrZlib +PresentationContext81 = GenericImplantTemplateStorage\UncompressedOrZlib +PresentationContext82 = GrayscalePlanarMPRVolumetricPresentationStateStorage\UncompressedOrZlib +PresentationContext83 = GrayscaleSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext84 = HangingProtocolStorage\UncompressedOrZlib +PresentationContext85 = HemodynamicWaveformStorage\UncompressedOrZlib +PresentationContext86 = ImplantAssemblyTemplateStorage\UncompressedOrZlib +PresentationContext87 = ImplantationPlanSRDocumentStorage\UncompressedOrZlib +PresentationContext88 = ImplantTemplateGroupStorage\UncompressedOrZlib +PresentationContext89 = IntraocularLensCalculationsStorage\UncompressedOrZlib +PresentationContext90 = KeratometryMeasurementsStorage\UncompressedOrZlib +PresentationContext91 = KeyObjectSelectionDocumentStorage\UncompressedOrZlib +PresentationContext92 = LensometryMeasurementsStorage\UncompressedOrZlib +PresentationContext93 = MacularGridThicknessAndVolumeReportStorage\UncompressedOrZlib +PresentationContext94 = MammographyCADSRStorage\UncompressedOrZlib +PresentationContext95 = MRSpectroscopyStorage\UncompressedOrZlib +PresentationContext96 = OphthalmicAxialMeasurementsStorage\UncompressedOrZlib +PresentationContext97 = OphthalmicVisualFieldStaticPerimetryMeasurementsStorage\UncompressedOrZlib +PresentationContext98 = ProcedureLogStorage\UncompressedOrZlib +PresentationContext99 = PseudoColorSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext100 = RadiopharmaceuticalRadiationDoseSRStorage\UncompressedOrZlib +PresentationContext101 = RawDataStorage\UncompressedOrZlib +PresentationContext102 = RealWorldValueMappingStorage\UncompressedOrZlib +PresentationContext103 = RespiratoryWaveformStorage\UncompressedOrZlib +PresentationContext104 = RTBeamsDeliveryInstructionStorage\UncompressedOrZlib +PresentationContext105 = RTBeamsTreatmentRecordStorage\UncompressedOrZlib +PresentationContext106 = RTBrachyApplicationSetupDeliveryInstructionStorage\UncompressedOrZlib +PresentationContext107 = RTBrachyTreatmentRecordStorage\UncompressedOrZlib +PresentationContext108 = RTDoseStorage\UncompressedOrZlib +PresentationContext109 = RTIonBeamsTreatmentRecordStorage\UncompressedOrZlib +PresentationContext110 = RTIonPlanStorage\UncompressedOrZlib +PresentationContext111 = RTPlanStorage\UncompressedOrZlib +PresentationContext112 = RTStructureSetStorage\UncompressedOrZlib +PresentationContext113 = RTTreatmentSummaryRecordStorage\UncompressedOrZlib +PresentationContext114 = SegmentationStorage\UncompressedOrZlib +PresentationContext115 = SimplifiedAdultEchoSRStorage\UncompressedOrZlib +PresentationContext116 = SpatialFiducialsStorage\UncompressedOrZlib +PresentationContext117 = SpatialRegistrationStorage\UncompressedOrZlib +PresentationContext118 = SpectaclePrescriptionReportStorage\UncompressedOrZlib +PresentationContext119 = StereometricRelationshipStorage\UncompressedOrZlib +PresentationContext120 = SubjectiveRefractionMeasurementsStorage\UncompressedOrZlib +PresentationContext121 = SurfaceScanMeshStorage\UncompressedOrZlib +PresentationContext122 = SurfaceScanPointCloudStorage\UncompressedOrZlib +PresentationContext123 = SurfaceSegmentationStorage\UncompressedOrZlib +PresentationContext124 = TractographyResultsStorage\UncompressedOrZlib +PresentationContext125 = TwelveLeadECGWaveformStorage\UncompressedOrZlib +PresentationContext126 = VisualAcuityMeasurementsStorage\UncompressedOrZlib +PresentationContext127 = XAXRFGrayscaleSoftcopyPresentationStateStorage\UncompressedOrZlib +PresentationContext128 = XRayRadiationDoseSRStorage\UncompressedOrZlib +# +# the following SOP classes are missing in the above list: +# +# - AdvancedBlendingPresentationStateStorage +# - BodyPositionWaveformStorage +# - CArmPhotonElectronRadiationRecordStorage +# - CArmPhotonElectronRadiationStorage +# - ColorPaletteStorage +# - DermoscopicPhotographyImageStorage +# - ElectromyogramWaveformStorage +# - ElectrooculogramWaveformStorage +# - EncapsulatedMTLStorage +# - EncapsulatedOBJStorage +# - EncapsulatedSTLStorage +# - EnhancedXRayRadiationDoseSRStorage +# - MicroscopyBulkSimpleAnnotationsStorage +# - MultichannelRespiratoryWaveformStorage +# - MultipleVolumeRenderingVolumetricPresentationStateStorage +# - OphthalmicOpticalCoherenceTomographyBscanVolumeAnalysisStorage +# - OphthalmicOpticalCoherenceTomographyEnFaceImageStorage +# - PatientRadiationDoseSRStorage +# - PerformedImagingAgentAdministrationSRStorage +# - PlannedImagingAgentAdministrationSRStorage +# - ProtocolApprovalStorage +# - RoboticArmRadiationStorage +# - RoboticRadiationRecordStorage +# - RoutineScalpElectroencephalogramWaveformStorage +# - RTPhysicianIntentStorage +# - RTRadiationRecordSetStorage +# - RTRadiationSalvageRecordStorage +# - RTRadiationSetDeliveryInstructionStorage +# - RTRadiationSetStorage +# - RTSegmentAnnotationStorage +# - RTTreatmentPreparationStorage +# - SegmentedVolumeRenderingVolumetricPresentationStateStorage +# - SleepElectroencephalogramWaveformStorage +# - TomotherapeuticRadiationRecordStorage +# - TomotherapeuticRadiationStorage +# - VolumeRenderingVolumetricPresentationStateStorage +# - XADefinedProcedureProtocolStorage +# - XAPerformedProcedureProtocolStorage +# +# - RETIRED_HardcopyColorImageStorage +# - RETIRED_HardcopyGrayscaleImageStorage +# - RETIRED_NuclearMedicineImageStorage +# - RETIRED_UltrasoundImageStorage +# - RETIRED_UltrasoundMultiframeImageStorage +# - RETIRED_VLImageStorage +# - RETIRED_VLMultiframeImageStorage +# - RETIRED_XRayAngiographicBiPlaneImageStorage +# +# - RETIRED_StandaloneCurveStorage +# - RETIRED_StandaloneModalityLUTStorage +# - RETIRED_StandaloneOverlayStorage +# - RETIRED_StandalonePETCurveStorage +# - RETIRED_StandaloneVOILUTStorage +# - RETIRED_StoredPrintStorage +# +# - DRAFT_RTBeamsDeliveryInstructionStorage +# - DRAFT_SRAudioStorage +# - DRAFT_SRComprehensiveStorage +# - DRAFT_SRDetailStorage +# - DRAFT_SRTextStorage +# - DRAFT_WaveformStorage +# +# - DICOS_2DAITStorage +# - DICOS_3DAITStorage +# - DICOS_CTImageStorage +# - DICOS_DigitalXRayImageStorageForPresentation +# - DICOS_DigitalXRayImageStorageForProcessing +# - DICOS_QuadrupoleResonanceStorage +# - DICOS_ThreatDetectionReportStorage +# +# - DICONDE_EddyCurrentImageStorage +# - DICONDE_EddyCurrentMultiframeImageStorage + +# ============================================================================ +[[Profiles]] +# ============================================================================ + +[Default] +PresentationContexts = GenericStorageSCP + +[AllDICOM] +PresentationContexts = AllDICOMStorageSCP diff --git a/Libs/DICOM/Core/Testing/Cpp/CMakeLists.txt b/Libs/DICOM/Core/Testing/Cpp/CMakeLists.txt index e8637d3525..1973baba55 100644 --- a/Libs/DICOM/Core/Testing/Cpp/CMakeLists.txt +++ b/Libs/DICOM/Core/Testing/Cpp/CMakeLists.txt @@ -9,14 +9,19 @@ create_test_sourcelist(Tests ${KIT}CppTests.cpp ctkDICOMDatabaseTest5.cpp ctkDICOMDatabaseTest6.cpp ctkDICOMDatabaseTest7.cpp + ctkDICOMEchoTest1.cpp ctkDICOMItemTest1.cpp ctkDICOMIndexerTest1.cpp + ctkDICOMJobTest1.cpp + ctkDICOMJobResponseSetTest1.cpp ctkDICOMModelTest1.cpp ctkDICOMPersonNameTest1.cpp ctkDICOMQueryTest1.cpp ctkDICOMQueryTest2.cpp ctkDICOMRetrieveTest1.cpp ctkDICOMRetrieveTest2.cpp + ctkDICOMSchedulerTest1.cpp + ctkDICOMServerTest1.cpp ctkDICOMTesterTest1.cpp ctkDICOMTesterTest2.cpp ) @@ -39,6 +44,10 @@ target_link_libraries(${KIT}CppTests # Add Tests # +SIMPLE_TEST(ctkDICOMJobTest1) +SIMPLE_TEST(ctkDICOMJobResponseSetTest1) +SIMPLE_TEST(ctkDICOMServerTest1) + # ctkDICOMDatabase SIMPLE_TEST(ctkDICOMDatabaseTest1) SIMPLE_TEST(ctkDICOMDatabaseTest2 ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA) @@ -52,6 +61,13 @@ SIMPLE_TEST(ctkDICOMDatabaseTest7) SIMPLE_TEST(ctkDICOMItemTest1) SIMPLE_TEST(ctkDICOMIndexerTest1 ) +# ctkDICOMEcho +SIMPLE_TEST(ctkDICOMEchoTest1 + ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000056.IMA + ) +set_property(TEST "ctkDICOMEchoTest1" PROPERTY RESOURCE_LOCK "dcmqrscp") + # ctkDICOMModel SIMPLE_TEST(ctkDICOMModelTest1 ${CMAKE_CURRENT_BINARY_DIR}/Testing/Temporary/ctkDICOMModelTest1-dicom.db @@ -81,6 +97,21 @@ SIMPLE_TEST( ctkDICOMCoreTest1 ${CMAKE_CURRENT_SOURCE_DIR}/../../Resources/dicom-sample.sql ) +# ctkDICOMScheduler +SIMPLE_TEST(ctkDICOMSchedulerTest1 + ${CTKData_DIR}/Data/DICOM/MRHEAD/000050.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000051.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000052.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000053.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000054.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000056.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000057.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000058.IMA + ${CTKData_DIR}/Data/DICOM/MRHEAD/000059.IMA + ) +set_property(TEST "ctkDICOMSchedulerTest1" PROPERTY RESOURCE_LOCK "dcmqrscp") + # ctkDICOMTester SIMPLE_TEST( ctkDICOMTesterTest1 ) set_property(TEST "ctkDICOMTesterTest1" PROPERTY RESOURCE_LOCK "dcmqrscp") diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMEchoTest1.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMEchoTest1.cpp new file mode 100644 index 0000000000..bb3fe2af79 --- /dev/null +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMEchoTest1.cpp @@ -0,0 +1,101 @@ +/*============================================================================= + + Library: CTK + + Copyright (c) German Cancer Research Center, + Division of Medical and Biological Informatics + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=============================================================================*/ + +// Qt includes +#include +#include +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMEcho.h" +#include "ctkDICOMTester.h" + +// STD includes +#include + +int ctkDICOMEchoTest1(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + + QStringList arguments = app.arguments(); + QString testName = arguments.takeFirst(); + + if (!arguments.count()) + { + std::cerr << "Usage: " << qPrintable(testName) + << " [...]" << std::endl; + return EXIT_FAILURE; + } + + ctkDICOMTester tester; + + std::cout << qPrintable(testName) << ": Starting dcmqrscp" << std::endl; + tester.startDCMQRSCP(); + + std::cout << qPrintable(testName) << ": Storing data to dcmqrscp" << std::endl; + tester.storeData(arguments); + + ctkDICOMEcho echo; + + // Test the default values + CHECK_QSTRING(echo.connectionName(), ""); + CHECK_QSTRING(echo.callingAETitle(), ""); + CHECK_QSTRING(echo.calledAETitle(), ""); + CHECK_QSTRING(echo.host(), ""); + CHECK_INT(echo.port(), 80); + CHECK_INT(echo.connectionTimeout(), 3); + + // Test setting and getting + echo.setConnectionName("connectionName"); + CHECK_QSTRING(echo.connectionName(), "connectionName"); + echo.setCallingAETitle("callingAETitle"); + CHECK_QSTRING(echo.callingAETitle(), "callingAETitle"); + echo.setCalledAETitle("calledAETitle"); + CHECK_QSTRING(echo.calledAETitle(), "calledAETitle"); + echo.setHost("host"); + CHECK_QSTRING(echo.host(), "host"); + echo.setPort(3000); + CHECK_INT(echo.port(), 3000); + echo.setConnectionTimeout(30); + CHECK_INT(echo.connectionTimeout(), 30); + + // Test echo + // this should print: Failed to establish association + CHECK_BOOL(echo.echo(), false); + + // this has to be successful + std::cout << qPrintable(testName) << ": Setting up echo" << std::endl; + echo.setCallingAETitle("CTK_AE"); + echo.setCalledAETitle("CTK_AE"); + echo.setHost("localhost"); + echo.setPort(tester.dcmqrscpPort()); + + std::cout << qPrintable(testName) << ": Running echo" << std::endl; + CHECK_BOOL(echo.echo(), true); + + return EXIT_SUCCESS; +} diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMJobResponseSetTest1.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMJobResponseSetTest1.cpp new file mode 100644 index 0000000000..b653adc4b2 --- /dev/null +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMJobResponseSetTest1.cpp @@ -0,0 +1,76 @@ +/*============================================================================= + + Library: CTK + + Copyright (c) German Cancer Research Center, + Division of Medical and Biological Informatics + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=============================================================================*/ + +// Qt includes +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMJobResponseSet.h" + +int ctkDICOMJobResponseSetTest1(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + + // Query Job and virtual parents (ctkDICOMJob and ctkAbstractJob) + ctkDICOMJobResponseSet jobResponseSet; + + // Test the default values + CHECK_QSTRING(jobResponseSet.filePath(), ""); + CHECK_BOOL(jobResponseSet.copyFile(), false); + CHECK_BOOL(jobResponseSet.overwriteExistingDataset(), false); + CHECK_INT(jobResponseSet.jobType(), ctkDICOMJobResponseSet::JobType::None); + CHECK_QSTRING(jobResponseSet.jobUID(), ""); + CHECK_QSTRING(jobResponseSet.patientID(), ""); + CHECK_QSTRING(jobResponseSet.studyInstanceUID(), ""); + CHECK_QSTRING(jobResponseSet.seriesInstanceUID(), ""); + CHECK_QSTRING(jobResponseSet.sopInstanceUID(), ""); + CHECK_QSTRING(jobResponseSet.connectionName(), ""); + + // Test setting and getting + jobResponseSet.setFilePath("filePath"); + CHECK_QSTRING(jobResponseSet.filePath(), "filePath"); + jobResponseSet.setCopyFile(true); + CHECK_BOOL(jobResponseSet.copyFile(), true); + jobResponseSet.setOverwriteExistingDataset(true); + CHECK_BOOL(jobResponseSet.overwriteExistingDataset(), true); + jobResponseSet.setJobType(ctkDICOMJobResponseSet::JobType::RetrieveStudy); + CHECK_INT(jobResponseSet.jobType(), ctkDICOMJobResponseSet::JobType::RetrieveStudy); + jobResponseSet.setJobUID("JobUID"); + CHECK_QSTRING(jobResponseSet.jobUID(), "JobUID"); + jobResponseSet.setPatientID("patientID"); + CHECK_QSTRING(jobResponseSet.patientID(), "patientID"); + jobResponseSet.setStudyInstanceUID("studyInstanceUID"); + CHECK_QSTRING(jobResponseSet.studyInstanceUID(), "studyInstanceUID"); + jobResponseSet.setSeriesInstanceUID("seriesInstanceUID"); + CHECK_QSTRING(jobResponseSet.seriesInstanceUID(), "seriesInstanceUID"); + jobResponseSet.setSOPInstanceUID("sopInstanceUID"); + CHECK_QSTRING(jobResponseSet.sopInstanceUID(), "sopInstanceUID"); + jobResponseSet.setConnectionName("connectionName"); + CHECK_QSTRING(jobResponseSet.connectionName(), "connectionName"); + + return EXIT_SUCCESS; +} diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMJobTest1.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMJobTest1.cpp new file mode 100644 index 0000000000..f3fa8893a0 --- /dev/null +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMJobTest1.cpp @@ -0,0 +1,141 @@ +/*============================================================================= + + Library: CTK + + Copyright (c) German Cancer Research Center, + Division of Medical and Biological Informatics + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=============================================================================*/ + +// Qt includes +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMInserterJob.h" +#include "ctkDICOMQueryJob.h" +#include "ctkDICOMRetrieveJob.h" +#include "ctkDICOMServer.h" +#include "ctkDICOMStorageListenerJob.h" + +int ctkDICOMJobTest1(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + + // Query Job and virtual parents (ctkDICOMJob and ctkAbstractJob) + ctkDICOMQueryJob queryJob; + + // Test the default values + CHECK_INT(queryJob.status(), ctkAbstractJob::JobStatus::Initialized); + CHECK_BOOL(queryJob.isPersistent(), false); + CHECK_INT(queryJob.retryCounter(), 0); + CHECK_INT(queryJob.retryDelay(), 100); + CHECK_INT(queryJob.maximumNumberOfRetry(), 3); + CHECK_INT(queryJob.maximumConcurrentJobsPerType(), 20); + CHECK_INT(queryJob.priority(), QThread::Priority::LowPriority); + CHECK_INT(queryJob.dicomLevel(), ctkDICOMJob::DICOMLevels::Patients); + CHECK_QSTRING(queryJob.patientID(), ""); + CHECK_QSTRING(queryJob.studyInstanceUID(), ""); + CHECK_QSTRING(queryJob.seriesInstanceUID(), ""); + CHECK_QSTRING(queryJob.sopInstanceUID(), ""); + CHECK_INT(queryJob.maximumPatientsQuery(), 25); + CHECK_POINTER(queryJob.server(), nullptr); + + // Test setting and getting + queryJob.setStatus(ctkAbstractJob::JobStatus::Running); + CHECK_INT(queryJob.status(), ctkAbstractJob::JobStatus::Running); + queryJob.setIsPersistent(true); + CHECK_BOOL(queryJob.isPersistent(), true); + queryJob.setJobUID("JobUID"); + CHECK_QSTRING(queryJob.jobUID(), "JobUID"); + queryJob.setRetryCounter(3); + CHECK_INT(queryJob.retryCounter(), 3); + queryJob.setRetryDelay(300); + CHECK_INT(queryJob.retryDelay(), 300); + queryJob.setMaximumNumberOfRetry(5); + CHECK_INT(queryJob.maximumNumberOfRetry(), 5); + queryJob.setMaximumConcurrentJobsPerType(5); + CHECK_INT(queryJob.maximumConcurrentJobsPerType(), 5); + queryJob.setPriority(QThread::Priority::HighPriority); + CHECK_INT(queryJob.priority(), QThread::Priority::HighPriority); + queryJob.setDICOMLevel(ctkDICOMJob::DICOMLevels::Studies); + CHECK_INT(queryJob.dicomLevel(), ctkDICOMJob::DICOMLevels::Studies); + queryJob.setPatientID("patientID"); + CHECK_QSTRING(queryJob.patientID(), "patientID"); + queryJob.setStudyInstanceUID("studyInstanceUID"); + CHECK_QSTRING(queryJob.studyInstanceUID(), "studyInstanceUID"); + queryJob.setSeriesInstanceUID("seriesInstanceUID"); + CHECK_QSTRING(queryJob.seriesInstanceUID(), "seriesInstanceUID"); + queryJob.setSOPInstanceUID("sopInstanceUID"); + CHECK_QSTRING(queryJob.sopInstanceUID(), "sopInstanceUID"); + queryJob.setMaximumPatientsQuery(100); + CHECK_INT(queryJob.maximumPatientsQuery(), 100); + ctkDICOMServer server; + server.setConnectionName("server"); + queryJob.setServer(server); + CHECK_QSTRING(queryJob.server()->connectionName(), "server"); + + // Inserter Job + ctkDICOMInserterJob inserterJob; + + // Test the default values + CHECK_INT(inserterJob.maximumConcurrentJobsPerType(), 1); + CHECK_QSTRING(inserterJob.databaseFilename(), ""); + QStringList tagsToPrecache; + CHECK_QSTRINGLIST(inserterJob.tagsToPrecache(), tagsToPrecache) + QStringList tagsToExcludeFromStorage; + CHECK_QSTRINGLIST(inserterJob.tagsToExcludeFromStorage(), tagsToExcludeFromStorage) + + // Test setting and getting + inserterJob.setDatabaseFilename("databaseFilename"); + CHECK_QSTRING(inserterJob.databaseFilename(), "databaseFilename"); + tagsToPrecache.append("tagsToPrecache"); + inserterJob.setTagsToPrecache(tagsToPrecache); + CHECK_QSTRINGLIST(inserterJob.tagsToPrecache(), tagsToPrecache) + tagsToExcludeFromStorage.append("tagsToExcludeFromStorage"); + inserterJob.setTagsToExcludeFromStorage(tagsToExcludeFromStorage); + CHECK_QSTRINGLIST(inserterJob.tagsToExcludeFromStorage(), tagsToExcludeFromStorage) + + ctkDICOMRetrieveJob retrieveJob; + + // Test the default values + CHECK_POINTER(retrieveJob.server(), nullptr); + + // Test setting and getting + retrieveJob.setServer(server); + CHECK_QSTRING(retrieveJob.server()->connectionName(), "server"); + + ctkDICOMStorageListenerJob storageListenerJob; + + // Test the default values + CHECK_INT(storageListenerJob.port(), 11112); + CHECK_QSTRING(storageListenerJob.AETitle(), "CTKSTORE"); + CHECK_INT(storageListenerJob.connectionTimeout(), 1); + + // Test setting and getting + storageListenerJob.setPort(80); + CHECK_INT(storageListenerJob.port(), 80); + storageListenerJob.setAETitle("AETitle"); + CHECK_QSTRING(storageListenerJob.AETitle(), "AETitle"); + storageListenerJob.setConnectionTimeout(5); + CHECK_INT(storageListenerJob.connectionTimeout(), 5); + + return EXIT_SUCCESS; +} diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMQueryTest2.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMQueryTest2.cpp index e43dd32a96..febf347193 100644 --- a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMQueryTest2.cpp +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMQueryTest2.cpp @@ -22,8 +22,12 @@ #include #include #include +#include #include +// ctkCore includes +#include + // ctkDICOMCore includes #include "ctkDICOMDatabase.h" #include "ctkDICOMQuery.h" @@ -48,12 +52,20 @@ int ctkDICOMQueryTest2( int argc, char * argv [] ) return EXIT_FAILURE; } + QTemporaryDir tempDirectory; + CHECK_BOOL(tempDirectory.isValid(), true); + ctkDICOMTester tester; tester.startDCMQRSCP(); tester.storeData(arguments); ctkDICOMDatabase database; + QDir databaseDirectory(tempDirectory.path()); + QString dbFile = QFileInfo(databaseDirectory, QString("ctkDICOM.sql")).absoluteFilePath(); + CHECK_BOOL(database.openDatabase(dbFile), true); + database.cleanup(true); + ctkDICOMQuery query; query.setCallingAETitle("CTK_AE"); query.setCalledAETitle("CTK_AE"); diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest1.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest1.cpp index 199c8734ec..a3e2b1a21c 100644 --- a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest1.cpp +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest1.cpp @@ -91,7 +91,7 @@ int ctkDICOMRetrieveTest1( int argc, char * argv [] ) QSharedPointer dicomDatabase(new ctkDICOMDatabase); retrieve.setDatabase(dicomDatabase); - if (retrieve.database() != dicomDatabase) + if (retrieve.dicomDatabase() != dicomDatabase) { std::cerr << __LINE__ << ": ctkDICOMRetrieve::setDatabase() failed." << std::endl; diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest2.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest2.cpp index bc38a3df5d..25625147e8 100644 --- a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest2.cpp +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMRetrieveTest2.cpp @@ -24,8 +24,12 @@ #include #include #include +#include #include +// ctkCore includes +#include + // ctkDICOMCore includes #include "ctkDICOMDatabase.h" #include "ctkDICOMQuery.h" @@ -51,6 +55,9 @@ int ctkDICOMRetrieveTest2( int argc, char * argv [] ) return EXIT_FAILURE; } + QTemporaryDir tempDirectory; + CHECK_BOOL(tempDirectory.isValid(), true); + ctkDICOMTester tester; std::cerr << "ctkDICOMRetrieveTest2: Starting dcmqrscp\n"; tester.startDCMQRSCP(); @@ -58,7 +65,12 @@ int ctkDICOMRetrieveTest2( int argc, char * argv [] ) std::cerr << "ctkDICOMRetrieveTest2: Storing data to dcmqrscp\n"; tester.storeData(arguments); - ctkDICOMDatabase queryDatabase; + ctkDICOMDatabase database; + + QDir databaseDirectory(tempDirectory.path()); + QString dbFile = QFileInfo(databaseDirectory, QString("ctkDICOM.sql")).absoluteFilePath(); + CHECK_BOOL(database.openDatabase(dbFile), true); + database.cleanup(true); std::cerr << "ctkDICOMRetrieveTest2: Setting up query\n"; ctkDICOMQuery query; @@ -68,7 +80,7 @@ int ctkDICOMRetrieveTest2( int argc, char * argv [] ) query.setPort(tester.dcmqrscpPort()); std::cerr << "ctkDICOMRetrieveTest2: Running query\n"; - bool res = query.query(queryDatabase); + bool res = query.query(database); if (!res) { std::cout << "ctkDICOMQuery::query() failed" << std::endl; @@ -81,10 +93,7 @@ int ctkDICOMRetrieveTest2( int argc, char * argv [] ) return EXIT_FAILURE; } - std::cerr << "ctkDICOMRetrieveTest2: Setting up retrieve database\n"; - QSharedPointer retrieveDatabase(new ctkDICOMDatabase); - retrieveDatabase->openDatabase( "./ctkDICOM.sql" ); - + std::cerr << "ctkDICOMRetrieveTest2: Setting up retrieve \n"; ctkDICOMRetrieve retrieve; retrieve.setCallingAETitle("CTK_AE"); retrieve.setCalledAETitle("CTK_AE"); @@ -92,7 +101,7 @@ int ctkDICOMRetrieveTest2( int argc, char * argv [] ) retrieve.setHost("localhost"); retrieve.setMoveDestinationAETitle("CTK_CLIENT_AE"); - retrieve.setDatabase(retrieveDatabase); + retrieve.setDatabase(database); std::cerr << "ctkDICOMRetrieveTest2: Retrieving\n"; typedef QPair StudyAndSeriesInstanceUIDPair; diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMSchedulerTest1.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMSchedulerTest1.cpp new file mode 100644 index 0000000000..6acc63722d --- /dev/null +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMSchedulerTest1.cpp @@ -0,0 +1,167 @@ +/*============================================================================= + + Library: CTK + + Copyright (c) German Cancer Research Center, + Division of Medical and Biological Informatics + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=============================================================================*/ + +// Qt includes +#include +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMScheduler.h" +#include "ctkDICOMServer.h" +#include "ctkDICOMTester.h" + +int ctkDICOMSchedulerTest1(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + + QStringList arguments = app.arguments(); + QString testName = arguments.takeFirst(); + + if (!arguments.count()) + { + std::cerr << "Usage: " << qPrintable(testName) + << " [...]" << std::endl; + return EXIT_FAILURE; + } + + QTemporaryDir tempDirectory; + CHECK_BOOL(tempDirectory.isValid(), true); + + int numberOfImages = arguments.count(); + + ctkDICOMTester tester; + + std::cout << qPrintable(testName) << ": Starting dcmqrscp" << std::endl; + tester.startDCMQRSCP(); + + std::cout << qPrintable(testName) << ": Storing data to dcmqrscp" << std::endl; + tester.storeData(arguments); + + ctkDICOMScheduler scheduler; + + // Test the default values + CHECK_INT(scheduler.maximumThreadCount(), 20); + CHECK_INT(scheduler.maximumNumberOfRetry(), 3); + CHECK_INT(scheduler.retryDelay(), 100); + CHECK_INT(scheduler.maximumPatientsQuery(), 25); + + // Test setting and getting + scheduler.setMaximumThreadCount(19); + CHECK_INT(scheduler.maximumThreadCount(), 19); + scheduler.setMaximumNumberOfRetry(5); + CHECK_INT(scheduler.maximumNumberOfRetry(), 5); + scheduler.setRetryDelay(300); + CHECK_INT(scheduler.retryDelay(), 300); + scheduler.setMaximumPatientsQuery(30); + CHECK_INT(scheduler.maximumPatientsQuery(), 30); + + // Test scheduler + std::cout << qPrintable(testName) << ": Setting up scheduler" << std::endl; + ctkDICOMDatabase database; + + QDir databaseDirectory(tempDirectory.path()); + QString dbFile = QFileInfo(databaseDirectory, QString("ctkDICOM.sql")).absoluteFilePath(); + CHECK_BOOL(database.openDatabase(dbFile), true); + CHECK_BOOL(database.isOpen(), true); + database.cleanup(true); + + CHECK_INT(database.patients().count(), 0); + + scheduler.setDicomDatabase(database); + + ctkDICOMServer server; + server.setConnectionName("Test"); + server.setCallingAETitle("CTK_AE"); + server.setCalledAETitle("CTK_AE"); + server.setHost("localhost"); + server.setPort(tester.dcmqrscpPort()); + server.setRetrieveProtocol(ctkDICOMServer::RetrieveProtocol::CGET); + + scheduler.addServer(server); + + QMap filsers; + filsers.insert("ID", "Facial"); + scheduler.setFilters(filsers); + + std::cout << qPrintable(testName) << ": Running queryStudies" << std::endl; + QString patientID = "Facial Expression"; + scheduler.queryStudies(patientID); + scheduler.waitForFinish(); + + CHECK_INT(database.patients().count(), 1); + + QString patientItem = database.patients().at(0); + QStringList studies = database.studiesForPatient(patientItem); + CHECK_INT(studies.count(), 1); + + std::cout << qPrintable(testName) << ": Running querySeries" << std::endl; + QString studyIstanceUID = studies[0]; + scheduler.querySeries(patientID, studyIstanceUID); + scheduler.waitForFinish(); + + QStringList series = database.seriesForStudy(studyIstanceUID); + CHECK_INT(series.count(), 1); + + std::cout << qPrintable(testName) << ": Running queryInstances" << std::endl; + QString seriesIstanceUID = series[0]; + scheduler.queryInstances(patientID, studyIstanceUID, seriesIstanceUID); + scheduler.waitForFinish(); + + QStringList instances = database.instancesForSeries(seriesIstanceUID); + QStringList files = database.filesForSeries(seriesIstanceUID); + files.removeAll(QString("")); + QStringList urls = database.urlsForSeries(seriesIstanceUID); + urls.removeAll(QString("")); + + CHECK_INT(instances.count(), numberOfImages); + CHECK_INT(files.count(), 0); + CHECK_INT(urls.count(), numberOfImages); + + std::cout << qPrintable(testName) << ": " + << "Running multiple retrieveSOPInstance. " + << "This will test " << numberOfImages << " retrieve concorrent jobs" << std::endl; + + foreach (const QString& sopIstanceUID, instances) + { + scheduler.retrieveSOPInstance(patientID, studyIstanceUID, seriesIstanceUID, sopIstanceUID); + } + + CHECK_INT(scheduler.numberOfJobs(), numberOfImages); + scheduler.waitForFinish(); + + instances = database.instancesForSeries(seriesIstanceUID); + files = database.filesForSeries(seriesIstanceUID); + files.removeAll(QString("")); + urls = database.urlsForSeries(seriesIstanceUID); + urls.removeAll(QString("")); + + CHECK_INT(instances.count(), numberOfImages); + CHECK_INT(files.count(), numberOfImages); + CHECK_INT(urls.count(), 0); + + return EXIT_SUCCESS; +} diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMServerTest1.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMServerTest1.cpp new file mode 100644 index 0000000000..c98573047c --- /dev/null +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMServerTest1.cpp @@ -0,0 +1,78 @@ +/*============================================================================= + + Library: CTK + + Copyright (c) German Cancer Research Center, + Division of Medical and Biological Informatics + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=============================================================================*/ + +// Qt includes +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMServer.h" + +int ctkDICOMServerTest1(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + + ctkDICOMServer server; + + // Test the default values + CHECK_QSTRING(server.connectionName(), ""); + CHECK_QSTRING(server.callingAETitle(), ""); + CHECK_QSTRING(server.calledAETitle(), ""); + CHECK_QSTRING(server.host(), ""); + CHECK_QSTRING(server.retrieveProtocolAsString(), "CGET"); + CHECK_QSTRING(server.moveDestinationAETitle(), ""); + CHECK_INT(server.port(), 80); + CHECK_INT(server.connectionTimeout(), 10); + CHECK_BOOL(server.queryRetrieveEnabled(), true); + CHECK_BOOL(server.storageEnabled(), true); + CHECK_BOOL(server.keepAssociationOpen(), false); + + // Test setting and getting + server.setConnectionName("connectionName"); + CHECK_QSTRING(server.connectionName(), "connectionName"); + server.setCallingAETitle("callingAETitle"); + CHECK_QSTRING(server.callingAETitle(), "callingAETitle"); + server.setCalledAETitle("calledAETitle"); + CHECK_QSTRING(server.calledAETitle(), "calledAETitle"); + server.setHost("host"); + CHECK_QSTRING(server.host(), "host"); + server.setRetrieveProtocolAsString("CMOVE"); + CHECK_QSTRING(server.retrieveProtocolAsString(), "CMOVE"); + server.setMoveDestinationAETitle("moveDestinationAETitle"); + CHECK_QSTRING(server.moveDestinationAETitle(), "moveDestinationAETitle"); + server.setPort(11112); + CHECK_INT(server.port(), 11112); + server.setConnectionTimeout(30); + CHECK_INT(server.connectionTimeout(), 30); + server.setQueryRetrieveEnabled(false); + CHECK_BOOL(server.queryRetrieveEnabled(), false); + server.setStorageEnabled(false); + CHECK_BOOL(server.storageEnabled(), false); + server.setKeepAssociationOpen(true); + CHECK_BOOL(server.keepAssociationOpen(), true); + + return EXIT_SUCCESS; +} diff --git a/Libs/DICOM/Core/ctkDICOMCorePythonQtDecorators.h b/Libs/DICOM/Core/ctkDICOMCorePythonQtDecorators.h index 63f6f8adea..ac9c7de9bf 100644 --- a/Libs/DICOM/Core/ctkDICOMCorePythonQtDecorators.h +++ b/Libs/DICOM/Core/ctkDICOMCorePythonQtDecorators.h @@ -27,6 +27,7 @@ // CTK includes #include #include +#include // NOTE: // @@ -43,10 +44,138 @@ class ctkDICOMCorePythonQtDecorators : public QObject ctkDICOMCorePythonQtDecorators() { + PythonQt::self()->registerCPPClass("ctkDICOMJobDetail", 0, "CTKDICOMCore"); + PythonQt::self()->addParentClass("ctkDICOMJobDetail", "ctkJobDetail", + PythonQtUpcastingOffset()); } public slots: + //---------------------------------------------------------------------------- + // ctkDICOMJobDetail + //---------------------------------------------------------------------------- + ctkDICOMJobDetail* new_ctkDICOMJobDetail() + { + return new ctkDICOMJobDetail(); + } + + void setJobType(ctkDICOMJobDetail* td, ctkDICOMJobResponseSet::JobType jobType) + { + td->JobType = jobType; + } + ctkDICOMJobResponseSet::JobType jobType(ctkDICOMJobDetail* td) + { + return td->JobType; + } + + void setPatientID(ctkDICOMJobDetail* td, const QString& patientID) + { + td->PatientID = patientID; + } + QString patientID(ctkDICOMJobDetail* td) + { + return td->PatientID; + } + + void setStudyInstanceUID(ctkDICOMJobDetail* td, const QString& studyInstanceUID) + { + td->StudyInstanceUID = studyInstanceUID; + } + QString studyInstanceUID(ctkDICOMJobDetail* td) + { + return td->StudyInstanceUID; + } + + void setSeriesInstanceUID(ctkDICOMJobDetail* td, const QString& seriesInstanceUID) + { + td->SeriesInstanceUID = seriesInstanceUID; + } + QString seriesInstanceUID(ctkDICOMJobDetail* td) + { + return td->SeriesInstanceUID; + } + + void setSOPInstanceUID(ctkDICOMJobDetail* td, const QString& sopInstanceUID) + { + td->SOPInstanceUID = sopInstanceUID; + } + QString sopInstanceUID(ctkDICOMJobDetail* td) + { + return td->SOPInstanceUID; + } + + void setConnectionName(ctkDICOMJobDetail* td, const QString& connectionName) + { + td->ConnectionName = connectionName; + } + QString connectionName(ctkDICOMJobDetail* td) + { + return td->ConnectionName; + } + + void setNumberOfDataSets(ctkDICOMJobDetail* td, int numberOfDataSets) + { + td->NumberOfDataSets = numberOfDataSets; + } + int numberOfDataSets(ctkDICOMJobDetail* td) + { + return td->NumberOfDataSets; + } + + //---------------------------------------------------------------------------- + // ctkDICOMJobResponseSet + //---------------------------------------------------------------------------- + void setFilePath(ctkDICOMJobResponseSet* ts, const QString& filePath) + { + ts->setFilePath(filePath); + } + + void setCopyFile(ctkDICOMJobResponseSet* ts, bool copyFile) + { + ts->setCopyFile(copyFile); + } + + void setOverwriteExistingDataset(ctkDICOMJobResponseSet* ts, bool overwriteExistingDataset) + { + ts->setOverwriteExistingDataset(overwriteExistingDataset); + } + + void setJobType(ctkDICOMJobResponseSet* ts, ctkDICOMJobResponseSet::JobType jobType) + { + ts->setJobType(jobType); + } + + void setJobUID(ctkDICOMJobResponseSet* ts, const QString& jobUID) + { + ts->setJobUID(jobUID); + } + + void setPatientID(ctkDICOMJobResponseSet* ts, const QString& patientID) + { + ts->setPatientID(patientID); + } + + void setStudyInstanceUID(ctkDICOMJobResponseSet* ts, const QString& studyInstanceUID) + { + ts->setStudyInstanceUID(studyInstanceUID); + } + + void setSeriesInstanceUID(ctkDICOMJobResponseSet* ts, const QString& seriesInstanceUID) + { + ts->setSeriesInstanceUID(seriesInstanceUID); + } + + void setSOPInstanceUID(ctkDICOMJobResponseSet* ts, const QString& sopInstanceUID) + { + ts->setSOPInstanceUID(sopInstanceUID); + } + + void setConnectionName(ctkDICOMJobResponseSet* ts, const QString& connectionName) + { + ts->setConnectionName(connectionName); + } + + //---------------------------------------------------------------------------- // ctkDICOMDisplayedFieldGeneratorRuleFactory diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index e02d692cab..70cf79c938 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -36,6 +36,7 @@ #include "ctkDICOMDatabase_p.h" #include "ctkDICOMAbstractThumbnailGenerator.h" #include "ctkDICOMItem.h" +#include "ctkDICOMJobResponseSet.h" #include "ctkLogger.h" #include "ctkUtils.h" @@ -75,7 +76,6 @@ static QString TableFieldSeparator(":"); //------------------------------------------------------------------------------ ctkDICOMDatabasePrivate::ctkDICOMDatabasePrivate(ctkDICOMDatabase& o) : q_ptr(&o) - , LoggedExecVerbose(false) , DisplayedFieldsTableAvailable(false) , UseShortStoragePath(true) , ThumbnailGenerator(nullptr) @@ -162,15 +162,12 @@ bool ctkDICOMDatabasePrivate::loggedExec(QSqlQuery& query, const QString& queryS if (!success) { QSqlError sqlError = query.lastError(); - logger.debug( "SQL failed\n Bad SQL: " + query.lastQuery()); - logger.debug( "Error text: " + sqlError.text()); + logger.debug("SQL failed\n Bad SQL: " + query.lastQuery()); + logger.debug("Error text: " + sqlError.text()); } else { - if (LoggedExecVerbose) - { - logger.debug( "SQL worked!\n SQL: " + query.lastQuery()); - } + logger.debug("SQL worked!\n SQL: " + query.lastQuery()); } return (success); } @@ -188,10 +185,7 @@ bool ctkDICOMDatabasePrivate::loggedExecBatch(QSqlQuery& query) } else { - if (LoggedExecVerbose) - { - logger.debug( "SQL worked!\n SQL: " + query.lastQuery()); - } + logger.debug("SQL worked!\n SQL: " + query.lastQuery()); } return (success); } @@ -286,10 +280,7 @@ bool ctkDICOMDatabasePrivate::executeScript(const QString script) { if (! (*it).startsWith("--") ) { - if (LoggedExecVerbose) - { - qDebug() << *it << "\n"; - } + logger.debug(*it + "\n"); query.exec(*it); if (query.lastError().type()) { @@ -324,13 +315,10 @@ bool ctkDICOMDatabasePrivate::insertPatient(const ctkDICOMItem& dataset, int& db // Check if patient is already present in the db - QString patientsName, patientID, studyInstanceUID, seriesInstanceUID; - if (!this->uidsForDataSet(dataset, patientsName, patientID, studyInstanceUID, seriesInstanceUID)) - { - // error occurred, message is already logged - return false; - } - QString patientsBirthDate(dataset.GetElementAsString(DCM_PatientBirthDate)); + QString patientsName, patientID, patientsBirthDate; + patientsName = dataset.GetElementAsString(DCM_PatientName); + patientID = dataset.GetElementAsString(DCM_PatientID); + patientsBirthDate = dataset.GetElementAsString(DCM_PatientBirthDate); QSqlQuery checkPatientExistsQuery(this->Database); checkPatientExistsQuery.prepare("SELECT * FROM Patients WHERE PatientID = ? AND PatientsName = ?"); @@ -343,15 +331,12 @@ bool ctkDICOMDatabasePrivate::insertPatient(const ctkDICOMItem& dataset, int& db { // we found him dbPatientID = checkPatientExistsQuery.value(checkPatientExistsQuery.record().indexOf("UID")).toInt(); - if (this->LoggedExecVerbose) + logger.debug("Found patient in the database as UId: " + QString::number(dbPatientID)); + foreach(QString key, this->InsertedPatientsCompositeIDCache.keys()) { - qDebug() << "Found patient in the database as UId: " << dbPatientID; - foreach(QString key, this->InsertedPatientsCompositeIDCache.keys()) - { - qDebug() << "Patient ID cache item: " << key<< "->" << this->InsertedPatientsCompositeIDCache[key]; - } - qDebug() << "New patient ID cache item: " << compositeID << "->" << dbPatientID; + logger.debug("Patient ID cache item: " + key + "->" + this->InsertedPatientsCompositeIDCache[key]); } + logger.debug("New patient ID cache item: " + compositeID + "->" + dbPatientID); this->InsertedPatientsCompositeIDCache[compositeID] = dbPatientID; return false; } @@ -381,10 +366,7 @@ bool ctkDICOMDatabasePrivate::insertPatient(const ctkDICOMItem& dataset, int& db loggedExec(insertPatientStatement); dbPatientID = insertPatientStatement.lastInsertId().toInt(); this->InsertedPatientsCompositeIDCache[compositeID] = dbPatientID; - if (this->LoggedExecVerbose) - { - logger.debug("New patient inserted: database item ID = " + QString().setNum(dbPatientID)); - } + logger.debug("New patient inserted: database item ID = " + QString().setNum(dbPatientID)); return true; } } @@ -399,10 +381,7 @@ bool ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& dataset, int dbPat checkStudyExistsQuery.exec(); if (!checkStudyExistsQuery.next()) { - if (this->LoggedExecVerbose) - { - qDebug() << "Need to insert new study: " << studyInstanceUID; - } + logger.debug("Need to insert new study: " + studyInstanceUID); QString studyID(dataset.GetElementAsString(DCM_StudyID) ); QString studyDate(dataset.GetElementAsString(DCM_StudyDate) ); @@ -433,7 +412,7 @@ bool ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& dataset, int dbPat insertStudyStatement.addBindValue( QDateTime::currentDateTime() ); if (!insertStudyStatement.exec()) { - logger.error( "Error executing statement: " + insertStudyStatement.lastQuery() + " Error: " + insertStudyStatement.lastError().text() ); + logger.error("Error executing statement: " + insertStudyStatement.lastQuery() + " Error: " + insertStudyStatement.lastError().text() ); } else { @@ -444,10 +423,7 @@ bool ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& dataset, int dbPat } else { - if (this->LoggedExecVerbose) - { - qDebug() << "Used existing study: " << studyInstanceUID; - } + logger.debug("Used existing study: " + studyInstanceUID); this->InsertedStudyUIDsCache.insert(studyInstanceUID); return false; } @@ -460,17 +436,11 @@ bool ctkDICOMDatabasePrivate::insertSeries(const ctkDICOMItem& dataset, QString QSqlQuery checkSeriesExistsQuery(this->Database); checkSeriesExistsQuery.prepare( "SELECT * FROM Series WHERE SeriesInstanceUID = ?" ); checkSeriesExistsQuery.bindValue( 0, seriesInstanceUID ); - if (this->LoggedExecVerbose) - { - logger.warn( "Statement: " + checkSeriesExistsQuery.lastQuery() ); - } + logger.debug("Statement: " + checkSeriesExistsQuery.lastQuery() ); checkSeriesExistsQuery.exec(); if (!checkSeriesExistsQuery.next()) { - if (this->LoggedExecVerbose) - { - qDebug() << "Need to insert new series: " << seriesInstanceUID; - } + logger.debug("Need to insert new series: " + seriesInstanceUID); QString seriesDate(dataset.GetElementAsString(DCM_SeriesDate) ); QString seriesTime(dataset.GetElementAsString(DCM_SeriesTime) ); @@ -520,10 +490,7 @@ bool ctkDICOMDatabasePrivate::insertSeries(const ctkDICOMItem& dataset, QString } else { - if (this->LoggedExecVerbose) - { - qDebug() << "Used existing series: " << seriesInstanceUID; - } + logger.debug("Used existing series: " + seriesInstanceUID); this->InsertedSeriesUIDsCache.insert(seriesInstanceUID); return false; } @@ -537,8 +504,14 @@ bool ctkDICOMDatabasePrivate::openTagCacheDatabase() { return true; } + QString tagCacheConnectionName = this->Database.connectionName() + "TagCache"; + if (QSqlDatabase::contains(tagCacheConnectionName)) + { + QSqlDatabase::removeDatabase(tagCacheConnectionName); + } + this->TagCacheDatabase = QSqlDatabase::addDatabase( - "QSQLITE", this->Database.connectionName() + "TagCache"); + "QSQLITE", tagCacheConnectionName); this->TagCacheDatabase.setDatabaseName(this->TagCacheDatabaseFilename); if ( !this->TagCacheDatabase.open() ) { @@ -583,20 +556,18 @@ void ctkDICOMDatabasePrivate::precacheTags(const ctkDICOMItem& dataset, const QS { value = dataset.GetAllElementValuesAsString(tagKey); } + sopInstanceUIDs << sopInstanceUID; tags << upperTag; values << value; } - this->TagCacheDatabase.transaction(); q->cacheTags(sopInstanceUIDs, tags, values); - this->TagCacheDatabase.commit(); } //------------------------------------------------------------------------------ bool ctkDICOMDatabasePrivate::removeImage(const QString& sopInstanceUID) { - Q_Q(ctkDICOMDatabase); QSqlQuery deleteFile(Database); deleteFile.prepare("DELETE FROM Images WHERE SOPInstanceUID == :sopInstanceUID"); deleteFile.bindValue(":sopInstanceUID", sopInstanceUID); @@ -661,10 +632,7 @@ bool ctkDICOMDatabasePrivate::storeDatasetFile(const ctkDICOMItem& dataset, cons if (originalFilePath.isEmpty()) { - if (this->LoggedExecVerbose) - { - logger.debug("Saving file: " + storedFilePath); - } + logger.debug("Saving file: " + storedFilePath); if (!dataset.SaveToFile(storedFilePath)) { logger.error("Error saving file: " + storedFilePath); @@ -676,10 +644,7 @@ bool ctkDICOMDatabasePrivate::storeDatasetFile(const ctkDICOMItem& dataset, cons // we're inserting an existing file QFile currentFile(originalFilePath); currentFile.copy(storedFilePath); - if (this->LoggedExecVerbose) - { - logger.debug("Copy file from: " + originalFilePath + " to: " + storedFilePath); - } + logger.debug("Copy file from: " + originalFilePath + " to: " + storedFilePath); } return true; @@ -689,7 +654,6 @@ bool ctkDICOMDatabasePrivate::storeDatasetFile(const ctkDICOMItem& dataset, cons bool ctkDICOMDatabasePrivate::indexingStatusForFile(const QString& filePath, const QString& sopInstanceUID, bool& datasetInDatabase, bool& datasetUpToDate, QString& databaseFilename) { - Q_Q(ctkDICOMDatabase); datasetInDatabase = false; datasetUpToDate = false; databaseFilename.clear(); @@ -756,20 +720,15 @@ bool ctkDICOMDatabasePrivate::insertPatientStudySeries(const ctkDICOMItem& datas } else { - if (this->LoggedExecVerbose) - { - qDebug() << "Insert new patient if not already in database: " << patientID << " " << patientsName; - } + logger.debug("Insert new patient if not already in database: " + patientID + " " + patientsName); if (this->insertPatient(dataset, dbPatientID)) { databaseWasChanged = true; emit q->patientAdded(dbPatientID, patientID, patientsName, patientsBirthDate); } } - if (this->LoggedExecVerbose) - { - qDebug() << "Going to insert this instance with dbPatientID: " << dbPatientID; - } + + logger.debug("Going to insert this instance with dbPatientID: " + QString::number(dbPatientID)); // Insert new study if needed QString studyInstanceUID(dataset.GetElementAsString(DCM_StudyInstanceUID)); @@ -777,10 +736,7 @@ bool ctkDICOMDatabasePrivate::insertPatientStudySeries(const ctkDICOMItem& datas { if (this->insertStudy(dataset, dbPatientID)) { - if (this->LoggedExecVerbose) - { - qDebug() << "Study Added"; - } + logger.debug("Study Added"); databaseWasChanged = true; // let users of this class track when things happen emit q->studyAdded(studyInstanceUID); @@ -792,10 +748,7 @@ bool ctkDICOMDatabasePrivate::insertPatientStudySeries(const ctkDICOMItem& datas { if (this->insertSeries(dataset, studyInstanceUID)) { - if (this->LoggedExecVerbose) - { - qDebug() << "Series Added"; - } + logger.debug("Series Added"); databaseWasChanged = true; emit q->seriesAdded(seriesInstanceUID); } @@ -836,7 +789,6 @@ bool ctkDICOMDatabasePrivate::storeThumbnailFile(const QString& originalFilePath bool ctkDICOMDatabasePrivate::uidsForDataSet(const ctkDICOMItem& dataset, QString& patientsName, QString& patientID, QString& studyInstanceUID, QString& seriesInstanceUID) { - Q_Q(ctkDICOMDatabase); // If the following fields can not be evaluated, cancel evaluation of the DICOM file patientsName = dataset.GetElementAsString(DCM_PatientName); patientID = dataset.GetElementAsString(DCM_PatientID); @@ -848,7 +800,6 @@ bool ctkDICOMDatabasePrivate::uidsForDataSet(const ctkDICOMItem& dataset, //------------------------------------------------------------------------------ bool ctkDICOMDatabasePrivate::uidsForDataSet(QString& patientsName, QString& patientID, QString& studyInstanceUID) { - Q_Q(ctkDICOMDatabase); if (patientID.isEmpty() && !studyInstanceUID.isEmpty()) { // Use study instance uid as patient id if patient id is empty - can happen on anonymized datasets @@ -883,20 +834,19 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& QString sopInstanceUID(dataset.GetElementAsString(DCM_SOPInstanceUID)); // Check to see if the file has already been loaded - if (this->LoggedExecVerbose) - { - qDebug() << "inserting filePath: " << filePath; - } + logger.debug("inserting filePath: " + filePath); // Check if the file has been already indexed and skip indexing if it is bool datasetInDatabase = false; bool datasetUpToDate = false; QString databaseFilename; + if (!indexingStatusForFile(filePath, sopInstanceUID, datasetInDatabase, datasetUpToDate, databaseFilename)) { // error occurred, message is already logged return; } + if (datasetInDatabase) { if (datasetUpToDate) @@ -932,20 +882,16 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& } bool databaseWasChanged = this->insertPatientStudySeries(dataset, patientID, patientsName); - - if (!storedFilePath.isEmpty() && !seriesInstanceUID.isEmpty()) + if (!sopInstanceUID.isEmpty() && !seriesInstanceUID.isEmpty() && !storedFilePath.isEmpty()) { - if (this->LoggedExecVerbose) - { - qDebug() << "Maybe add Instance"; - } + logger.debug("Maybe add Instance"); bool alreadyInserted = false; if (!storeFile) { // file is linked, maybe it is already inserted QSqlQuery checkImageExistsQuery(Database); - checkImageExistsQuery.prepare("SELECT * FROM Images WHERE Filename = ?"); - checkImageExistsQuery.addBindValue(storedFilePath); + checkImageExistsQuery.prepare("SELECT * FROM Images WHERE SOPInstanceUID = ?"); + checkImageExistsQuery.addBindValue(sopInstanceUID); checkImageExistsQuery.exec(); alreadyInserted = checkImageExistsQuery.next(); } @@ -971,17 +917,23 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& insertImageStatement.addBindValue(QString("")); insertImageStatement.addBindValue(seriesInstanceUID); insertImageStatement.addBindValue(QDateTime::currentDateTime()); - insertImageStatement.exec(); - // insert was needed, so cache any application-requested tags - this->precacheTags(dataset, sopInstanceUID); + if ( !insertImageStatement.exec() ) + { + logger.error("Error executing statement: " + + insertImageStatement.lastQuery() + + " Error: " + insertImageStatement.lastError().text()); + } + else + { + // insert was needed, so cache any application-requested tags + this->precacheTags(dataset, sopInstanceUID); + } // let users of this class track when things happen emit q->instanceAdded(sopInstanceUID); - if (this->LoggedExecVerbose) - { - qDebug() << "Instance Added"; - } + + logger.debug("Instance Added"); databaseWasChanged = true; } if (generateThumbnail) @@ -995,154 +947,6 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& } } -//------------------------------------------------------------------------------ -void ctkDICOMDatabase::insert(const QList& indexingResults) -{ - Q_D(ctkDICOMDatabase); - bool databaseWasChanged = false; - - d->TagCacheDatabase.transaction(); - d->Database.transaction(); - - QDir databaseDirectory(this->databaseDirectory()); - foreach(const ctkDICOMDatabase::IndexingResult & indexingResult, indexingResults) - { - const ctkDICOMItem& dataset = *indexingResult.dataset.data(); - QString filePath = indexingResult.filePath; - bool generateThumbnail = false; // thumbnail will be generated when needed, don't slow down import with that - bool storeFile = indexingResult.copyFile; - - // Check to see if the file has already been loaded - QString sopInstanceUID(dataset.GetElementAsString(DCM_SOPInstanceUID)); - bool datasetInDatabase = false; - bool datasetUpToDate = false; - if (indexingResult.overwriteExistingDataset) - { - // overwrite was requested based on exact file match - datasetInDatabase = true; - datasetUpToDate = false; - } - else - { - // there is no exact file match, but there may be still a different file in the database - // for the same SOP instance UID - QString databaseFilename; - if (!d->indexingStatusForFile(filePath, sopInstanceUID, datasetInDatabase, datasetUpToDate, databaseFilename)) - { - // error occurred, message is already logged - continue; - } - } - - if (datasetInDatabase) - { - if (datasetUpToDate) - { - continue; - } - // File is updated, delete record and re-index - if (!d->removeImage(sopInstanceUID)) - { - logger.error("Failed to insert file into database (cannot update pre-existing item): " + filePath); - continue; - } - } - - // Verify that minimum required fields are present - QString patientsName, patientID, studyInstanceUID, seriesInstanceUID; - if (!d->uidsForDataSet(dataset, patientsName, patientID, studyInstanceUID, seriesInstanceUID)) - { - logger.error("Failed to insert file into database (required fields missing): " + filePath); - continue; - } - - // Store a copy of the dataset - QString storedFilePath = filePath; - if (storeFile && !seriesInstanceUID.isEmpty() && !this->isInMemory()) - { - if (!d->storeDatasetFile(dataset, filePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID, storedFilePath)) - { - continue; - } - } - - if (d->insertPatientStudySeries(dataset, patientID, patientsName)) - { - databaseWasChanged = true; - } - - if (!storedFilePath.isEmpty() && !seriesInstanceUID.isEmpty()) - { - // Insert all pre-cached fields into tag cache - QSqlQuery insertTags(d->TagCacheDatabase); - insertTags.prepare("INSERT OR REPLACE INTO TagCache VALUES(?,?,?)"); - insertTags.bindValue(0, sopInstanceUID); - foreach(const QString & tag, d->TagsToPrecache) - { - QString upperTag = tag.toUpper(); - unsigned short group, element; - this->tagToGroupElement(upperTag, group, element); - DcmTagKey tagKey(group, element); - QString value; - if (d->TagsToExcludeFromStorage.contains(upperTag)) - { - if (dataset.TagExists(tagKey)) - { - value = ValueIsNotStored; - } - else - { - value = TagNotInInstance; - } - } - else - { - value = dataset.GetAllElementValuesAsString(tagKey); - } - insertTags.bindValue(1, upperTag); - if (value.isEmpty()) - { - insertTags.bindValue(2, TagNotInInstance); - } - else - { - insertTags.bindValue(2, value); - } - insertTags.exec(); - } - - // Insert image files - QSqlQuery insertImageStatement(d->Database); - insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ?, ? )"); - insertImageStatement.addBindValue(sopInstanceUID); - insertImageStatement.addBindValue(d->internalPathFromAbsolute(storedFilePath)); - insertImageStatement.addBindValue(QString("")); - insertImageStatement.addBindValue(seriesInstanceUID); - insertImageStatement.addBindValue(QDateTime::currentDateTime()); - insertImageStatement.exec(); - emit instanceAdded(sopInstanceUID); - if (d->LoggedExecVerbose) - { - qDebug() << "Instance Added"; - } - databaseWasChanged = true; - - if (generateThumbnail) - { - d->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); - } - } - } - - d->Database.commit(); - d->TagCacheDatabase.commit(); - - if (databaseWasChanged && this->isInMemory()) - { - emit this->databaseChanged(); - } -} - //------------------------------------------------------------------------------ QString ctkDICOMDatabasePrivate::getDisplayPatientFieldsKey(const QString& patientID, const QString& patientsName, const QString& patientsBirthDate, @@ -1335,6 +1139,10 @@ bool ctkDICOMDatabasePrivate::applyDisplayedFieldsChanges( QMap currentStudy = displayedFieldsMapStudy[currentStudyInstanceUid]; QSqlQuery displayStudiesQuery(this->Database); displayStudiesQuery.prepare("SELECT StudyInstanceUID FROM Studies WHERE StudyInstanceUID = ? ;"); @@ -1388,6 +1196,10 @@ bool ctkDICOMDatabasePrivate::applyDisplayedFieldsChanges( QMap currentSeries = displayedFieldsMapSeries[currentSeriesInstanceUid]; QSqlQuery displaySeriesQuery(this->Database); @@ -1485,7 +1297,8 @@ ctkDICOMDatabase::~ctkDICOMDatabase() } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& connectionName ) +bool ctkDICOMDatabase::openDatabase(const QString& databaseFile, + const QString& connectionName) { Q_D(ctkDICOMDatabase); bool wasOpen = this->isOpen(); @@ -1510,16 +1323,22 @@ void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& c { verifiedConnectionName = QUuid::createUuid().toString(); } + + if (QSqlDatabase::contains(verifiedConnectionName)) + { + QSqlDatabase::removeDatabase(verifiedConnectionName); + } + d->Database = QSqlDatabase::addDatabase("QSQLITE", verifiedConnectionName); d->Database.setDatabaseName(databaseFile); - if ( ! (d->Database.open()) ) + if (!(d->Database.open())) { d->LastError = d->Database.lastError().text(); if (wasOpen) { emit closed(); } - return; + return false; } // Disable synchronous writing to make modifications faster @@ -1536,7 +1355,7 @@ void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& c { emit closed(); } - return; + return false; } } d->resetLastInsertedValues(); @@ -1567,6 +1386,7 @@ void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& c this->setTagsToPrecache(tags); emit opened(); + return true; } //------------------------------------------------------------------------------ @@ -2366,17 +2186,29 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) { Q_D(ctkDICOMDatabase); + if (fileName.isEmpty()) + { + return ""; + } + // Read from cache, if available // first, try treating argument as filePath QString sopInstanceUID = this->instanceForFile(fileName); // second, try treating argument as a url + bool isUrl = false; if (sopInstanceUID.isEmpty()) { + isUrl = true; sopInstanceUID = this->instanceForURL(fileName); } + if (sopInstanceUID.isEmpty()) + { + return ""; + } + // third, look for the value tag = tag.toUpper(); QString value = this->cachedTag(sopInstanceUID, tag); @@ -2389,7 +2221,12 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) return value; } - // Read value from file as a fallback (won't work if fileName is a URL) + if (isUrl) + { + return ""; + } + + // Read value from file as a fallback value = d->readValueFromFile(fileName, sopInstanceUID, tag); return value; } @@ -2397,7 +2234,6 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) //------------------------------------------------------------------------------ QString ctkDICOMDatabase::fileValue(const QString fileName, const unsigned short group, const unsigned short element) { - Q_D(ctkDICOMDatabase); QString tag = this->groupElementToTag(group, element); return this->fileValue(fileName, tag); } @@ -2438,7 +2274,14 @@ bool ctkDICOMDatabase::instanceValueExists(const QString sopInstanceUID, const u bool ctkDICOMDatabase::fileValueExists(const QString fileName, QString tag) { Q_D(ctkDICOMDatabase); + + if (fileName.isEmpty()) + { + return false; + } + tag = tag.toUpper(); + QString sopInstanceUID = this->instanceForFile(fileName); QString value = this->cachedTag(sopInstanceUID, tag); if (value == TagNotInInstance || value == ValueIsEmptyString) @@ -2458,7 +2301,6 @@ bool ctkDICOMDatabase::fileValueExists(const QString fileName, QString tag) //------------------------------------------------------------------------------ bool ctkDICOMDatabase::fileValueExists(const QString fileName, const unsigned short group, const unsigned short element) { - Q_D(ctkDICOMDatabase); QString tag = this->groupElementToTag(group, element); return this->fileValueExists(fileName, tag); } @@ -2521,14 +2363,6 @@ void ctkDICOMDatabase::insert( const ctkDICOMItem& dataset, bool storeFile, bool d->insert(dataset, QString(), storeFile, generateThumbnail); } -//------------------------------------------------------------------------------ -void ctkDICOMDatabase::insert(const QString& filePath, const ctkDICOMItem& dataset, - bool storeFile, bool generateThumbnail) -{ - Q_D(ctkDICOMDatabase); - d->insert(dataset, filePath, storeFile, generateThumbnail); -} - //------------------------------------------------------------------------------ void ctkDICOMDatabase::insert( const QString& filePath, bool storeFile, bool generateThumbnail, bool createHierarchy, const QString& destinationDirectoryName) { @@ -2543,10 +2377,7 @@ void ctkDICOMDatabase::insert( const QString& filePath, bool storeFile, bool gen return; } - if (d->LoggedExecVerbose) - { - logger.debug( "Processing " + filePath ); - } + logger.debug( "Processing " + filePath ); ctkDICOMItem dataset; @@ -2562,10 +2393,423 @@ void ctkDICOMDatabase::insert( const QString& filePath, bool storeFile, bool gen } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::setTagsToPrecache(const QStringList tags) +void ctkDICOMDatabase::insert(const QList& indexingResults) { Q_D(ctkDICOMDatabase); - if (d->TagsToPrecache == tags) + bool databaseWasChanged = false; + + d->TagCacheDatabase.transaction(); + d->Database.transaction(); + + QDir databaseDirectory(this->databaseDirectory()); + foreach(const ctkDICOMDatabase::IndexingResult & indexingResult, indexingResults) + { + const ctkDICOMItem& dataset = *indexingResult.dataset.data(); + QString filePath = indexingResult.filePath; + bool generateThumbnail = false; // thumbnail will be generated when needed, don't slow down import with that + bool storeFile = indexingResult.copyFile; + + // Check to see if the file has already been loaded + QString sopInstanceUID(dataset.GetElementAsString(DCM_SOPInstanceUID)); + bool datasetInDatabase = false; + bool datasetUpToDate = false; + if (indexingResult.overwriteExistingDataset) + { + // overwrite was requested based on exact file match + datasetInDatabase = true; + datasetUpToDate = false; + } + else + { + // there is no exact file match, but there may be still a different file in the database + // for the same SOP instance UID + QString databaseFilename; + if (!d->indexingStatusForFile(filePath, sopInstanceUID, datasetInDatabase, datasetUpToDate, databaseFilename)) + { + // error occurred, message is already logged + continue; + } + } + + if (datasetInDatabase) + { + if (datasetUpToDate) + { + continue; + } + // File is updated, delete record and re-index + if (!d->removeImage(sopInstanceUID)) + { + logger.error("Failed to insert file into database (cannot update pre-existing item): " + filePath); + continue; + } + } + + // Verify that minimum required fields are present + QString patientsName, patientID, studyInstanceUID, seriesInstanceUID; + if (!d->uidsForDataSet(dataset, patientsName, patientID, studyInstanceUID, seriesInstanceUID)) + { + logger.error("Failed to insert file into database (required fields missing): " + filePath); + continue; + } + + // Store a copy of the dataset + QString storedFilePath = filePath; + if (storeFile && !seriesInstanceUID.isEmpty() && !this->isInMemory()) + { + if (!d->storeDatasetFile(dataset, filePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID, storedFilePath)) + { + continue; + } + } + + if (d->insertPatientStudySeries(dataset, patientID, patientsName)) + { + databaseWasChanged = true; + } + + if (!storedFilePath.isEmpty() && !seriesInstanceUID.isEmpty()) + { + // Insert all pre-cached fields into tag cache + QSqlQuery insertTags(d->TagCacheDatabase); + insertTags.prepare("INSERT OR REPLACE INTO TagCache VALUES(?,?,?)"); + insertTags.bindValue(0, sopInstanceUID); + foreach(const QString & tag, d->TagsToPrecache) + { + unsigned short group, element; + this->tagToGroupElement(tag, group, element); + DcmTagKey tagKey(group, element); + QString value; + if (d->TagsToExcludeFromStorage.contains(tag)) + { + if (dataset.TagExists(tagKey)) + { + value = ValueIsNotStored; + } + else + { + value = TagNotInInstance; + } + } + else + { + value = dataset.GetAllElementValuesAsString(tagKey); + } + insertTags.bindValue(1, tag); + if (value.isEmpty()) + { + insertTags.bindValue(2, TagNotInInstance); + } + else + { + insertTags.bindValue(2, value); + } + insertTags.exec(); + } + + // Insert image files + QSqlQuery insertImageStatement(d->Database); + insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ?, ? )"); + insertImageStatement.addBindValue(sopInstanceUID); + insertImageStatement.addBindValue(d->internalPathFromAbsolute(storedFilePath)); + insertImageStatement.addBindValue(QString("")); + insertImageStatement.addBindValue(seriesInstanceUID); + insertImageStatement.addBindValue(QDateTime::currentDateTime()); + insertImageStatement.exec(); + emit instanceAdded(sopInstanceUID); + logger.debug( "Instance Added" ); + databaseWasChanged = true; + + if (generateThumbnail) + { + d->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); + } + } + } + + d->Database.commit(); + d->TagCacheDatabase.commit(); + + if (databaseWasChanged && this->isInMemory()) + { + emit this->databaseChanged(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::insert(QList> jobResponseSets) +{ + Q_D(ctkDICOMDatabase); + + bool databaseWasChanged = false; + + d->TagCacheDatabase.transaction(); + d->Database.transaction(); + + QDir databaseDirectory(this->databaseDirectory()); + foreach (QSharedPointer jobResponseSet, jobResponseSets) + { + ctkDICOMJobResponseSet::JobType jobType = jobResponseSet->jobType(); + QString filePath = jobResponseSet->filePath(); + QString url; + bool generateThumbnail = false; // thumbnail will be generated when needed, don't slow down import with that + bool storeFile = jobResponseSet->copyFile(); + + QMap datasets = jobResponseSet->datasets(); + for(QString key : datasets.keys()) + { + ctkDICOMItem* dataset = datasets.value(key); + if (!dataset) + { + continue; + } + QString patientID, patientName, studyInstanceUID, seriesInstanceUID, sopInstanceUID; + patientName = dataset->GetElementAsString(DCM_PatientName); + patientID = dataset->GetElementAsString(DCM_PatientID); + studyInstanceUID = dataset->GetElementAsString(DCM_StudyInstanceUID); + seriesInstanceUID = dataset->GetElementAsString(DCM_SeriesInstanceUID); + sopInstanceUID = dataset->GetElementAsString(DCM_SOPInstanceUID); + + if (patientID.isEmpty()) + { + if (jobType == ctkDICOMJobResponseSet::JobType::QueryPatients) + { + patientID = key; + } + else if (jobType == ctkDICOMJobResponseSet::JobType::QueryStudies || + jobType == ctkDICOMJobResponseSet::JobType::QuerySeries || + jobType == ctkDICOMJobResponseSet::JobType::QueryInstances || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveStudy || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance) + { + patientID = jobResponseSet->patientID(); + } + + dataset->SetElementAsString(DCM_PatientID, patientID); + } + + if (studyInstanceUID.isEmpty()) + { + if (jobType == ctkDICOMJobResponseSet::JobType::QueryStudies) + { + studyInstanceUID = key; + } + else if (jobType == ctkDICOMJobResponseSet::JobType::QuerySeries || + jobType == ctkDICOMJobResponseSet::JobType::QueryInstances || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveStudy || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance) + { + studyInstanceUID = jobResponseSet->studyInstanceUID(); + } + + dataset->SetElementAsString(DCM_StudyInstanceUID, studyInstanceUID); + } + + if (patientName.isEmpty() && !studyInstanceUID.isEmpty()) + { + QString patientUID = this->patientForStudy(studyInstanceUID); + patientName = this->nameForPatient(patientUID); + dataset->SetElementAsString(DCM_PatientName, patientName); + } + + if (seriesInstanceUID.isEmpty()) + { + if (jobType == ctkDICOMJobResponseSet::JobType::QuerySeries) + { + seriesInstanceUID = key; + } + else if (jobType == ctkDICOMJobResponseSet::JobType::QueryInstances || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance) + { + seriesInstanceUID = jobResponseSet->seriesInstanceUID(); + } + + dataset->SetElementAsString(DCM_SeriesInstanceUID, seriesInstanceUID); + } + + if (sopInstanceUID.isEmpty()) + { + if (jobType == ctkDICOMJobResponseSet::JobType::QueryInstances) + { + sopInstanceUID = key; + } + else if (jobType == ctkDICOMJobResponseSet::JobType::RetrieveStudy || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance || + jobType == ctkDICOMJobResponseSet::JobType::StoreSOPInstance) + { + sopInstanceUID = jobResponseSet->sopInstanceUID(); + } + + dataset->SetElementAsString(DCM_SOPInstanceUID, sopInstanceUID); + } + + if (patientID.isEmpty()) + { + logger.error("ctkDICOMDatabase::insert: dataset has no patientID"); + continue; + } + + if (patientName.isEmpty()) + { + logger.error("ctkDICOMDatabase::insert: dataset has no patientName"); + continue; + } + + if (studyInstanceUID.isEmpty() && jobType != ctkDICOMJobResponseSet::JobType::QueryPatients) + { + logger.error("ctkDICOMDatabase::insert: dataset has no studyInstanceUID"); + continue; + } + + if (jobType == ctkDICOMJobResponseSet::JobType::QueryInstances) + { + url = "dimse+ctk://" + jobResponseSet->connectionName(); + if (!studyInstanceUID.isEmpty()) + { + url += "/" + studyInstanceUID; + } + if (!seriesInstanceUID.isEmpty()) + { + url += "/" + seriesInstanceUID; + } + if (!sopInstanceUID.isEmpty()) + { + url += "/" + sopInstanceUID; + } + } + + // Check to see if the file has already been loaded + bool datasetInDatabase = false; + bool datasetUpToDate = false; + if (jobResponseSet->overwriteExistingDataset()) + { + // overwrite was requested based on exact file match + datasetInDatabase = true; + datasetUpToDate = false; + } + else + { + // there is no exact file match, but there may be still a different file in the database + // for the same SOP instance UID + QString databaseFilename; + if (!d->indexingStatusForFile(filePath, sopInstanceUID, datasetInDatabase, datasetUpToDate, databaseFilename)) + { + // error occurred, message is already logged + continue; + } + } + + if (datasetInDatabase) + { + if (datasetUpToDate) + { + continue; + } + // File is updated, delete record and re-index + if (!d->removeImage(sopInstanceUID)) + { + logger.error("Failed to insert file into database (cannot update pre-existing item): " + filePath); + continue; + } + } + + // Store a copy of the dataset + QString storedFilePath = filePath; + if (storeFile && !seriesInstanceUID.isEmpty() && !this->isInMemory()) + { + if (!d->storeDatasetFile(*dataset, filePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID, storedFilePath)) + { + continue; + } + } + + if (d->insertPatientStudySeries(*dataset, patientID, patientName)) + { + databaseWasChanged = true; + } + + if (!sopInstanceUID.isEmpty() && + !seriesInstanceUID.isEmpty() && + (!storedFilePath.isEmpty() || + !url.isEmpty())) + { + logger.debug( "Maybe add Instance" ); + bool alreadyInserted = false; + if (!storeFile) + { + // file is linked, maybe it is already inserted + QSqlQuery checkImageExistsQuery(d->Database); + checkImageExistsQuery.prepare("SELECT * FROM Images WHERE SOPInstanceUID = ?"); + checkImageExistsQuery.addBindValue(sopInstanceUID); + checkImageExistsQuery.exec(); + alreadyInserted = checkImageExistsQuery.next(); + } + if (!alreadyInserted) + { + // Get filename that will be stored in the database. + // Use relative path if a copy is stored in the database to make the database relocatable. + QString storedFilePathInDatabase; + if (storeFile) + { + QDir databaseDirectory(this->databaseDirectory()); + storedFilePathInDatabase = databaseDirectory.relativeFilePath(storedFilePath); + } + else + { + storedFilePathInDatabase = storedFilePath; + } + + QSqlQuery insertImageStatement(d->Database); + insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ?, ? )"); + insertImageStatement.addBindValue(sopInstanceUID); + insertImageStatement.addBindValue(storedFilePathInDatabase); + insertImageStatement.addBindValue(url); + insertImageStatement.addBindValue(seriesInstanceUID); + insertImageStatement.addBindValue(QDateTime::currentDateTime()); + + if ( !insertImageStatement.exec() ) + { + logger.error( "Error executing statement: " + + insertImageStatement.lastQuery() + + " Error: " + insertImageStatement.lastError().text() ); + } + else + { + // insert was needed, so cache any application-requested tags + d->precacheTags(*dataset, sopInstanceUID); + } + + // let users of this class track when things happen + emit instanceAdded(sopInstanceUID); + logger.debug( "Instance Added" ); + databaseWasChanged = true; + } + if (generateThumbnail) + { + d->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); + } + } + } + } + + d->Database.commit(); + d->TagCacheDatabase.commit(); + + if (databaseWasChanged && this->isInMemory()) + { + emit this->databaseChanged(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::setTagsToPrecache(const QStringList tags) +{ + Q_D(ctkDICOMDatabase); + if (d->TagsToPrecache == tags) { return; } @@ -2659,7 +2903,7 @@ bool ctkDICOMDatabase::isInMemory() const } //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clearCachedTags/*=true*/) +bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clearCachedTags/*=false*/, bool cleanup/*=true*/) { Q_D(ctkDICOMDatabase); @@ -2722,22 +2966,23 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clear if (QFileInfo(dbFilePath).isRelative()) { QString absPath = d->absolutePathFromInternal(dbFilePath); - if (QFile(absPath).remove()) + QFile file(absPath); + if (file.exists()) { - if (d->LoggedExecVerbose) + if (file.remove()) { logger.debug("Removed file " + absPath); + QString fileFolder = QFileInfo(absPath).absoluteDir().path(); + if (foldersToRemove.isEmpty() || foldersToRemove.last() != fileFolder) + { + foldersToRemove << fileFolder; + } } - QString fileFolder = QFileInfo(absPath).absoluteDir().path(); - if (foldersToRemove.isEmpty() || foldersToRemove.last() != fileFolder) + else { - foldersToRemove << fileFolder; + logger.warn("Failed to remove file " + absPath); } } - else - { - logger.warn("Failed to remove file " + absPath); - } } // Remove thumbnail (if exists) QFile thumbnailFile(d->absolutePathFromInternal(thumbnailPath)); @@ -2762,7 +3007,10 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clear QDir().rmpath(folderToRemove); } - this->cleanup(); + if (cleanup) + { + this->cleanup(); + } d->resetLastInsertedValues(); @@ -2790,7 +3038,7 @@ bool ctkDICOMDatabase::cleanup(bool vacuum/*=false*/) } //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::removeStudy(const QString& studyInstanceUID) +bool ctkDICOMDatabase::removeStudy(const QString& studyInstanceUID, bool cleanup/*=true*/) { Q_D(ctkDICOMDatabase); @@ -2807,7 +3055,7 @@ bool ctkDICOMDatabase::removeStudy(const QString& studyInstanceUID) while ( seriesForStudy.next() ) { QString seriesInstanceUID = seriesForStudy.value(seriesForStudy.record().indexOf("SeriesInstanceUID")).toString(); - if ( ! this->removeSeries(seriesInstanceUID) ) + if ( ! this->removeSeries(seriesInstanceUID, false, cleanup) ) { result = false; } @@ -2823,7 +3071,7 @@ bool ctkDICOMDatabase::removeStudy(const QString& studyInstanceUID) } //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::removePatient(const QString& patientID) +bool ctkDICOMDatabase::removePatient(const QString& patientID, bool cleanup/*=true*/) { Q_D(ctkDICOMDatabase); @@ -2840,7 +3088,7 @@ bool ctkDICOMDatabase::removePatient(const QString& patientID) while ( studiesForPatient.next() ) { QString studyInstanceUID = studiesForPatient.value(studiesForPatient.record().indexOf("StudyInstanceUID")).toString(); - if ( ! this->removeStudy(studyInstanceUID) ) + if ( ! this->removeStudy(studyInstanceUID, cleanup) ) { result = false; } @@ -3356,3 +3604,31 @@ QString ctkDICOMDatabase::compositePatientID(const QString& patientID, const QSt { return QString("%1~%2~%3").arg(patientID).arg(patientsBirthDate).arg(patientsName); } + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::setLoadedSeries(const QStringList &seriesList) +{ + Q_D(ctkDICOMDatabase); + d->LoadedSeries = seriesList; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::loadedSeries() const +{ + Q_D(const ctkDICOMDatabase); + return d->LoadedSeries; +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::setVisibleSeries(const QStringList &seriesList) +{ + Q_D(ctkDICOMDatabase); + d->VisibleSeries = seriesList; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::visibleSeries() const +{ + Q_D(const ctkDICOMDatabase); + return d->VisibleSeries; +} diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.h b/Libs/DICOM/Core/ctkDICOMDatabase.h index 6f6a30d41d..03bbeca5be 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase.h @@ -34,6 +34,7 @@ class ctkDICOMDatabasePrivate; class DcmDataset; class ctkDICOMAbstractThumbnailGenerator; class ctkDICOMDisplayedFieldGenerator; +class ctkDICOMJobResponseSet; /// \ingroup DICOM_Core /// @@ -52,7 +53,6 @@ class ctkDICOMDisplayedFieldGenerator; /// parallel to "dicom" directory called "thumbs". class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject { - Q_OBJECT Q_PROPERTY(bool isOpen READ isOpen) Q_PROPERTY(bool isInMemory READ isInMemory) @@ -64,6 +64,8 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject Q_PROPERTY(QStringList patientFieldNames READ patientFieldNames) Q_PROPERTY(QStringList studyFieldNames READ studyFieldNames) Q_PROPERTY(QStringList seriesFieldNames READ seriesFieldNames) + Q_PROPERTY(QStringList loadedSeries READ loadedSeries WRITE setLoadedSeries) + Q_PROPERTY(QStringList visibleSeries READ visibleSeries WRITE setVisibleSeries) Q_PROPERTY(bool useShortStoragePath READ useShortStoragePath WRITE setUseShortStoragePath) public: @@ -118,7 +120,7 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// must be avoided as it breaks previously created database object /// that used the same connection name). /// @param update the schema if it is found to be out of date - Q_INVOKABLE virtual void openDatabase(const QString databaseFile, + Q_INVOKABLE virtual bool openDatabase(const QString& databaseFile, const QString& connectionName = ""); /// Close the database. It must not be used afterwards. @@ -259,11 +261,8 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject bool storeFile = true, bool generateThumbnail = true, bool createHierarchy = true, const QString& destinationDirectoryName = QString() ); - - Q_INVOKABLE void insert(const QString& filePath, const ctkDICOMItem& ctkDataset, - bool storeFile = true, bool generateThumbnail = true); - Q_INVOKABLE void insert(const QList& indexingResults); + Q_INVOKABLE void insert(QList> jobResponseSets); /// When a DICOM file is stored in the database (insert is called with storeFile=true) then /// path is constructed from study, series, and SOP instance UID. @@ -309,9 +308,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// if set to False the they are left in the database unchanged. /// By default clearCachedTags is disabled because it significantly increases deletion time /// on large databases. - Q_INVOKABLE bool removeSeries(const QString& seriesInstanceUID, bool clearCachedTags=false); - Q_INVOKABLE bool removeStudy(const QString& studyInstanceUID); - Q_INVOKABLE bool removePatient(const QString& patientID); + Q_INVOKABLE bool removeSeries(const QString& seriesInstanceUID, bool clearCachedTags=false, bool cleanup=true); + Q_INVOKABLE bool removeStudy(const QString& studyInstanceUID, bool cleanup=true); + Q_INVOKABLE bool removePatient(const QString& patientID, bool cleanup=true); /// Remove all patients, studies, series, which do not have associated images. /// If vacuum is set to true then the whole database content is attempted to /// cleaned from remnants of all previously deleted data from the file. @@ -404,6 +403,14 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// inserted under the same patient. Q_INVOKABLE static QString compositePatientID(const QString& patientID, const QString& patientsName, const QString& patientsBirthDate); + /// Set a list of loaded series + void setLoadedSeries(const QStringList& seriesList); + QStringList loadedSeries() const; + + /// Set a list of visible series + void setVisibleSeries(const QStringList& seriesList); + QStringList visibleSeries() const; + Q_SIGNALS: /// Things inserted to database. diff --git a/Libs/DICOM/Core/ctkDICOMDatabase_p.h b/Libs/DICOM/Core/ctkDICOMDatabase_p.h index e76b94480a..f9595ff0fd 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase_p.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase_p.h @@ -53,7 +53,6 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabasePrivate bool loggedExec(QSqlQuery& query); bool loggedExec(QSqlQuery& query, const QString& queryString); bool loggedExecBatch(QSqlQuery& query); - bool LoggedExecVerbose; bool removeImage(const QString& sopInstanceUID); @@ -172,6 +171,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabasePrivate /// Facilitate using custom schema with the database without subclassing QString SchemaVersion; + + QStringList LoadedSeries; + QStringList VisibleSeries; }; #endif diff --git a/Libs/DICOM/Core/ctkDICOMEcho.cpp b/Libs/DICOM/Core/ctkDICOMEcho.cpp new file mode 100644 index 0000000000..ad635ae387 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMEcho.cpp @@ -0,0 +1,215 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMEcho.h" +#include "ctkLogger.h" + +// DCMTK includes +#include +#include +#include /* for class OFStandard */ +#include + +static ctkLogger logger("org.commontk.dicom.DICOMEcho"); + +//------------------------------------------------------------------------------ +class ctkDICOMEchoPrivate +{ +public: + ctkDICOMEchoPrivate(); + ~ctkDICOMEchoPrivate() = default; + + QString ConnectionName; + QString CallingAETitle; + QString CalledAETitle; + QString Host; + int Port; + DcmSCU SCU; +}; + +//------------------------------------------------------------------------------ +// ctkDICOMEchoPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMEchoPrivate::ctkDICOMEchoPrivate() +{ + this->ConnectionName = ""; + this->CallingAETitle = ""; + this->CalledAETitle = ""; + this->Host = ""; + this->Port = 80; + + this->SCU.setACSETimeout(3); + this->SCU.setConnectionTimeout(3); +} + +//------------------------------------------------------------------------------ +// ctkDICOMEcho methods + +//------------------------------------------------------------------------------ +ctkDICOMEcho::ctkDICOMEcho(QObject* parentObject) + : QObject(parentObject) + , d_ptr(new ctkDICOMEchoPrivate) +{ + Q_D(ctkDICOMEcho); + + d->SCU.setVerbosePCMode(false); +} + +//------------------------------------------------------------------------------ +ctkDICOMEcho::~ctkDICOMEcho() = default; + +//------------------------------------------------------------------------------ +void ctkDICOMEcho::setConnectionName(const QString& connectionName) +{ + Q_D(ctkDICOMEcho); + d->ConnectionName = connectionName; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMEcho::connectionName() const +{ + Q_D(const ctkDICOMEcho); + return d->ConnectionName; +} + +//------------------------------------------------------------------------------ +void ctkDICOMEcho::setCallingAETitle(const QString& callingAETitle) +{ + Q_D(ctkDICOMEcho); + d->CallingAETitle = callingAETitle; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMEcho::callingAETitle() const +{ + Q_D(const ctkDICOMEcho); + return d->CallingAETitle; +} + +//------------------------------------------------------------------------------ +void ctkDICOMEcho::setCalledAETitle(const QString& calledAETitle) +{ + Q_D(ctkDICOMEcho); + d->CalledAETitle = calledAETitle; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMEcho::calledAETitle() const +{ + Q_D(const ctkDICOMEcho); + return d->CalledAETitle; +} + +//------------------------------------------------------------------------------ +void ctkDICOMEcho::setHost(const QString& host) +{ + Q_D(ctkDICOMEcho); + d->Host = host; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMEcho::host() const +{ + Q_D(const ctkDICOMEcho); + return d->Host; +} + +//------------------------------------------------------------------------------ +void ctkDICOMEcho::setPort(int port) +{ + Q_D(ctkDICOMEcho); + d->Port = port; +} + +//------------------------------------------------------------------------------ +int ctkDICOMEcho::port() const +{ + Q_D(const ctkDICOMEcho); + return d->Port; +} + +//----------------------------------------------------------------------------- +void ctkDICOMEcho::setConnectionTimeout(int timeout) +{ + Q_D(ctkDICOMEcho); + d->SCU.setACSETimeout(timeout); + d->SCU.setConnectionTimeout(timeout); +} + +//----------------------------------------------------------------------------- +int ctkDICOMEcho::connectionTimeout() const +{ + Q_D(const ctkDICOMEcho); + return d->SCU.getConnectionTimeout(); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMEcho::echo() +{ + Q_D(ctkDICOMEcho); + + d->SCU.setPeerAETitle(OFString(this->calledAETitle().toStdString().c_str())); + d->SCU.setPeerHostName(OFString(this->host().toStdString().c_str())); + d->SCU.setPeerPort(this->port()); + + logger.debug("Setting Transfer Syntaxes"); + + OFList transferSyntaxes; + transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + transferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); + transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); + + d->SCU.addPresentationContext(UID_VerificationSOPClass, transferSyntaxes); + if (!d->SCU.initNetwork().good()) + { + logger.error("Error initializing the network"); + return false; + } + logger.debug("Negotiating Association"); + + OFCondition result = d->SCU.negotiateAssociation(); + if (result.bad()) + { + logger.error("Error negotiating the association: " + QString(result.text())); + return false; + } + + logger.debug("Seding Echo"); + // Issue ECHO request and let scu find presentation context itself (0) + OFCondition status = d->SCU.sendECHORequest(0); + if (!status.good()) + { + logger.error("Echo failed"); + d->SCU.releaseAssociation(); + return false; + } + + d->SCU.releaseAssociation(); + + return true; +} diff --git a/Libs/DICOM/Core/ctkDICOMEcho.h b/Libs/DICOM/Core/ctkDICOMEcho.h new file mode 100644 index 0000000000..296a098b02 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMEcho.h @@ -0,0 +1,100 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +=========================================================================*/ + +#ifndef __ctkDICOMEcho_h +#define __ctkDICOMEcho_h + +// Qt includes +#include +#include +#include + +// CTK includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" + +class ctkDICOMEchoPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMEcho : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString connectionName READ connectionName WRITE setConnectionName); + Q_PROPERTY(QString callingAETitle READ callingAETitle WRITE setCallingAETitle); + Q_PROPERTY(QString calledAETitle READ calledAETitle WRITE setCalledAETitle); + Q_PROPERTY(QString host READ host WRITE setHost); + Q_PROPERTY(int port READ port WRITE setPort); + Q_PROPERTY(int connectionTimeout READ connectionTimeout WRITE setConnectionTimeout); + +public: + explicit ctkDICOMEcho(QObject* parent = 0); + virtual ~ctkDICOMEcho(); + + ///@{ + /// Name identifying the server + void setConnectionName(const QString& connectionName); + QString connectionName() const; + ///@} + + ///@{ + /// Set methods for connectivity. + /// Empty by default + void setCallingAETitle(const QString& callingAETitle); + QString callingAETitle() const; + + void setCalledAETitle(const QString& calledAETitle); + QString calledAETitle() const; + ///@} + + ///@{ + /// Peer hostname being connected to + /// Empty by default + void setHost(const QString& host); + QString host() const; + ///@} + + ///@{ + /// Specify a port for the packet headers. + /// \a port ranges from 0 to 65535. + /// 80 by default. + void setPort(int port); + int port() const; + ///@} + + ///@{ + /// Connection timeout, default 3 sec. + void setConnectionTimeout(int timeout); + int connectionTimeout() const; + ///@} + + /// Echo connection. + bool echo(); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMEcho); + Q_DISABLE_COPY(ctkDICOMEcho); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMIndexer.cpp b/Libs/DICOM/Core/ctkDICOMIndexer.cpp index 7304150b32..36419794d7 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer.cpp +++ b/Libs/DICOM/Core/ctkDICOMIndexer.cpp @@ -138,8 +138,8 @@ void ctkDICOMIndexerPrivateWorker::start() imagesCountAfter = database.imagesCount(); double elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0; - qDebug() << QString("DICOM indexer has updated display fields for %1 files [%2s]") - .arg(imagesCountAfter-imagesCountBefore).arg(QString::number(elapsedTimeInSeconds, 'f', 2)); + logger.debug(QString("DICOM indexer has updated display fields for %1 files [%2s]") + .arg(imagesCountAfter-imagesCountBefore).arg(QString::number(elapsedTimeInSeconds, 'f', 2))); // restart if new requests has been queued during displayed fields update } while (!this->RequestQueue->isEmpty()); @@ -250,8 +250,8 @@ void ctkDICOMIndexerPrivateWorker::processIndexingRequest(DICOMIndexingQueue::In } float elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0; - qDebug() << QString("DICOM indexer has successfully processed %1 files [%2s]") - .arg(currentFileIndex).arg(QString::number(elapsedTimeInSeconds, 'f', 2)); + logger.debug(QString("DICOM indexer has successfully processed %1 files [%2s]") + .arg(currentFileIndex).arg(QString::number(elapsedTimeInSeconds, 'f', 2))); } @@ -279,8 +279,8 @@ void ctkDICOMIndexerPrivateWorker::writeIndexingResultsToDatabase(ctkDICOMDataba this->NumberOfInstancesInserted = 0; float elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0; - qDebug() << QString("DICOM indexer has successfully inserted %1 files [%2s]") - .arg(indexingResults.count()).arg(QString::number(elapsedTimeInSeconds, 'f', 2)); + logger.debug(QString("DICOM indexer has successfully inserted %1 files [%2s]") + .arg(indexingResults.count()).arg(QString::number(elapsedTimeInSeconds, 'f', 2))); } @@ -634,10 +634,9 @@ bool ctkDICOMIndexer::addDicomdir(const QString& directoryName, bool copyFile/*= } } float elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0; - qDebug() - << QString("DICOM indexer has successfully processed DICOMDIR in %1 [%2s]") - .arg(directoryName) - .arg(QString::number(elapsedTimeInSeconds,'f', 2)); + logger.debug(QString("DICOM indexer has successfully processed DICOMDIR in %1 [%2s]") + .arg(directoryName) + .arg(QString::number(elapsedTimeInSeconds,'f', 2))); this->addListOfFiles(listOfInstances, copyFile); } return success; diff --git a/Libs/DICOM/Core/ctkDICOMIndexer_p.h b/Libs/DICOM/Core/ctkDICOMIndexer_p.h index bec7104153..3357a046e6 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer_p.h +++ b/Libs/DICOM/Core/ctkDICOMIndexer_p.h @@ -259,7 +259,6 @@ class ctkDICOMIndexerPrivate : public QObject Q_SIGNALS: void startWorker(); -//public Q_SLOTS: public: DICOMIndexingQueue RequestQueue; diff --git a/Libs/DICOM/Core/ctkDICOMInserter.cpp b/Libs/DICOM/Core/ctkDICOMInserter.cpp new file mode 100644 index 0000000000..ceaf811b27 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMInserter.cpp @@ -0,0 +1,160 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include + +// ctkDICOMCore includes +#include "ctkLogger.h" +#include "ctkDICOMDatabase.h" +#include "ctkDICOMInserter.h" +#include "ctkDICOMJobResponseSet.h" + +static ctkLogger logger("org.commontk.dicom.DICOMQuery"); + +//------------------------------------------------------------------------------ +class ctkDICOMInserterPrivate +{ +public: + ctkDICOMInserterPrivate(); + ~ctkDICOMInserterPrivate() = default; + + bool Canceled; + QString DatabaseFilename; + QStringList TagsToPrecache; + QStringList TagsToExcludeFromStorage; +}; + +//------------------------------------------------------------------------------ +// ctkDICOMInserterPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMInserterPrivate::ctkDICOMInserterPrivate() +{ + this->Canceled = false; +} + +//------------------------------------------------------------------------------ +// ctkDICOMInserter methods + +//------------------------------------------------------------------------------ +ctkDICOMInserter::ctkDICOMInserter(QObject* parentObject) + : QObject(parentObject) + , d_ptr(new ctkDICOMInserterPrivate) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMInserter::~ctkDICOMInserter() = default; + +//------------------------------------------------------------------------------ +void ctkDICOMInserter::setDatabaseFilename(const QString& databaseFilename) +{ + Q_D(ctkDICOMInserter); + d->DatabaseFilename = databaseFilename; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMInserter::databaseFilename() const +{ + Q_D(const ctkDICOMInserter); + return d->DatabaseFilename; +} + +//------------------------------------------------------------------------------ +void ctkDICOMInserter::setTagsToPrecache(const QStringList& tagsToPrecache) +{ + Q_D(ctkDICOMInserter); + d->TagsToPrecache = tagsToPrecache; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMInserter::tagsToPrecache() const +{ + Q_D(const ctkDICOMInserter); + return d->TagsToPrecache; +} + +//------------------------------------------------------------------------------ +void ctkDICOMInserter::setTagsToExcludeFromStorage(const QStringList& tagsToExcludeFromStorage) +{ + Q_D(ctkDICOMInserter); + d->TagsToExcludeFromStorage = tagsToExcludeFromStorage; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMInserter::tagsToExcludeFromStorage() const +{ + Q_D(const ctkDICOMInserter); + return d->TagsToExcludeFromStorage; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMInserter::wasCanceled() +{ + Q_D(const ctkDICOMInserter); + return d->Canceled; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMInserter::addJobResponseSets(QList> jobResponseSets) +{ + Q_D(const ctkDICOMInserter); + if (d->Canceled) + { + return false; + } + + emit updatingDatabase(true); + + ctkDICOMDatabase database; + QString dbConnectionName = + "db_" + QString::number(reinterpret_cast(QThread::currentThreadId()), 16); + + database.openDatabase(d->DatabaseFilename, dbConnectionName); + database.setTagsToPrecache(d->TagsToPrecache); + database.setTagsToExcludeFromStorage(d->TagsToExcludeFromStorage); + + // To Do: We should ensure that only one write operation occurs at a time. + // In the ctkDICOMScheduler, we ensure this by utilizing a job queue, preventing multiple inserter jobs from running concurrently. + // Similarly, it would be necessary to implement a check in the insert method of ctkDICOMDatabase + // to determine if any other process is currently writing (for example, a UI element writing the patient's name into the database). + // Therefore, we propose the inclusion of a static variable in ctkDICOMDatabase that indicates ongoing write operations + // for each DatabaseFilename, except in cases where it is an in-memory database. + database.insert(jobResponseSets); + database.updateDisplayedFields(); + + database.closeDatabase(); + + emit updatingDatabase(false); + emit done(); + + return true; +} + +//---------------------------------------------------------------------------- +void ctkDICOMInserter::cancel() +{ + Q_D(ctkDICOMInserter); + d->Canceled = true; +} diff --git a/Libs/DICOM/Core/ctkDICOMInserter.h b/Libs/DICOM/Core/ctkDICOMInserter.h new file mode 100644 index 0000000000..1e81551af6 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMInserter.h @@ -0,0 +1,87 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMInserter_h +#define __ctkDICOMInserter_h + +// Qt includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" + +class ctkDICOMInserterPrivate; +class ctkDICOMJobResponseSet; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMInserter : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString databaseFilename READ databaseFilename WRITE setDatabaseFilename); + Q_PROPERTY(QStringList tagsToPrecache READ tagsToPrecache WRITE setTagsToPrecache); + Q_PROPERTY(QStringList tagsToExcludeFromStorage READ tagsToExcludeFromStorage WRITE setTagsToExcludeFromStorage); + +public: + explicit ctkDICOMInserter(QObject* parent = 0); + virtual ~ctkDICOMInserter(); + + ///@{ + /// Database Filename + void setDatabaseFilename(const QString& databaseFilename); + QString databaseFilename() const; + ///@} + + ///@{ + /// Database TagsToPrecache + void setTagsToPrecache(const QStringList& tagsToPrecache); + QStringList tagsToPrecache() const; + ///@} + + ///@{ + /// Database TagsToPrecache + void setTagsToExcludeFromStorage(const QStringList& tagsToExcludeFromStorage); + QStringList tagsToExcludeFromStorage() const; + ///@} + + /// operation is canceled? + Q_INVOKABLE bool wasCanceled(); + + /// add JobResponseSets from queries and retrieves + Q_INVOKABLE bool addJobResponseSets(QList> jobResponseSets); + +Q_SIGNALS: + void updatingDatabase(bool); + void done(); + +public Q_SLOTS: + void cancel(); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMInserter); + Q_DISABLE_COPY(ctkDICOMInserter); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMInserterJob.cpp b/Libs/DICOM/Core/ctkDICOMInserterJob.cpp new file mode 100644 index 0000000000..0e5c9d638b --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMInserterJob.cpp @@ -0,0 +1,118 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkDICOMCore includes +#include "ctkDICOMInserterJob.h" +#include "ctkDICOMInserterWorker.h" +#include "ctkLogger.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMInserterJob"); + +//------------------------------------------------------------------------------ +// ctkDICOMInserterJob methods + +//------------------------------------------------------------------------------ +ctkDICOMInserterJob::ctkDICOMInserterJob() +{ + this->DatabaseFilename = ""; + this->MaximumConcurrentJobsPerType = 1; +} + +//------------------------------------------------------------------------------ +ctkDICOMInserterJob::~ctkDICOMInserterJob() = default; + +//------------------------------------------------------------------------------ +QString ctkDICOMInserterJob::loggerReport(const QString& status) const +{ + return QString("ctkDICOMInserterJob: insert job %1.\n" + "Number of jobResponseSet to process: %2\n") + .arg(status) + .arg(this->JobResponseSets.count()); +} + +//------------------------------------------------------------------------------ +void ctkDICOMInserterJob::setDatabaseFilename(const QString& databaseFilename) +{ + this->DatabaseFilename = databaseFilename; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMInserterJob::databaseFilename() const +{ + return this->DatabaseFilename; +} + +//------------------------------------------------------------------------------ +void ctkDICOMInserterJob::setTagsToPrecache(const QStringList& tagsToPrecache) +{ + this->TagsToPrecache = tagsToPrecache; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMInserterJob::tagsToPrecache() const +{ + return this->TagsToPrecache; +} + +//------------------------------------------------------------------------------ +void ctkDICOMInserterJob::setTagsToExcludeFromStorage(const QStringList& tagsToExcludeFromStorage) +{ + this->TagsToExcludeFromStorage = tagsToExcludeFromStorage; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMInserterJob::tagsToExcludeFromStorage() const +{ + return this->TagsToExcludeFromStorage; +} + +//------------------------------------------------------------------------------ +ctkAbstractJob* ctkDICOMInserterJob::clone() const +{ + ctkDICOMInserterJob* newInserterJob = new ctkDICOMInserterJob; + newInserterJob->setDICOMLevel(this->dicomLevel()); + newInserterJob->setPatientID(this->patientID()); + newInserterJob->setStudyInstanceUID(this->studyInstanceUID()); + newInserterJob->setSeriesInstanceUID(this->seriesInstanceUID()); + newInserterJob->setSOPInstanceUID(this->sopInstanceUID()); + newInserterJob->setMaximumNumberOfRetry(this->maximumNumberOfRetry()); + newInserterJob->setRetryDelay(this->retryDelay()); + newInserterJob->setRetryCounter(this->retryCounter()); + newInserterJob->setIsPersistent(this->isPersistent()); + newInserterJob->setMaximumConcurrentJobsPerType(this->maximumConcurrentJobsPerType()); + newInserterJob->setPriority(this->priority()); + newInserterJob->setDatabaseFilename(this->databaseFilename()); + newInserterJob->setTagsToPrecache(this->tagsToPrecache()); + newInserterJob->setTagsToExcludeFromStorage(this->tagsToExcludeFromStorage()); + + return newInserterJob; +} + +//------------------------------------------------------------------------------ +ctkAbstractWorker* ctkDICOMInserterJob::createWorker() +{ + ctkDICOMInserterWorker* worker = + new ctkDICOMInserterWorker; + worker->setJob(*this); + return worker; +} diff --git a/Libs/DICOM/Core/ctkDICOMInserterJob.h b/Libs/DICOM/Core/ctkDICOMInserterJob.h new file mode 100644 index 0000000000..7f8981cabe --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMInserterJob.h @@ -0,0 +1,88 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMInserterJob_h +#define __ctkDICOMInserterJob_h + +// Qt includes +#include +#include + +// ctkCore includes +class ctkAbstractWorker; + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMJob.h" +class ctkDICOMServer; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMInserterJob : public ctkDICOMJob +{ + Q_OBJECT + Q_PROPERTY(QString databaseFilename READ databaseFilename WRITE setDatabaseFilename); + Q_PROPERTY(QStringList tagsToPrecache READ tagsToPrecache WRITE setTagsToPrecache); + Q_PROPERTY(QStringList tagsToExcludeFromStorage READ tagsToExcludeFromStorage WRITE setTagsToExcludeFromStorage); + +public: + typedef ctkDICOMJob Superclass; + explicit ctkDICOMInserterJob(); + virtual ~ctkDICOMInserterJob(); + + /// Logger report string formatting for specific task + Q_INVOKABLE QString loggerReport(const QString& status) const override; + + ///@{ + /// Database Filename + void setDatabaseFilename(const QString& databaseFilename); + QString databaseFilename() const; + ///}@ + + ///@{ + /// Database TagsToPrecache + void setTagsToPrecache(const QStringList& tagsToPrecache); + QStringList tagsToPrecache() const; + ///}@ + + ///@{ + /// Database TagsToPrecache + void setTagsToExcludeFromStorage(const QStringList& tagsToExcludeFromStorage); + QStringList tagsToExcludeFromStorage() const; + ///}@ + + /// \see ctkAbstractJob::clone() + Q_INVOKABLE ctkAbstractJob* clone() const override; + + /// Generate worker for job + Q_INVOKABLE ctkAbstractWorker* createWorker() override; + +protected: + QString DatabaseFilename; + QStringList TagsToPrecache; + QStringList TagsToExcludeFromStorage; + +private: + Q_DISABLE_COPY(ctkDICOMInserterJob); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMInserterWorker.cpp b/Libs/DICOM/Core/ctkDICOMInserterWorker.cpp new file mode 100644 index 0000000000..5d1fdf7045 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMInserterWorker.cpp @@ -0,0 +1,166 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMInserterJob.h" +#include "ctkDICOMInserterWorker_p.h" +#include "ctkDICOMJobResponseSet.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMInserterWorker"); + +//------------------------------------------------------------------------------ +// ctkDICOMInserterWorkerPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMInserterWorkerPrivate::ctkDICOMInserterWorkerPrivate(ctkDICOMInserterWorker* object) + : q_ptr(object) +{ + this->Inserter = QSharedPointer(new ctkDICOMInserter); +} + +//------------------------------------------------------------------------------ +ctkDICOMInserterWorkerPrivate::~ctkDICOMInserterWorkerPrivate() = default; + +//------------------------------------------------------------------------------ +void ctkDICOMInserterWorkerPrivate::setInserterParameters() +{ + Q_Q(ctkDICOMInserterWorker); + + QSharedPointer inserterJob = + qSharedPointerObjectCast(q->Job); + if (!inserterJob) + { + return; + } + + this->Inserter->setDatabaseFilename(inserterJob->databaseFilename()); + this->Inserter->setTagsToPrecache(inserterJob->tagsToPrecache()); + this->Inserter->setTagsToExcludeFromStorage(inserterJob->tagsToExcludeFromStorage()); +} + +//------------------------------------------------------------------------------ +// ctkDICOMInserterWorker methods + +//------------------------------------------------------------------------------ +ctkDICOMInserterWorker::ctkDICOMInserterWorker() + : d_ptr(new ctkDICOMInserterWorkerPrivate(this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMInserterWorker::ctkDICOMInserterWorker(ctkDICOMInserterWorkerPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMInserterWorker::~ctkDICOMInserterWorker() = default; + +//---------------------------------------------------------------------------- +void ctkDICOMInserterWorker::cancel() +{ + Q_D(const ctkDICOMInserterWorker); + d->Inserter->cancel(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMInserterWorker::run() +{ + Q_D(const ctkDICOMInserterWorker); + QSharedPointer inserterJob = + qSharedPointerObjectCast(this->Job); + if (!inserterJob) + { + return; + } + + if (inserterJob->status() == ctkAbstractJob::JobStatus::Stopped) + { + emit inserterJob->canceled(); + this->onJobCanceled(); + inserterJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + + inserterJob->setStatus(ctkAbstractJob::JobStatus::Running); + emit inserterJob->started(); + + logger.debug(QString("ctkDICOMInserterWorker : running job %1 in thread %2.\n") + .arg(inserterJob->jobUID()) + .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); + + QList> jobResponseSets = inserterJob->jobResponseSetsShared(); + d->Inserter->addJobResponseSets(jobResponseSets); + + if (inserterJob->status() == ctkAbstractJob::JobStatus::Stopped) + { + emit inserterJob->canceled(); + this->onJobCanceled(); + inserterJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + + foreach (QSharedPointer jobResponseSet, jobResponseSets) + { + emit inserterJob->progressJobDetail(jobResponseSet->toVariant()); + } + + inserterJob->setStatus(ctkAbstractJob::JobStatus::Finished); + emit inserterJob->finished(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMInserterWorker::setJob(QSharedPointer job) +{ + Q_D(ctkDICOMInserterWorker); + + QSharedPointer inserterJob = + qSharedPointerObjectCast(job); + if (!inserterJob) + { + return; + } + + this->Superclass::setJob(job); + d->setInserterParameters(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMInserterWorker::inserterShared() const +{ + Q_D(const ctkDICOMInserterWorker); + return d->Inserter; +} + +//------------------------------------------------------------------------------ +ctkDICOMInserter* ctkDICOMInserterWorker::inserter() const +{ + Q_D(const ctkDICOMInserterWorker); + return d->Inserter.data(); +} diff --git a/Libs/DICOM/Core/ctkDICOMInserterWorker.h b/Libs/DICOM/Core/ctkDICOMInserterWorker.h new file mode 100644 index 0000000000..ffd710aa7d --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMInserterWorker.h @@ -0,0 +1,78 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMInserterWorker_h +#define __ctkDICOMInserterWorker_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkAbstractWorker.h" +class ctkDICOMInserter; +class ctkDICOMInserterWorkerPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMInserterWorker : public ctkAbstractWorker +{ + Q_OBJECT + +public: + typedef ctkAbstractWorker Superclass; + explicit ctkDICOMInserterWorker(); + virtual ~ctkDICOMInserterWorker(); + + /// Execute worker + void run() override; + + /// Cancel worker + void cancel() override; + + /// Job + void setJob(QSharedPointer job) override; + using ctkAbstractWorker::setJob; + + ///@{ + /// Inserter + QSharedPointer inserterShared() const; + Q_INVOKABLE ctkDICOMInserter* inserter() const; + ///@} + +protected: + QScopedPointer d_ptr; + + /// Constructor allowing derived class to specify a specialized pimpl. + /// + /// \note You are responsible to call init() in the constructor of + /// derived class. Doing so ensures the derived class is fully + /// instantiated in case virtual method are called within init() itself. + ctkDICOMInserterWorker(ctkDICOMInserterWorkerPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMInserterWorker); + Q_DISABLE_COPY(ctkDICOMInserterWorker); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMInserterWorker_p.h b/Libs/DICOM/Core/ctkDICOMInserterWorker_p.h new file mode 100644 index 0000000000..42822c5857 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMInserterWorker_p.h @@ -0,0 +1,53 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMInserterWorkerPrivate_h +#define __ctkDICOMInserterWorkerPrivate_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMInserter.h" +#include "ctkDICOMInserterWorker.h" + +//------------------------------------------------------------------------------ +class ctkDICOMInserterWorkerPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMInserterWorker) + +protected: + ctkDICOMInserterWorker* const q_ptr; + +public: + ctkDICOMInserterWorkerPrivate(ctkDICOMInserterWorker* object); + virtual ~ctkDICOMInserterWorkerPrivate(); + + void setInserterParameters(); + + QSharedPointer Inserter; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMItem.cpp b/Libs/DICOM/Core/ctkDICOMItem.cpp index 441275ea1b..2eea84c65a 100644 --- a/Libs/DICOM/Core/ctkDICOMItem.cpp +++ b/Libs/DICOM/Core/ctkDICOMItem.cpp @@ -129,6 +129,18 @@ void ctkDICOMItem::InitializeFromFile(const QString& filename, InitializeFromItem(dataset, true); } +ctkDICOMItem* ctkDICOMItem::Clone() +{ + Q_D(ctkDICOMItem); + ctkDICOMItem* newItem = new ctkDICOMItem; + // Given that we exclusively handle `DcmItem` and `DcmDataset` (where `DcmDataset` + // inherits from `DcmItem`), we safely assume that casting from `DcmObject` to + // `DcmItem` is always valid. + DcmItem* dcmItem = dynamic_cast(d->m_DcmItem->clone()); + newItem->InitializeFromItem(dcmItem, true); + return newItem; +} + void ctkDICOMItem::Serialize() { Q_D(ctkDICOMItem); @@ -247,6 +259,12 @@ DcmItem& ctkDICOMItem::GetDcmItem() const return *d->m_DcmItem; } +DcmItem* ctkDICOMItem::GetDcmItemPointer() const +{ + const Q_D(ctkDICOMItem); + return d->m_DcmItem; +} + OFCondition ctkDICOMItem::findAndGetElement(const DcmTag& tag, DcmElement*& element, const OFBool searchIntoSub) const { EnsureDcmDataSetIsInitialized(); diff --git a/Libs/DICOM/Core/ctkDICOMItem.h b/Libs/DICOM/Core/ctkDICOMItem.h index f6bb26db9d..9a42b73bd1 100644 --- a/Libs/DICOM/Core/ctkDICOMItem.h +++ b/Libs/DICOM/Core/ctkDICOMItem.h @@ -96,7 +96,10 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMItem const Uint32 maxReadLength = DCM_MaxReadLength, const E_FileReadMode readMode = ERM_autoDetect); - + /// \brief Clone this object. + /// + /// \returns deep copy of this object. + ctkDICOMItem* Clone(); /// \brief Save dataset to file /// @@ -247,6 +250,16 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMItem /// static QString TagVR( const DcmTag& tag ); + /// + /// \brief return dcm item + /// + DcmItem& GetDcmItem() const; + + /// + /// \brief return dcm item pointer + /// + DcmItem* GetDcmItemPointer() const; + protected: /// @@ -267,8 +280,6 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMItem QScopedPointer d_ptr; - DcmItem& GetDcmItem() const; - private: Q_DECLARE_PRIVATE(ctkDICOMItem); }; diff --git a/Libs/DICOM/Core/ctkDICOMJob.cpp b/Libs/DICOM/Core/ctkDICOMJob.cpp new file mode 100644 index 0000000000..4fc5d8ce3c --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMJob.cpp @@ -0,0 +1,167 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMJob.h" +#include "ctkDICOMJobResponseSet.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMJob"); + +//------------------------------------------------------------------------------ +// ctkDICOMJob methods + +//------------------------------------------------------------------------------ +ctkDICOMJob::ctkDICOMJob() +{ + this->DICOMLevel = DICOMLevels::Patients; + this->PatientID = ""; + this->StudyInstanceUID = ""; + this->SeriesInstanceUID = ""; + this->SOPInstanceUID = ""; +} + +//------------------------------------------------------------------------------ +ctkDICOMJob::~ctkDICOMJob() = default; + +//------------------------------------------------------------------------------ +void ctkDICOMJob::setDICOMLevel(const DICOMLevels& dicomLevel) +{ + this->DICOMLevel = dicomLevel; +} + +//------------------------------------------------------------------------------ +ctkDICOMJob::DICOMLevels ctkDICOMJob::dicomLevel() const +{ + return this->DICOMLevel; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJob::setPatientID(const QString& patientID) +{ + this->PatientID = patientID; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMJob::patientID() const +{ + return this->PatientID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJob::setStudyInstanceUID(const QString& studyInstanceUID) +{ + this->StudyInstanceUID = studyInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJob::studyInstanceUID() const +{ + return this->StudyInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJob::setSeriesInstanceUID(const QString& seriesInstanceUID) +{ + this->SeriesInstanceUID = seriesInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJob::seriesInstanceUID() const +{ + return this->SeriesInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJob::setSOPInstanceUID(const QString& sopInstanceUID) +{ + this->SOPInstanceUID = sopInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJob::sopInstanceUID() const +{ + return this->SOPInstanceUID; +} + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//------------------------------------------------------------------------------ +QList ctkDICOMJob::jobResponseSets() const +{ + QList jobResponseSets; + foreach (QSharedPointer jobResponseSet, this->JobResponseSets) + { + jobResponseSets.append(jobResponseSet.data()); + } + + return jobResponseSets; +} + +//------------------------------------------------------------------------------ +QList> ctkDICOMJob::jobResponseSetsShared() const +{ + return this->JobResponseSets; +} + +//------------------------------------------------------------------------------ +void ctkDICOMJob::setJobResponseSets(const QList& jobResponseSets) +{ + this->JobResponseSets.clear(); + foreach (ctkDICOMJobResponseSet* jobResponseSet, jobResponseSets) + { + this->JobResponseSets.append(QSharedPointer(jobResponseSet, skipDelete)); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMJob::setJobResponseSets(const QList>& jobResponseSets) +{ + this->JobResponseSets = jobResponseSets; +} + +//------------------------------------------------------------------------------ +void ctkDICOMJob::copyJobResponseSets(const QList>& jobResponseSets) +{ + this->JobResponseSets.clear(); + foreach (QSharedPointer jobResponseSet, jobResponseSets) + { + QSharedPointer jobResponseSetCopy = + QSharedPointer(jobResponseSet->clone()); + this->JobResponseSets.append(jobResponseSetCopy); + } +} + +//------------------------------------------------------------------------------ +QVariant ctkDICOMJob::toVariant() +{ + return QVariant::fromValue(ctkDICOMJobDetail(*this)); +} diff --git a/Libs/DICOM/Core/ctkDICOMJob.h b/Libs/DICOM/Core/ctkDICOMJob.h new file mode 100644 index 0000000000..daaca74bfb --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMJob.h @@ -0,0 +1,124 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMJob_h +#define __ctkDICOMJob_h + +// Qt includes +#include +#include +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +class ctkDICOMJobResponseSet; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMJob : public ctkAbstractJob +{ + Q_OBJECT + Q_ENUMS(DICOMLevel) + Q_PROPERTY(QString studyInstanceUID READ studyInstanceUID WRITE setStudyInstanceUID); + Q_PROPERTY(QString seriesInstanceUID READ seriesInstanceUID WRITE setSeriesInstanceUID); + Q_PROPERTY(QString sopInstanceUID READ sopInstanceUID WRITE setSOPInstanceUID); + Q_PROPERTY(DICOMLevels dicomLevel READ dicomLevel WRITE setDICOMLevel); + +public: + typedef ctkAbstractJob Superclass; + explicit ctkDICOMJob(); + virtual ~ctkDICOMJob(); + + enum DICOMLevels + { + Patients, + Studies, + Series, + Instances + }; + + ///@{ + /// DICOM Level + void setDICOMLevel(const DICOMLevels& dicomLevel); + DICOMLevels dicomLevel() const; + ///@} + + ///@{ + /// Patient ID + void setPatientID(const QString& patientID); + QString patientID() const; + ///@} + + ///@{ + /// Study instance UID + void setStudyInstanceUID(const QString& studyInstanceUID); + QString studyInstanceUID() const; + ///@} + + ///@{ + /// Series instance UID + void setSeriesInstanceUID(const QString& seriesInstanceUID); + QString seriesInstanceUID() const; + ///@} + + ///@{ + /// SOP instance UID + void setSOPInstanceUID(const QString& sopInstanceUID); + QString sopInstanceUID() const; + ///@} + + ///@{ + /// Access the list of responses. + Q_INVOKABLE QList jobResponseSets() const; + QList> jobResponseSetsShared() const; + Q_INVOKABLE void setJobResponseSets(const QList& jobResponseSets); + void setJobResponseSets(const QList>& jobResponseSets); + void copyJobResponseSets(const QList>& jobResponseSets); + ///@} + + /// Return the QVariant value of this job. + /// + /// The value is set using the ctkDICOMJobDetail metatype and is used to pass + /// information between threads using Qt signals. + /// \sa ctkDICOMJobDetail + Q_INVOKABLE virtual QVariant toVariant() override; + +Q_SIGNALS: + void progressJobDetail(QVariant); + void finishedJobDetail(QVariant); + +protected: + QString PatientID; + QString StudyInstanceUID; + QString SeriesInstanceUID; + QString SOPInstanceUID; + ctkDICOMJob::DICOMLevels DICOMLevel; + QList> JobResponseSets; + +private: + Q_DISABLE_COPY(ctkDICOMJob); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMJobResponseSet.cpp b/Libs/DICOM/Core/ctkDICOMJobResponseSet.cpp new file mode 100644 index 0000000000..17f41565e6 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMJobResponseSet.cpp @@ -0,0 +1,379 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMItem.h" +#include "ctkDICOMJobResponseSet.h" + +// DCMTK includes +#include + +static ctkLogger logger("org.commontk.dicom.DICOMTaskResults"); + +//------------------------------------------------------------------------------ +class ctkDICOMJobResponseSetPrivate : public QObject +{ + Q_DECLARE_PUBLIC(ctkDICOMJobResponseSet); + +protected: + ctkDICOMJobResponseSet* const q_ptr; + +public: + ctkDICOMJobResponseSetPrivate(ctkDICOMJobResponseSet& obj); + ~ctkDICOMJobResponseSetPrivate(); + + QString FilePath; + bool CopyFile; + bool OverwriteExistingDataset; + + ctkDICOMJobResponseSet::JobType JobType; + QString JobUID; + QString PatientID; + QString StudyInstanceUID; + QString SeriesInstanceUID; + QString SOPInstanceUID; + QString ConnectionName; + QMap> Datasets; +}; + +//------------------------------------------------------------------------------ +// ctkDICOMJobResponseSetPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMJobResponseSetPrivate::ctkDICOMJobResponseSetPrivate(ctkDICOMJobResponseSet& obj) + : q_ptr(&obj) +{ + this->JobType = ctkDICOMJobResponseSet::JobType::None; + this->JobUID = ""; + this->PatientID = ""; + this->StudyInstanceUID = ""; + this->SeriesInstanceUID = ""; + this->SOPInstanceUID = ""; + this->ConnectionName = ""; + this->CopyFile = false; + this->OverwriteExistingDataset = false; + this->FilePath = ""; +} + +//------------------------------------------------------------------------------ +ctkDICOMJobResponseSetPrivate::~ctkDICOMJobResponseSetPrivate() +{ + this->Datasets.clear(); +} + +//------------------------------------------------------------------------------ +// ctkDICOMJobResponseSet methods + +//------------------------------------------------------------------------------ +ctkDICOMJobResponseSet::ctkDICOMJobResponseSet(QObject* parent) + : QObject(parent), + d_ptr(new ctkDICOMJobResponseSetPrivate(*this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMJobResponseSet::~ctkDICOMJobResponseSet() = default; + +//---------------------------------------------------------------------------- +void ctkDICOMJobResponseSet::setFilePath(const QString& filePath) +{ + Q_D(ctkDICOMJobResponseSet); + d->FilePath = filePath; + + if (d->FilePath.isEmpty()) + { + return; + } + + QSharedPointer dataset = + QSharedPointer(new ctkDICOMItem); + dataset->InitializeFromFile(filePath); + + DcmItem dcmItem = dataset->GetDcmItem(); + OFString SOPInstanceUID; + dcmItem.findAndGetOFString(DCM_SOPInstanceUID, SOPInstanceUID); + + d->Datasets.insert(QString(SOPInstanceUID.c_str()), dataset); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMJobResponseSet::filePath() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->FilePath; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJobResponseSet::setCopyFile(bool copyFile) +{ + Q_D(ctkDICOMJobResponseSet); + d->CopyFile = copyFile; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMJobResponseSet::copyFile() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->CopyFile; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJobResponseSet::setOverwriteExistingDataset(bool overwriteExistingDataset) +{ + Q_D(ctkDICOMJobResponseSet); + d->OverwriteExistingDataset = overwriteExistingDataset; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMJobResponseSet::overwriteExistingDataset() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->OverwriteExistingDataset; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJobResponseSet::setJobType(ctkDICOMJobResponseSet::JobType jobType) +{ + Q_D(ctkDICOMJobResponseSet); + d->JobType = jobType; +} + +//------------------------------------------------------------------------------ +ctkDICOMJobResponseSet::JobType ctkDICOMJobResponseSet::jobType() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->JobType; +} + +//------------------------------------------------------------------------------ +void ctkDICOMJobResponseSet::setJobUID(const QString& jobUID) +{ + Q_D(ctkDICOMJobResponseSet); + d->JobUID = jobUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJobResponseSet::jobUID() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->JobUID; +} + +//------------------------------------------------------------------------------ +void ctkDICOMJobResponseSet::setPatientID(const QString& patientID) +{ + Q_D(ctkDICOMJobResponseSet); + d->PatientID = patientID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJobResponseSet::patientID() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->PatientID; +} + +//------------------------------------------------------------------------------ +void ctkDICOMJobResponseSet::setStudyInstanceUID(const QString& studyInstanceUID) +{ + Q_D(ctkDICOMJobResponseSet); + d->StudyInstanceUID = studyInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJobResponseSet::studyInstanceUID() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->StudyInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJobResponseSet::setSeriesInstanceUID(const QString& seriesInstanceUID) +{ + Q_D(ctkDICOMJobResponseSet); + d->SeriesInstanceUID = seriesInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJobResponseSet::seriesInstanceUID() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->SeriesInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMJobResponseSet::setSOPInstanceUID(const QString& sopInstanceUID) +{ + Q_D(ctkDICOMJobResponseSet); + d->SOPInstanceUID = sopInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJobResponseSet::sopInstanceUID() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->SOPInstanceUID; +} + +//------------------------------------------------------------------------------ +void ctkDICOMJobResponseSet::setConnectionName(const QString& connectionName) +{ + Q_D(ctkDICOMJobResponseSet); + d->ConnectionName = connectionName; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMJobResponseSet::connectionName() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->ConnectionName; +} + +//------------------------------------------------------------------------------ +void ctkDICOMJobResponseSet::setDataset(DcmItem* dcmItem, bool takeOwnership) +{ + Q_D(ctkDICOMJobResponseSet); + if (!dcmItem) + { + return; + } + + QSharedPointer dataset = + QSharedPointer(new ctkDICOMItem); + dataset->InitializeFromItem(dcmItem, takeOwnership); + + OFString SOPInstanceUID; + dcmItem->findAndGetOFString(DCM_SOPInstanceUID, SOPInstanceUID); + d->Datasets.insert(QString(SOPInstanceUID.c_str()), dataset); +} + +//------------------------------------------------------------------------------ +ctkDICOMItem* ctkDICOMJobResponseSet::dataset() const +{ + Q_D(const ctkDICOMJobResponseSet); + if (d->Datasets.count() == 0) + { + return nullptr; + } + return d->Datasets.first().data(); +} + +//------------------------------------------------------------------------------ +QSharedPointer ctkDICOMJobResponseSet::datasetShared() const +{ + Q_D(const ctkDICOMJobResponseSet); + if (d->Datasets.count() == 0) + { + return nullptr; + } + return d->Datasets.first(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMJobResponseSet::setDatasets(const QMap& dcmItems, bool takeOwnership) +{ + Q_D(ctkDICOMJobResponseSet); + for (const QString& key : dcmItems.keys()) + { + DcmItem* dcmItem = dcmItems.value(key); + if (!dcmItem) + { + continue; + } + + QSharedPointer dataset = + QSharedPointer(new ctkDICOMItem); + dataset->InitializeFromItem(dcmItem, takeOwnership); + + d->Datasets.insert(key, dataset); + } +} + +//------------------------------------------------------------------------------ +QMap ctkDICOMJobResponseSet::datasets() const +{ + Q_D(const ctkDICOMJobResponseSet); + QMap datasets; + + for (const QString& key : d->Datasets.keys()) + { + QSharedPointer dcmItem = d->Datasets.value(key); + if (!dcmItem) + { + continue; + } + + datasets.insert(key, dcmItem.data()); + } + + return datasets; +} + +//------------------------------------------------------------------------------ +QMap> ctkDICOMJobResponseSet::datasetsShared() const +{ + Q_D(const ctkDICOMJobResponseSet); + return d->Datasets; +} + +//------------------------------------------------------------------------------ +ctkDICOMJobResponseSet* ctkDICOMJobResponseSet::clone() +{ + ctkDICOMJobResponseSet* newJobResponseSet = new ctkDICOMJobResponseSet; + + newJobResponseSet->setFilePath(this->filePath()); + newJobResponseSet->setCopyFile(this->copyFile()); + newJobResponseSet->setOverwriteExistingDataset(this->overwriteExistingDataset()); + newJobResponseSet->setJobType(this->jobType()); + newJobResponseSet->setJobUID(this->jobUID()); + newJobResponseSet->setPatientID(this->patientID()); + newJobResponseSet->setStudyInstanceUID(this->studyInstanceUID()); + newJobResponseSet->setSeriesInstanceUID(this->seriesInstanceUID()); + newJobResponseSet->setSOPInstanceUID(this->sopInstanceUID()); + newJobResponseSet->setConnectionName(this->connectionName()); + + // Clone datasets + QMap datasets = this->datasets(); + for (const QString& key : datasets.keys()) + { + ctkDICOMItem* dataset = datasets.value(key); + if (!dataset) + { + continue; + } + QSharedPointer newDataset = + QSharedPointer(dataset->Clone()); + newJobResponseSet->d_func()->Datasets.insert(key, newDataset); + } + + return newJobResponseSet; +} + +//------------------------------------------------------------------------------ +QVariant ctkDICOMJobResponseSet::toVariant() +{ + return QVariant::fromValue(ctkDICOMJobDetail(*this)); +} diff --git a/Libs/DICOM/Core/ctkDICOMJobResponseSet.h b/Libs/DICOM/Core/ctkDICOMJobResponseSet.h new file mode 100644 index 0000000000..d4062515f7 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMJobResponseSet.h @@ -0,0 +1,210 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMJobResponseSet_h +#define __ctkDICOMJobResponseSet_h + +// Qt includes +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMJob.h" +#include "ctkDICOMItem.h" + +class ctkDICOMJobResponseSetPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMJobResponseSet : public QObject +{ + Q_OBJECT + Q_ENUMS(JobType) + Q_PROPERTY(QString filePath READ filePath WRITE setFilePath); + Q_PROPERTY(bool copyFile READ copyFile WRITE setCopyFile); + Q_PROPERTY(bool overwriteExistingDataset READ overwriteExistingDataset WRITE setOverwriteExistingDataset); + Q_PROPERTY(JobType jobType READ jobType WRITE setJobType); + Q_PROPERTY(QString jobUID READ jobUID WRITE setJobUID); + Q_PROPERTY(QString patientID READ patientID WRITE setPatientID); + Q_PROPERTY(QString studyInstanceUID READ studyInstanceUID WRITE setStudyInstanceUID); + Q_PROPERTY(QString seriesInstanceUID READ seriesInstanceUID WRITE setSeriesInstanceUID); + Q_PROPERTY(QString sopInstanceUID READ sopInstanceUID WRITE setSOPInstanceUID); + Q_PROPERTY(QString connectionName READ connectionName WRITE setConnectionName); + +public: + explicit ctkDICOMJobResponseSet(QObject* parent = 0); + virtual ~ctkDICOMJobResponseSet(); + + ///@{ + /// File Path + void setFilePath(const QString& filePath); + QString filePath() const; + ///@} + + ///@{ + /// Copy File + /// false as default + void setCopyFile(bool copyFile); + bool copyFile() const; + ///@} + + ///@{ + /// Overwrite existing dataset + /// false as default + void setOverwriteExistingDataset(bool overwriteExistingDataset); + bool overwriteExistingDataset() const; + ///@} + + ///@{ + /// Job type + enum JobType + { + None = 0, + QueryPatients, + QueryStudies, + QuerySeries, + QueryInstances, + RetrieveStudy, + RetrieveSeries, + RetrieveSOPInstance, + StoreSOPInstance + }; + void setJobType(JobType jobType); + JobType jobType() const; + ///@} + + ///@{ + /// Task UID + void setJobUID(const QString& jobUID); + QString jobUID() const; + ///@} + + ///@{ + /// Patient ID + void setPatientID(const QString& patientID); + QString patientID() const; + ///@} + + ///@{ + /// Study instance UID + void setStudyInstanceUID(const QString& studyInstanceUID); + QString studyInstanceUID() const; + ///@} + + ///@{ + /// Series instance UID + void setSeriesInstanceUID(const QString& seriesInstanceUID); + QString seriesInstanceUID() const; + ///@} + + ///@{ + /// SOP instance UID + void setSOPInstanceUID(const QString& sopInstanceUID); + QString sopInstanceUID() const; + ///@} + + ///@{ + /// Connection name + void setConnectionName(const QString& connectionName); + QString connectionName() const; + ///@} + + ///@{ + /// DCM datasets + Q_INVOKABLE void setDataset(DcmItem* dcmItem, bool takeOwnership = true); + Q_INVOKABLE ctkDICOMItem* dataset() const; + QSharedPointer datasetShared() const; + Q_INVOKABLE void setDatasets(const QMap& dcmItems, bool takeOwnership = true); + Q_INVOKABLE QMap datasets() const; + QMap> datasetsShared() const; + ///@} + + /// Create a copy of this JobResponseSet. + Q_INVOKABLE ctkDICOMJobResponseSet* clone(); + + /// Return the QVariant value of this JobResponseSet. + /// + /// The value is set using the ctkDICOMJobDetail metatype and is used to pass + /// information between threads using Qt signals. + Q_INVOKABLE QVariant toVariant(); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMJobResponseSet); + Q_DISABLE_COPY(ctkDICOMJobResponseSet); +}; + +//------------------------------------------------------------------------------ +struct CTK_DICOM_CORE_EXPORT ctkDICOMJobDetail : ctkJobDetail +{ + explicit ctkDICOMJobDetail() = default; + + explicit ctkDICOMJobDetail(const ctkDICOMJob& job) : ctkJobDetail(job) + { + this->DICOMLevel = job.dicomLevel(); + this->PatientID = job.patientID(); + this->StudyInstanceUID = job.studyInstanceUID(); + this->SeriesInstanceUID = job.seriesInstanceUID(); + this->SOPInstanceUID = job.sopInstanceUID(); + } + + explicit ctkDICOMJobDetail(const ctkDICOMJob& job, const QString& connectionName) + : ctkDICOMJobDetail(job) + { + this->ConnectionName = connectionName; + } + + explicit ctkDICOMJobDetail(const ctkDICOMJobResponseSet& responseSet) + { + this->JobUID = responseSet.jobUID(); + this->JobType = responseSet.jobType(); + this->PatientID = responseSet.patientID(); + this->StudyInstanceUID = responseSet.studyInstanceUID(); + this->SeriesInstanceUID = responseSet.seriesInstanceUID(); + this->SOPInstanceUID = responseSet.sopInstanceUID(); + this->ConnectionName = responseSet.connectionName(); + this->NumberOfDataSets = responseSet.datasets().count(); + } + virtual ~ctkDICOMJobDetail() = default; + + QString PatientID; + QString StudyInstanceUID; + QString SeriesInstanceUID; + QString SOPInstanceUID; + + // Common to DICOM Query and Retrieve jobs, and DICOM JobResponseSet + QString ConnectionName; + + // Specific to DICOM Query and Retrieve jobs + ctkDICOMJob::DICOMLevels DICOMLevel{ctkDICOMJob::DICOMLevels::Patients}; + + // Specific to DICOM JobResponseSet + ctkDICOMJobResponseSet::JobType JobType{ctkDICOMJobResponseSet::JobType::None}; + int NumberOfDataSets{0}; +}; +Q_DECLARE_METATYPE(ctkDICOMJobDetail); + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMModel.h b/Libs/DICOM/Core/ctkDICOMModel.h index c8e4eba86d..c1652ac738 100644 --- a/Libs/DICOM/Core/ctkDICOMModel.h +++ b/Libs/DICOM/Core/ctkDICOMModel.h @@ -27,6 +27,7 @@ #include #include +// ctkDICOMCore includes #include "ctkDICOMCoreExport.h" class ctkDICOMModelPrivate; diff --git a/Libs/DICOM/Core/ctkDICOMQuery.cpp b/Libs/DICOM/Core/ctkDICOMQuery.cpp index 565a962f9f..42b453121b 100644 --- a/Libs/DICOM/Core/ctkDICOMQuery.cpp +++ b/Libs/DICOM/Core/ctkDICOMQuery.cpp @@ -33,25 +33,21 @@ // ctkDICOMCore includes #include "ctkDICOMQuery.h" -#include "ctkDICOMUtil.h" #include "ctkLogger.h" +#include "ctkDICOMJobResponseSet.h" // DCMTK includes -#include "dcmtk/dcmnet/dimse.h" -#include "dcmtk/dcmnet/diutil.h" -#include "dcmtk/dcmnet/scu.h" - -#include +#include #include #include #include +#include #include #include #include #include /* for class OFStandard */ #include /* for class DicomDirInterface */ - static ctkLogger logger ( "org.commontk.dicom.DICOMQuery" ); //------------------------------------------------------------------------------ @@ -70,13 +66,14 @@ class ctkDICOMQuerySCUPrivate : public DcmSCU QRResponse *response, OFBool &waitForNextResponse) { - if (this->query) - { - logger.debug ( "FIND RESPONSE" ); - emit this->query->debug(/*no tr*/"Got a find response!"); - return this->DcmSCU::handleFINDResponse(presID, response, waitForNextResponse); - } - return DIMSE_NULLKEY; + if (!this->query || this->query->wasCanceled()) + { + return EC_IllegalCall; + } + + logger.debug ( "FIND RESPONSE" ); + emit this->query->debug(/*no tr*/"Got a find response!"); + return this->DcmSCU::handleFINDResponse(presID, response, waitForNextResponse); }; }; @@ -88,22 +85,25 @@ class ctkDICOMQueryPrivate ~ctkDICOMQueryPrivate(); /// Add a StudyInstanceUID to be queried - void addStudyInstanceUIDAndDataset(const QString& StudyInstanceUID, DcmDataset* dataset ); + void addStudyInstanceUIDAndDataset(const QString& studyInstanceUID, DcmDataset* dataset ); /// Add StudyInstanceUID and SeriesInstanceUID that may be further retrieved - void addStudyAndSeriesInstanceUID( const QString& StudyInstanceUID, const QString& SeriesInstanceUID ); - - QString CallingAETitle; - QString CalledAETitle; - QString Host; - int Port; - bool PreferCGET; - QMap Filters; + void addStudyAndSeriesInstanceUID( const QString& studyInstanceUID, const QString& seriesInstanceUID ); + + QString ConnectionName; + QString CallingAETitle; + QString CalledAETitle; + QString Host; + int Port; + QMap Filters; ctkDICOMQuerySCUPrivate SCU; - DcmDataset* Query; - QList< QPair > StudyAndSeriesInstanceUIDPairList; - QStringList StudyInstanceUIDList; - QList StudyDatasetList; - bool Canceled; + Uint16 PresentationContext; + QSharedPointer QueryDcmDataset; + QList> StudyAndSeriesInstanceUIDPairList; + QMap StudyDatasets; + bool Canceled; + int MaximumPatientsQuery; + QString JobUID; + QList> JobResponseSets; }; //------------------------------------------------------------------------------ @@ -112,29 +112,32 @@ class ctkDICOMQueryPrivate //------------------------------------------------------------------------------ ctkDICOMQueryPrivate::ctkDICOMQueryPrivate() { - this->Query = new DcmDataset(); + this->QueryDcmDataset = QSharedPointer(new DcmDataset); + this->PresentationContext = 0; this->Port = 0; this->Canceled = false; - this->PreferCGET = false; + this->MaximumPatientsQuery = 25; + + this->SCU.setACSETimeout(10); + this->SCU.setConnectionTimeout(10); } //------------------------------------------------------------------------------ ctkDICOMQueryPrivate::~ctkDICOMQueryPrivate() { - delete this->Query; + this->JobResponseSets.clear(); } //------------------------------------------------------------------------------ -void ctkDICOMQueryPrivate::addStudyAndSeriesInstanceUID( const QString& study, const QString& series ) +void ctkDICOMQueryPrivate::addStudyAndSeriesInstanceUID( const QString& studyInstanceUID, const QString& seriesInstanceUID ) { - this->StudyAndSeriesInstanceUIDPairList.push_back (qMakePair( study, series ) ); + this->StudyAndSeriesInstanceUIDPairList.push_back (qMakePair( studyInstanceUID, seriesInstanceUID ) ); } //------------------------------------------------------------------------------ -void ctkDICOMQueryPrivate::addStudyInstanceUIDAndDataset( const QString& study, DcmDataset* dataset ) +void ctkDICOMQueryPrivate::addStudyInstanceUIDAndDataset( const QString& studyInstanceUID, DcmDataset* dataset ) { - this->StudyInstanceUIDList.append ( study ); - this->StudyDatasetList.append ( dataset ); + this->StudyDatasets[studyInstanceUID] = dataset; } //------------------------------------------------------------------------------ @@ -146,6 +149,8 @@ ctkDICOMQuery::ctkDICOMQuery(QObject* parentObject) , d_ptr(new ctkDICOMQueryPrivate) { Q_D(ctkDICOMQuery); + + d->SCU.setVerbosePCMode(false); d->SCU.query = this; // give the dcmtk level access to this for emitting signals } @@ -154,9 +159,22 @@ ctkDICOMQuery::~ctkDICOMQuery() { } -/// Set methods for connectivity //------------------------------------------------------------------------------ -void ctkDICOMQuery::setCallingAETitle( const QString& callingAETitle ) +void ctkDICOMQuery::setConnectionName(const QString& connectionName) +{ + Q_D(ctkDICOMQuery); + d->ConnectionName = connectionName; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMQuery::connectionName() const +{ + Q_D(const ctkDICOMQuery); + return d->ConnectionName; +} + +//------------------------------------------------------------------------------ +void ctkDICOMQuery::setCallingAETitle(const QString& callingAETitle) { Q_D(ctkDICOMQuery); d->CallingAETitle = callingAETitle; @@ -170,7 +188,7 @@ QString ctkDICOMQuery::callingAETitle() const } //------------------------------------------------------------------------------ -void ctkDICOMQuery::setCalledAETitle( const QString& calledAETitle ) +void ctkDICOMQuery::setCalledAETitle(const QString& calledAETitle) { Q_D(ctkDICOMQuery); d->CalledAETitle = calledAETitle; @@ -184,7 +202,7 @@ QString ctkDICOMQuery::calledAETitle()const } //------------------------------------------------------------------------------ -void ctkDICOMQuery::setHost( const QString& host ) +void ctkDICOMQuery::setHost(const QString& host) { Q_D(ctkDICOMQuery); d->Host = host; @@ -198,7 +216,7 @@ QString ctkDICOMQuery::host() const } //------------------------------------------------------------------------------ -void ctkDICOMQuery::setPort ( int port ) +void ctkDICOMQuery::setPort(int port) { Q_D(ctkDICOMQuery); d->Port = port; @@ -211,22 +229,44 @@ int ctkDICOMQuery::port()const return d->Port; } +//----------------------------------------------------------------------------- +void ctkDICOMQuery::setConnectionTimeout(int timeout) +{ + Q_D(ctkDICOMQuery); + d->SCU.setACSETimeout(timeout); + d->SCU.setConnectionTimeout(timeout); +} + +//----------------------------------------------------------------------------- +int ctkDICOMQuery::connectionTimeout() const +{ + Q_D(const ctkDICOMQuery); + return d->SCU.getConnectionTimeout(); +} + //------------------------------------------------------------------------------ -void ctkDICOMQuery::setPreferCGET ( bool preferCGET ) +void ctkDICOMQuery::setMaximumPatientsQuery(int maximumPatientsQuery) { Q_D(ctkDICOMQuery); - d->PreferCGET = preferCGET; + d->MaximumPatientsQuery = maximumPatientsQuery; +} + +//------------------------------------------------------------------------------ +int ctkDICOMQuery::maximumPatientsQuery() +{ + Q_D(const ctkDICOMQuery); + return d->MaximumPatientsQuery; } //------------------------------------------------------------------------------ -bool ctkDICOMQuery::preferCGET()const +bool ctkDICOMQuery::wasCanceled() { Q_D(const ctkDICOMQuery); - return d->PreferCGET; + return d->Canceled; } //------------------------------------------------------------------------------ -void ctkDICOMQuery::setFilters( const QMap& filters ) +void ctkDICOMQuery::setFilters(const QMap& filters) { Q_D(ctkDICOMQuery); d->Filters = filters; @@ -247,276 +287,862 @@ QList< QPair > ctkDICOMQuery::studyAndSeriesInstanceUIDQueried( } //------------------------------------------------------------------------------ -bool ctkDICOMQuery::query(ctkDICOMDatabase& database ) +QList ctkDICOMQuery::jobResponseSets() const +{ + Q_D(const ctkDICOMQuery); + QList jobResponseSets; + foreach(QSharedPointer jobResponseSet, d->JobResponseSets) + { + jobResponseSets.append(jobResponseSet.data()); + } + + return jobResponseSets; +} + +//------------------------------------------------------------------------------ +QList> ctkDICOMQuery::jobResponseSetsShared() const +{ + Q_D(const ctkDICOMQuery); + return d->JobResponseSets; +} + +//------------------------------------------------------------------------------ +void ctkDICOMQuery::setJobUID(const QString &jobUID) +{ + Q_D(ctkDICOMQuery); + d->JobUID = jobUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMQuery::jobUID() const { - // turn on logging if needed for debug: - //ctk::setDICOMLogLevel(ctkErrorLogLevel::Debug); + Q_D(const ctkDICOMQuery); + return d->JobUID; +} - // ctkDICOMDatabase::setDatabase ( database ); +//------------------------------------------------------------------------------ +bool ctkDICOMQuery::query(ctkDICOMDatabase& database) +{ Q_D(ctkDICOMQuery); // In the following, we emit progress(int) after progress(QString), this // is in case the connected object doesn't refresh its ui when the progress // message is updated but only if the progress value is (e.g. QProgressDialog) - if ( database.database().isOpen() ) + if (database.database().isOpen()) { - logger.debug ( "DB open in Query" ); + logger.debug("DB open in Query"); emit progress(tr("DB open in Query")); } else { - logger.debug ( "DB not open in Query" ); + logger.debug("DB not open in Query"); emit progress(tr("DB not open in Query")); } emit progress(0); - if (d->Canceled) {return false;} - - d->StudyAndSeriesInstanceUIDPairList.clear(); - d->StudyInstanceUIDList.clear(); - d->SCU.setAETitle ( OFString(this->callingAETitle().toStdString().c_str()) ); - d->SCU.setPeerAETitle ( OFString(this->calledAETitle().toStdString().c_str()) ); - d->SCU.setPeerHostName ( OFString(this->host().toStdString().c_str()) ); - d->SCU.setPeerPort ( this->port() ); - - logger.error ( "Setting Transfer Syntaxes" ); - emit progress(tr("Setting Transfer Syntaxes")); - emit progress(10); - if (d->Canceled) {return false;} - - OFList transferSyntaxes; - transferSyntaxes.push_back ( UID_LittleEndianExplicitTransferSyntax ); - transferSyntaxes.push_back ( UID_BigEndianExplicitTransferSyntax ); - transferSyntaxes.push_back ( UID_LittleEndianImplicitTransferSyntax ); - - d->SCU.addPresentationContext ( UID_FINDStudyRootQueryRetrieveInformationModel, transferSyntaxes ); - // d->SCU.addPresentationContext ( UID_VerificationSOPClass, transferSyntaxes ); - if ( !d->SCU.initNetwork().good() ) + if (d->Canceled) { - logger.error( "Error initializing the network" ); - emit progress(tr("Error initializing the network")); - emit progress(100); return false; } - logger.debug ( "Negotiating Association" ); - emit progress(tr("Negotiating Association")); - emit progress(20); - if (d->Canceled) {return false;} - OFCondition result = d->SCU.negotiateAssociation(); - if (result.bad()) + d->StudyAndSeriesInstanceUIDPairList.clear(); + d->StudyDatasets.clear(); + + // initSCU + if (!this->initializeSCU()) { - logger.error( "Error negotiating the association: " + QString(result.text()) ); - emit progress(tr("Error negotiating the association")); - emit progress(100); return false; } // Clear the query - d->Query->clear(); + d->QueryDcmDataset->clear(); // Insert all keys that we like to receive values for - d->Query->insertEmptyElement ( DCM_PatientID ); - d->Query->insertEmptyElement ( DCM_PatientName ); - d->Query->insertEmptyElement ( DCM_PatientBirthDate ); - d->Query->insertEmptyElement ( DCM_StudyID ); - d->Query->insertEmptyElement ( DCM_StudyInstanceUID ); - d->Query->insertEmptyElement ( DCM_StudyDescription ); - d->Query->insertEmptyElement ( DCM_StudyDate ); - d->Query->insertEmptyElement ( DCM_StudyTime ); - d->Query->insertEmptyElement ( DCM_ModalitiesInStudy ); - d->Query->insertEmptyElement ( DCM_AccessionNumber ); - d->Query->insertEmptyElement ( DCM_NumberOfStudyRelatedInstances ); // Number of images in the series - d->Query->insertEmptyElement ( DCM_NumberOfStudyRelatedSeries ); // Number of series in the study + d->QueryDcmDataset->insertEmptyElement(DCM_PatientID); + d->QueryDcmDataset->insertEmptyElement(DCM_PatientName); + d->QueryDcmDataset->insertEmptyElement(DCM_PatientBirthDate); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyID); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyInstanceUID); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyDescription); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyDate); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyTime); + d->QueryDcmDataset->insertEmptyElement(DCM_ModalitiesInStudy); + d->QueryDcmDataset->insertEmptyElement(DCM_AccessionNumber); + d->QueryDcmDataset->insertEmptyElement(DCM_NumberOfStudyRelatedInstances); // Number of images in the series + d->QueryDcmDataset->insertEmptyElement(DCM_NumberOfStudyRelatedSeries); // Number of series in the study // Make clear we define our search values in ISO Latin 1 (default would be ASCII) - d->Query->putAndInsertOFStringArray(DCM_SpecificCharacterSet, "ISO_IR 100"); + d->QueryDcmDataset->putAndInsertOFStringArray(DCM_SpecificCharacterSet, "ISO_IR 100"); - d->Query->putAndInsertString ( DCM_QueryRetrieveLevel, "STUDY" ); - - /* Now, for all keys that the user provided for filtering on STUDY level, - * overwrite empty keys with value. For now, only Patient's Name, Patient ID, - * Study Description, Modalities in Study, and Study Date are used. - */ - QString seriesDescription; - foreach( QString key, d->Filters.keys() ) - { - if ( key == QString("Name") && !d->Filters[key].toString().isEmpty()) - { - // make the filter a wildcard in dicom style - d->Query->putAndInsertString( DCM_PatientName, - (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data()); - } - else if ( key == QString("Study") && !d->Filters[key].toString().isEmpty()) - { - // make the filter a wildcard in dicom style - d->Query->putAndInsertString( DCM_StudyDescription, - (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data()); - } - else if ( key == QString("ID") && !d->Filters[key].toString().isEmpty()) - { - // make the filter a wildcard in dicom style - d->Query->putAndInsertString( DCM_PatientID, - (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data()); - } - else if (key == QString("AccessionNumber") && !d->Filters[key].toString().isEmpty()) - { - // make the filter a wildcard in dicom style - d->Query->putAndInsertString(DCM_AccessionNumber, - (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data()); - } - else if ( key == QString("Modalities") && !d->Filters[key].toString().isEmpty()) - { - // make the filter be an "OR" of modalities using backslash (dicom-style) - QString modalitySearch(""); - foreach (const QString& modality, d->Filters[key].toStringList()) - { - modalitySearch += modality + QString("\\"); - } - modalitySearch.chop(1); // remove final backslash - logger.debug("modalityInStudySearch " + modalitySearch); - d->Query->putAndInsertString( DCM_ModalitiesInStudy, modalitySearch.toLatin1().data() ); - } - // Remember Series Description for later series query if we go through the keys now - else if ( key == QString("Series") && !d->Filters[key].toString().isEmpty()) - { - // make the filter a wildcard in dicom style - seriesDescription = "*" + d->Filters[key].toString() + "*"; - } - else - { - logger.debug("Ignoring unknown search key: " + key); - } - } + d->QueryDcmDataset->putAndInsertString (DCM_QueryRetrieveLevel, "STUDY"); - if ( d->Filters.keys().contains("StartDate") && d->Filters.keys().contains("EndDate") ) + QString seriesDescription = this->applyFilters(); + if (d->Canceled) { - QString dateRange = d->Filters["StartDate"].toString() + - QString("-") + - d->Filters["EndDate"].toString(); - d->Query->putAndInsertString ( DCM_StudyDate, dateRange.toLatin1().data() ); - logger.debug("Query on study date " + dateRange); + return false; } - emit progress(30); - if (d->Canceled) {return false;} OFList responses; Uint16 presentationContext = 0; // Check for any accepted presentation context for FIND in study root (don't care about transfer syntax) - presentationContext = d->SCU.findPresentationContextID ( UID_FINDStudyRootQueryRetrieveInformationModel, ""); - if ( presentationContext == 0 ) + presentationContext = d->SCU.findPresentationContextID(UID_FINDStudyRootQueryRetrieveInformationModel, ""); + if (presentationContext == 0) { - logger.error ( "Failed to find acceptable presentation context" ); + logger.error("Failed to find acceptable presentation context"); emit progress(tr("Failed to find acceptable presentation context")); } else { - logger.info ( "Found useful presentation context" ); + logger.info("Found useful presentation context"); emit progress(tr("Found useful presentation context")); } emit progress(40); - if (d->Canceled) {return false;} + if (d->Canceled) + { + return false; + } - OFCondition status = d->SCU.sendFINDRequest ( presentationContext, d->Query, &responses ); - if ( !status.good() ) + OFCondition status = d->SCU.sendFINDRequest(presentationContext, d->QueryDcmDataset.data(), &responses); + if (!status.good()) { - logger.error ( "Find failed" ); + logger.error("Find failed"); emit progress(tr("Find failed")); - d->SCU.closeAssociation ( DCMSCU_RELEASE_ASSOCIATION ); + d->SCU.releaseAssociation(); emit progress(100); return false; } - logger.debug ( "Find succeeded"); + logger.debug("Find succeeded"); emit progress(tr("Find succeeded")); emit progress(50); - if (d->Canceled) {return false;} + if (d->Canceled) + { + return false; + } - for ( OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++ ) + for (OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++) { DcmDataset *dataset = (*it)->m_dataset; - if ( dataset != NULL ) // the last response is always empty + if (dataset != NULL) // the last response is always empty { - database.insert ( dataset, false /* do not store to disk*/, false /* no thumbnail*/); + database.insert(dataset, false /* do not store to disk*/, false /* no thumbnail*/); OFString StudyInstanceUID; - dataset->findAndGetOFString ( DCM_StudyInstanceUID, StudyInstanceUID ); - d->addStudyInstanceUIDAndDataset ( StudyInstanceUID.c_str(), dataset ); - emit progress(tr("Processing Study: ") + QString(StudyInstanceUID.c_str())); + dataset->findAndGetOFString(DCM_StudyInstanceUID, StudyInstanceUID); + d->addStudyInstanceUIDAndDataset(StudyInstanceUID.c_str(), dataset); + emit progress(tr("Processing: ") + QString(StudyInstanceUID.c_str())); emit progress(50); - if (d->Canceled) {return false;} + if (d->Canceled) + { + return false; + } } } /* Only ask for series attributes now. This requires kicking out the rest of former query. */ - d->Query->clear(); - d->Query->insertEmptyElement ( DCM_SeriesNumber ); - d->Query->insertEmptyElement ( DCM_SeriesDescription ); - d->Query->insertEmptyElement ( DCM_SeriesInstanceUID ); - d->Query->insertEmptyElement ( DCM_SeriesDate ); - d->Query->insertEmptyElement ( DCM_SeriesTime ); - d->Query->insertEmptyElement ( DCM_Modality ); - d->Query->insertEmptyElement ( DCM_NumberOfSeriesRelatedInstances ); // Number of images in the series + d->QueryDcmDataset->clear(); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesNumber); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesDescription); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesInstanceUID); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesDate); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesTime); + d->QueryDcmDataset->insertEmptyElement(DCM_Modality); + d->QueryDcmDataset->insertEmptyElement(DCM_Rows); + d->QueryDcmDataset->insertEmptyElement(DCM_Columns); + d->QueryDcmDataset->insertEmptyElement(DCM_NumberOfSeriesRelatedInstances); // Number of images in the series /* Add user-defined filters */ - d->Query->putAndInsertOFStringArray(DCM_SeriesDescription, seriesDescription.toLatin1().data()); + d->QueryDcmDataset->putAndInsertOFStringArray(DCM_SeriesDescription, seriesDescription.toLatin1().data()); // Now search each within each Study that was identified - d->Query->putAndInsertString ( DCM_QueryRetrieveLevel, "SERIES" ); - float progressRatio = 25. / d->StudyInstanceUIDList.count(); + d->QueryDcmDataset->putAndInsertString(DCM_QueryRetrieveLevel, "SERIES"); + float progressRatio = 25. / d->StudyDatasets.count(); int i = 0; - QListIterator datasetIterator(d->StudyDatasetList); - Q_FOREACH(const QString & StudyInstanceUID, d->StudyInstanceUIDList ) + foreach(QString studyInstanceUID, d->StudyDatasets.keys()) { - DcmDataset *studyDataset = datasetIterator.next(); + DcmDataset *studyDataset = d->StudyDatasets.value(studyInstanceUID); DcmElement *patientName, *patientID; studyDataset->findAndGetElement(DCM_PatientName, patientName); studyDataset->findAndGetElement(DCM_PatientID, patientID); - logger.debug ( "Starting Series C-FIND for Study: " + StudyInstanceUID ); - emit progress(tr("Starting Series C-FIND for Study: ") + StudyInstanceUID); + logger.debug("Starting Series C-FIND for Study: " + studyInstanceUID); + emit progress(tr("Starting Series C-FIND for Study: ") + studyInstanceUID); emit progress(50 + (progressRatio * i++)); - if (d->Canceled) {return false;} + if (d->Canceled) + { + return false; + } - d->Query->putAndInsertString ( DCM_StudyInstanceUID, StudyInstanceUID.toStdString().c_str() ); + d->QueryDcmDataset->putAndInsertString (DCM_StudyInstanceUID, studyInstanceUID.toStdString().c_str()); OFList responses; - status = d->SCU.sendFINDRequest ( presentationContext, d->Query, &responses ); - if ( status.good() ) + status = d->SCU.sendFINDRequest(presentationContext, d->QueryDcmDataset.data(), &responses); + if (status.good()) { - for ( OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++ ) + for (OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++) { DcmDataset *dataset = (*it)->m_dataset; - if ( dataset != NULL ) + if (dataset != NULL) { - OFString SeriesInstanceUID; - dataset->findAndGetOFString ( DCM_SeriesInstanceUID, SeriesInstanceUID ); - d->addStudyAndSeriesInstanceUID ( StudyInstanceUID.toStdString().c_str(), SeriesInstanceUID.c_str() ); + OFString seriesInstanceUID; + dataset->findAndGetOFString(DCM_SeriesInstanceUID, seriesInstanceUID); + d->addStudyAndSeriesInstanceUID(studyInstanceUID.toStdString().c_str(), seriesInstanceUID.c_str()); // add the patient elements not provided for the series level query - dataset->insert( patientName, true ); - dataset->insert( patientID, true ); + dataset->insert(patientName, true); + dataset->insert(patientID, true); // insert series dataset - database.insert ( dataset, false /* do not store */, false /* no thumbnail */ ); + database.insert(dataset, false /* do not store */, false /* no thumbnail */); } } - logger.debug ( "Find succeeded on Series level for Study: " + StudyInstanceUID ); - emit progress(tr("Find succeeded on Series level for Study: ") + StudyInstanceUID); + logger.debug ("Find succeeded on Series level for Study: " + studyInstanceUID); + emit progress(tr("Find succeeded on Series level for Study: ") + studyInstanceUID); emit progress(50 + (progressRatio * i++)); - if (d->Canceled) {return false;} + if (d->Canceled) + { + return false; + } } else { - logger.error ( "Find on Series level failed for Study: " + StudyInstanceUID ); - emit progress(tr("Find on Series level failed for Study: ") + StudyInstanceUID); + logger.error ("Find on Series level failed for Study: " + studyInstanceUID); + emit progress(tr("Find on Series level failed for Study: ") + studyInstanceUID); } emit progress(50 + (progressRatio * i++)); - if (d->Canceled) {return false;} + if (d->Canceled) + { + return false; } - d->SCU.closeAssociation ( DCMSCU_RELEASE_ASSOCIATION ); + } + d->SCU.releaseAssociation(); emit progress(100); return true; } //---------------------------------------------------------------------------- -void ctkDICOMQuery::cancel() +bool ctkDICOMQuery::queryPatients() { Q_D(ctkDICOMQuery); - d->Canceled = true; + + // In the following, we emit progress(int) after progress(QString), this + // is in case the connected object doesn't refresh its ui when the progress + // message is updated but only if the progress value is (e.g. QProgressDialog) + emit progress(0); + if (d->Canceled) + { + return false; + } + + d->JobResponseSets.clear(); + + // initSCU + if (!this->initializeSCU()) + { + return false; + } + + // Clear the query + d->QueryDcmDataset->clear(); + + // Insert all keys that we like to receive values for + d->QueryDcmDataset->insertEmptyElement(DCM_PatientID); + d->QueryDcmDataset->insertEmptyElement(DCM_PatientName); + d->QueryDcmDataset->insertEmptyElement(DCM_PatientBirthDate); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyID); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyInstanceUID); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyDescription); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyDate); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyTime); + d->QueryDcmDataset->insertEmptyElement(DCM_ModalitiesInStudy); + d->QueryDcmDataset->insertEmptyElement(DCM_AccessionNumber); + d->QueryDcmDataset->insertEmptyElement(DCM_NumberOfStudyRelatedSeries); // Number of series in the study + + // Make clear we define our search values in ISO Latin 1 (default would be ASCII) + d->QueryDcmDataset->putAndInsertOFStringArray(DCM_SpecificCharacterSet, "ISO_IR 100"); + + d->QueryDcmDataset->putAndInsertString(DCM_QueryRetrieveLevel, "PATIENT"); + + QString seriesDescription = this->applyFilters(); + if (d->Canceled) + { + return false; + } + + Uint16 presentationContext = 0; + // Check for any accepted presentation context for FIND in study root (don't care about transfer syntax) + presentationContext = d->SCU.findPresentationContextID(UID_FINDStudyRootQueryRetrieveInformationModel, ""); + if (presentationContext == 0) + { + logger.error("Failed to find acceptable presentation context"); + emit progress(tr("Failed to find acceptable presentation context")); + } + else + { + logger.debug("Found useful presentation context"); + emit progress(tr("Found useful presentation context")); + } + emit progress(40); + if (d->Canceled) + { + return false; + } + + logger.debug("Starting Patients C-FIND"); + emit progress(tr("Starting Patients C-FIND")); + emit progress(50); + if (d->Canceled) + { + return false; + } + + QSharedPointer JobResponseSet = + QSharedPointer(new ctkDICOMJobResponseSet); + JobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::QueryPatients); + JobResponseSet->setConnectionName(d->ConnectionName); + JobResponseSet->setJobUID(d->JobUID); + + QMap datasetsMap; + OFList responses; + OFCondition status = d->SCU.sendFINDRequest(presentationContext, d->QueryDcmDataset.data(), &responses); + if (status.good()) + { + int contResponses = 0; + for (OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++) + { + contResponses++; + if (contResponses > d->MaximumPatientsQuery) + { + logger.warn(QString("ctkDICOMQuery: the number of responses of the query task at patients level " + "surpassed the maximum value of permitted results (i.e. %1).").arg(d->MaximumPatientsQuery)); + + break; + } + DcmDataset *dataset = (*it)->m_dataset; + if ( dataset != NULL ) + { + OFString patientID; + dataset->findAndGetOFString(DCM_PatientID, patientID); + datasetsMap.insert(patientID.c_str(), dataset); + } + } + + JobResponseSet->setDatasets(datasetsMap); + d->JobResponseSets.append(JobResponseSet); + + logger.debug("Find succeeded on Patient level"); + emit progress(tr("Find succeeded on Patient level")); + } + else + { + logger.error("Find on Patient level failed"); + emit progress(tr("Find on Patient level failed")); + } + + emit progress(100); + if (d->Canceled) + { + return false; + } + + d->SCU.releaseAssociation(); + return true; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMQuery::queryStudies(const QString& patientID) +{ + Q_D(ctkDICOMQuery); + // In the following, we emit progress(int) after progress(QString), this + // is in case the connected object doesn't refresh its ui when the progress + // message is updated but only if the progress value is (e.g. QProgressDialog) + emit progress(0); + if (d->Canceled) + { + return false; + } + + d->JobResponseSets.clear(); + + // initSCU + if (!this->initializeSCU()) + { + return false; + } + + // Clear the query + d->QueryDcmDataset->clear(); + + // Insert all keys that we like to receive values for + d->QueryDcmDataset->insertEmptyElement(DCM_PatientID); + d->QueryDcmDataset->insertEmptyElement(DCM_PatientName); + d->QueryDcmDataset->insertEmptyElement(DCM_PatientBirthDate); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyID); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyInstanceUID); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyDescription); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyDate); + d->QueryDcmDataset->insertEmptyElement(DCM_StudyTime); + d->QueryDcmDataset->insertEmptyElement(DCM_ModalitiesInStudy); + d->QueryDcmDataset->insertEmptyElement(DCM_AccessionNumber); + d->QueryDcmDataset->insertEmptyElement(DCM_NumberOfStudyRelatedSeries); // Number of series in the study + + // Make clear we define our search values in ISO Latin 1 (default would be ASCII) + d->QueryDcmDataset->putAndInsertOFStringArray(DCM_SpecificCharacterSet, "ISO_IR 100"); + + d->QueryDcmDataset->putAndInsertString(DCM_QueryRetrieveLevel, "STUDY"); + + QString seriesDescription = this->applyFilters(); + if (d->Canceled) + { + return false; + } + + Uint16 presentationContext = 0; + // Check for any accepted presentation context for FIND in study root (don't care about transfer syntax) + presentationContext = d->SCU.findPresentationContextID(UID_FINDStudyRootQueryRetrieveInformationModel, ""); + if (presentationContext == 0) + { + logger.error("Failed to find acceptable presentation context"); + emit progress(tr("Failed to find acceptable presentation context")); + } + else + { + logger.debug("Found useful presentation context"); + emit progress(tr("Found useful presentation context")); + } + emit progress(40); + if (d->Canceled) + { + return false; + } + + logger.debug("Starting Studies C-FIND for Patient: " + patientID); + emit progress(tr("Starting Studies C-FIND for Patient: ") + patientID); + emit progress(50); + if (d->Canceled) + { + return false; + } + + d->QueryDcmDataset->putAndInsertString(DCM_PatientID, patientID.toStdString().c_str()); + + QSharedPointer JobResponseSet = + QSharedPointer(new ctkDICOMJobResponseSet); + JobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::QueryStudies); + JobResponseSet->setPatientID(patientID.toStdString().c_str()); + JobResponseSet->setConnectionName(d->ConnectionName); + JobResponseSet->setJobUID(d->JobUID); + + QMap datasetsMap; + + OFList responses; + OFCondition status = d->SCU.sendFINDRequest(presentationContext, d->QueryDcmDataset.data(), &responses); + if (status.good()) + { + for (OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++) + { + DcmDataset *dataset = (*it)->m_dataset; + if ( dataset != NULL ) + { + OFString studyInstanceUID; + dataset->findAndGetOFString(DCM_StudyInstanceUID, studyInstanceUID); + datasetsMap.insert(studyInstanceUID.c_str(), dataset); + } + } + + JobResponseSet->setDatasets(datasetsMap); + d->JobResponseSets.append(JobResponseSet); + + logger.debug("Find succeeded on Study level for Patient: " + patientID); + emit progress(tr("Find succeeded on Study level for Patient: ") + patientID); + } + else + { + logger.error("Find on Study level failed for Patient: " + patientID); + emit progress(tr("Find on Study level failed for Patient: ") + patientID); + } + + emit progress(100); + if (d->Canceled) + { + return false; + } + + d->SCU.releaseAssociation(); + return true; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMQuery::querySeries(const QString& patientID, + const QString& studyInstanceUID) +{ + Q_D(ctkDICOMQuery); + + // In the following, we emit progress(int) after progress(QString), this + // is in case the connected object doesn't refresh its ui when the progress + // message is updated but only if the progress value is (e.g. QProgressDialog) + emit progress(0); + if (d->Canceled) + { + return false; + } + + d->JobResponseSets.clear(); + + // initSCU + if (!this->initializeSCU()) + { + return false; + } + + // Insert all keys that we like to receive values for + d->QueryDcmDataset->clear(); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesNumber); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesDescription); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesInstanceUID); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesDate); + d->QueryDcmDataset->insertEmptyElement(DCM_SeriesTime); + d->QueryDcmDataset->insertEmptyElement(DCM_Modality); + d->QueryDcmDataset->insertEmptyElement(DCM_NumberOfSeriesRelatedInstances); // Number of images in the series + + QString seriesDescription = this->applyFilters(); + if (d->Canceled) + { + return false; + } + + /* Add user-defined filters */ + d->QueryDcmDataset->putAndInsertOFStringArray(DCM_SeriesDescription, seriesDescription.toLatin1().data()); + + // Now search each within each Study that was identified + d->QueryDcmDataset->putAndInsertString(DCM_QueryRetrieveLevel, "SERIES"); + + Uint16 presentationContext = 0; + // Check for any accepted presentation context for FIND in study root (don't care about transfer syntax) + presentationContext = d->SCU.findPresentationContextID(UID_FINDStudyRootQueryRetrieveInformationModel, ""); + if (presentationContext == 0) + { + logger.error("Failed to find acceptable presentation context"); + emit progress(tr("Failed to find acceptable presentation context")); + } + else + { + logger.debug("Found useful presentation context"); + emit progress(tr("Found useful presentation context")); + } + emit progress(40); + if (d->Canceled) + { + return false; + } + + logger.debug("Starting Series C-FIND for Study: " + studyInstanceUID); + emit progress(tr("Starting Series C-FIND for Study: ") + studyInstanceUID); + emit progress(50); + if (d->Canceled) + { + return false; + } + + d->QueryDcmDataset->putAndInsertString(DCM_PatientID, patientID.toStdString().c_str()); + d->QueryDcmDataset->putAndInsertString(DCM_StudyInstanceUID, studyInstanceUID.toStdString().c_str()); + + QSharedPointer JobResponseSet = + QSharedPointer(new ctkDICOMJobResponseSet); + JobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::QuerySeries); + JobResponseSet->setPatientID(patientID.toStdString().c_str()); + JobResponseSet->setStudyInstanceUID(studyInstanceUID.toStdString().c_str()); + JobResponseSet->setConnectionName(d->ConnectionName); + JobResponseSet->setJobUID(d->JobUID); + + QMap datasetsMap; + + OFList responses; + OFCondition status = d->SCU.sendFINDRequest(presentationContext, d->QueryDcmDataset.data(), &responses); + if (status.good()) + { + for (OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++) + { + DcmDataset *dataset = (*it)->m_dataset; + if ( dataset != NULL ) + { + OFString seriesInstanceUID; + dataset->findAndGetOFString(DCM_SeriesInstanceUID, seriesInstanceUID); + datasetsMap.insert(seriesInstanceUID.c_str(), dataset); + } + } + + JobResponseSet->setDatasets(datasetsMap); + d->JobResponseSets.append(JobResponseSet); + + logger.debug("Find succeeded on Series level for Study: " + studyInstanceUID); + emit progress(tr("Find succeeded on Series level for Study: ") + studyInstanceUID); + } + else + { + logger.error("Find on Series level failed for Study: " + studyInstanceUID); + emit progress(tr("Find on Series level failed for Study: ") + studyInstanceUID); + } + + emit progress(100); + if (d->Canceled) + { + return false; + } + + d->SCU.releaseAssociation(); + return true; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMQuery::queryInstances(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID) +{ + Q_D(ctkDICOMQuery); + + // In the following, we emit progress(int) after progress(QString), this + // is in case the connected object doesn't refresh its ui when the progress + // message is updated but only if the progress value is (e.g. QProgressDialog) + emit progress(0); + if (d->Canceled) + { + return false; + } + + d->JobResponseSets.clear(); + + // initSCU + if (!this->initializeSCU()) + { + return false; + } + + // Insert all keys that we like to receive values for + d->QueryDcmDataset->clear(); + d->QueryDcmDataset->insertEmptyElement(DCM_InstanceNumber); + d->QueryDcmDataset->insertEmptyElement(DCM_SOPInstanceUID); + d->QueryDcmDataset->insertEmptyElement(DCM_Rows); + d->QueryDcmDataset->insertEmptyElement(DCM_Columns); + + QString seriesDescription = this->applyFilters(); + if (d->Canceled) + { + return false; + } + + /* Add user-defined filters */ + d->QueryDcmDataset->putAndInsertOFStringArray(DCM_SeriesDescription, seriesDescription.toLatin1().data()); + + // Now search each within each Study that was identified + d->QueryDcmDataset->putAndInsertString(DCM_QueryRetrieveLevel, "IMAGE"); + + // Check for any accepted presentation context for FIND in study root (don't care about transfer syntax) + d->PresentationContext = d->SCU.findPresentationContextID(UID_FINDStudyRootQueryRetrieveInformationModel, ""); + if (d->PresentationContext == 0) + { + logger.error("Failed to find acceptable presentation context"); + emit progress(tr("Failed to find acceptable presentation context")); + } + else + { + logger.debug("Found useful presentation context"); + emit progress(tr("Found useful presentation context")); + } + emit progress(40); + if (d->Canceled) + { + return false; + } + + logger.debug("Starting Instances C-FIND for Series: " + seriesInstanceUID); + emit progress(tr("Starting Instances C-FIND for Series: ") + seriesInstanceUID); + emit progress(50); + if (d->Canceled) + { + return false; + } + + d->QueryDcmDataset->putAndInsertString(DCM_PatientID, patientID.toStdString().c_str()); + d->QueryDcmDataset->putAndInsertString(DCM_StudyInstanceUID, studyInstanceUID.toStdString().c_str()); + d->QueryDcmDataset->putAndInsertString(DCM_SeriesInstanceUID, seriesInstanceUID.toStdString().c_str()); + + QSharedPointer JobResponseSet = + QSharedPointer(new ctkDICOMJobResponseSet); + JobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::QueryInstances); + JobResponseSet->setPatientID(patientID.toStdString().c_str()); + JobResponseSet->setStudyInstanceUID(studyInstanceUID.toStdString().c_str()); + JobResponseSet->setSeriesInstanceUID(seriesInstanceUID.toStdString().c_str()); + JobResponseSet->setConnectionName(d->ConnectionName); + JobResponseSet->setJobUID(d->JobUID); + + QMap datasetsMap; + + OFList responses; + OFCondition status = d->SCU.sendFINDRequest(d->PresentationContext, d->QueryDcmDataset.data(), &responses); + if (status.good()) + { + for (OFListIterator(QRResponse*) it = responses.begin(); it != responses.end(); it++) + { + DcmItem *dataset = (*it)->m_dataset; + if (dataset != NULL) + { + OFString SOPInstanceUID; + dataset->findAndGetOFString(DCM_SOPInstanceUID, SOPInstanceUID); + datasetsMap.insert(SOPInstanceUID.c_str(), dataset); + } + } + logger.debug("Find succeeded on Series level for Series: " + seriesInstanceUID); + emit progress(tr("Find succeeded on Series level for Series: ") + seriesInstanceUID); + } + else + { + logger.error("Find on Series level failed for Series: " + seriesInstanceUID); + emit progress(tr("Find on Series level failed for Series: ") + seriesInstanceUID); + } + + JobResponseSet->setDatasets(datasetsMap); + d->JobResponseSets.append(JobResponseSet); + + emit progress(100); + if (d->Canceled) + { + return false; + } + + d->SCU.releaseAssociation(); + return true; +} + +//---------------------------------------------------------------------------- +void ctkDICOMQuery::cancel() +{ + Q_D(ctkDICOMQuery); + d->Canceled = true; + + if (d->PresentationContext != 0) + { + d->SCU.sendCANCELRequest(d->PresentationContext); + d->PresentationContext = 0; + } +} + +//---------------------------------------------------------------------------- +bool ctkDICOMQuery::initializeSCU() +{ + Q_D(ctkDICOMQuery); + + d->SCU.setAETitle(OFString(this->callingAETitle().toStdString().c_str())); + d->SCU.setPeerAETitle(OFString(this->calledAETitle().toStdString().c_str())); + d->SCU.setPeerHostName(OFString(this->host().toStdString().c_str())); + d->SCU.setPeerPort(this->port()); + + logger.debug("Setting Transfer Syntaxes"); + emit progress(tr("Setting Transfer Syntaxes")); + emit progress(10); + if (d->Canceled) + { + return false; + } + + OFList transferSyntaxes; + transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + transferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); + transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); + + d->SCU.addPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel, transferSyntaxes); + if (!d->SCU.initNetwork().good()) + { + logger.error("Error initializing the network"); + emit progress(tr("Error initializing the network")); + emit progress(100); + return false; + } + logger.debug("Negotiating Association"); + emit progress(tr("Negotiating Association")); + emit progress(20); + if (d->Canceled) + { + return false; + } + + OFCondition result = d->SCU.negotiateAssociation(); + if (result.bad()) + { + logger.error("Error negotiating the association: " + QString(result.text())); + emit progress(tr("Error negotiating the association")); + emit progress(100); + return false; + } + + return true; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMQuery::applyFilters() +{ + Q_D(ctkDICOMQuery); + + /* Now, for all keys that the user provided for filtering on STUDY level, + * overwrite empty keys with value. For now, only Patient's Name, Patient ID, + * Study Description, Modalities in Study, and Study Date are used. + */ + QString seriesDescription; + foreach(QString key, d->Filters.keys()) + { + if ( key == QString("Name") && !d->Filters[key].toString().isEmpty()) + { + // make the filter a wildcard in dicom style + d->QueryDcmDataset->putAndInsertString( DCM_PatientName, + (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data()); + } + else if ( key == QString("Study") && !d->Filters[key].toString().isEmpty()) + { + // make the filter a wildcard in dicom style + d->QueryDcmDataset->putAndInsertString( DCM_StudyDescription, + (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data()); + } + else if ( key == QString("ID") && !d->Filters[key].toString().isEmpty()) + { + // make the filter a wildcard in dicom style + d->QueryDcmDataset->putAndInsertString( DCM_PatientID, + (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data()); + } + else if (key == QString("AccessionNumber") && !d->Filters[key].toString().isEmpty()) + { + // make the filter a wildcard in dicom style + d->QueryDcmDataset->putAndInsertString(DCM_AccessionNumber, + (QString("*") + d->Filters[key].toString() + QString("*")).toLatin1().data()); + } + else if ( key == QString("Modalities") && !d->Filters[key].toString().isEmpty()) + { + // make the filter be an "OR" of modalities using backslash (dicom-style) + QString modalitySearch(""); + foreach (const QString& modality, d->Filters[key].toStringList()) + { + modalitySearch += modality + QString("\\"); + } + modalitySearch.chop(1); // remove final backslash + logger.debug("modalityInStudySearch " + modalitySearch); + d->QueryDcmDataset->putAndInsertString( DCM_ModalitiesInStudy, modalitySearch.toLatin1().data() ); + } + // Remember Series Description for later series query if we go through the keys now + else if ( key == QString("Series") && !d->Filters[key].toString().isEmpty()) + { + // make the filter a wildcard in dicom style + seriesDescription = "*" + d->Filters[key].toString() + "*"; + } + else + { + logger.debug("Ignoring unknown search key: " + key); + } + } + + if ( d->Filters.keys().contains("StartDate") && d->Filters.keys().contains("EndDate") ) + { + QString dateRange = d->Filters["StartDate"].toString() + + QString("-") + + d->Filters["EndDate"].toString(); + d->QueryDcmDataset->putAndInsertString ( DCM_StudyDate, dateRange.toLatin1().data() ); + logger.debug("Query on study date " + dateRange); + } + + emit progress(30); + + return seriesDescription; } diff --git a/Libs/DICOM/Core/ctkDICOMQuery.h b/Libs/DICOM/Core/ctkDICOMQuery.h index 652a3c4708..8096400861 100644 --- a/Libs/DICOM/Core/ctkDICOMQuery.h +++ b/Libs/DICOM/Core/ctkDICOMQuery.h @@ -25,60 +25,79 @@ #include #include #include -#include -// CTK includes +// ctkCore includes #include +// ctkDICOMCore includes #include "ctkDICOMCoreExport.h" #include "ctkDICOMDatabase.h" - class ctkDICOMQueryPrivate; +class ctkDICOMJobResponseSet; /// \ingroup DICOM_Core class CTK_DICOM_CORE_EXPORT ctkDICOMQuery : public QObject { Q_OBJECT + Q_PROPERTY(QString connectionName READ connectionName WRITE setConnectionName); Q_PROPERTY(QString callingAETitle READ callingAETitle WRITE setCallingAETitle); Q_PROPERTY(QString calledAETitle READ calledAETitle WRITE setCalledAETitle); Q_PROPERTY(QString host READ host WRITE setHost); Q_PROPERTY(int port READ port WRITE setPort); - Q_PROPERTY(bool preferCGET READ preferCGET WRITE setPreferCGET); - Q_PROPERTY(QList< QPair > studyAndSeriesInstanceUIDQueried READ studyAndSeriesInstanceUIDQueried); - Q_PROPERTY(QMap filters READ filters WRITE setFilters); + Q_PROPERTY(int connectionTimeout READ connectionTimeout WRITE setConnectionTimeout); + Q_PROPERTY(int maximumPatientsQuery READ maximumPatientsQuery WRITE setMaximumPatientsQuery); + Q_PROPERTY(QList> studyAndSeriesInstanceUIDQueried READ studyAndSeriesInstanceUIDQueried); + Q_PROPERTY(QString jobUID READ jobUID WRITE setJobUID); public: explicit ctkDICOMQuery(QObject* parent = 0); virtual ~ctkDICOMQuery(); - /// Set methods for connectivity + ///@{ + /// Name identifying the server + void setConnectionName(const QString& connectionName); + QString connectionName() const; + ///@} + + ///@{ + /// Methods for connectivity. /// Empty by default - void setCallingAETitle ( const QString& callingAETitle ); + void setCallingAETitle(const QString& callingAETitle); QString callingAETitle()const; + + void setCalledAETitle(const QString& calledAETitle); + QString calledAETitle() const; + ///@} + + ///@{ + /// Peer hostname being connected to /// Empty by default - void setCalledAETitle ( const QString& calledAETitle ); - QString calledAETitle()const; - /// Empty by default - void setHost ( const QString& host ); - QString host()const; + void setHost(const QString& host); + QString host() const; + ///@} + + ///@{ /// Specify a port for the packet headers. /// \a port ranges from 0 to 65535. /// 0 by default. - void setPort ( int port ); - int port()const; - /// Prefer CGET over CMOVE for retrieval of query results - /// false by default - void setPreferCGET ( bool preferCGET ); - bool preferCGET()const; - - /// Query a remote DICOM Image Store SCP - /// You must at least set the host and port before calling query() - Q_INVOKABLE bool query(ctkDICOMDatabase& database); - - /// Access the list of study and series instance UIDs from the last query - QList< QPair > studyAndSeriesInstanceUIDQueried()const; - - /// + void setPort(int port); + int port() const; + ///@} + + ///@{ + /// connection timeout, default 10 sec. + void setConnectionTimeout(int timeout); + int connectionTimeout() const; + ///@} + + ///@{ + /// maximum number of responses allowed in one query + /// when query is at Patient level. Default is 25. + void setMaximumPatientsQuery(int maximumPatientsQuery); + int maximumPatientsQuery(); + ///@} + + ///@{ /// Filters are keyword/value pairs as generated by /// the ctkDICOMWidgets in a human readable (and editable) /// format. The Query is responsible for converting these @@ -87,7 +106,7 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMQuery : public QObject /// StartDate and EndDate /// Key DICOM Tag Type Example /// ----------------------------------------------------------- - /// Name DCM_PatientName QString JOHNDOE + /// Name DCM_PatientName QString JOHNDOE /// Study DCM_StudyDescription QString /// Series DCM_SeriesDescription QString /// ID DCM_PatientID QString @@ -95,8 +114,53 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMQuery : public QObject /// StartDate DCM_StudyDate QString 20090101 /// EndDate DCM_StudyDate QString 20091231 /// No filter (empty) by default. - void setFilters(const QMap&); - QMap filters()const; + Q_INVOKABLE void setFilters(const QMap&); + Q_INVOKABLE QMap filters()const; + ///@} + + /// operation is canceled? + Q_INVOKABLE bool wasCanceled(); + + /// Query a remote DICOM Image Store SCP. + /// You must at least set the host and port before calling query() + Q_INVOKABLE bool query(ctkDICOMDatabase& database); + + /// Access the list of study and series instance UIDs from the last query method. + QList> studyAndSeriesInstanceUIDQueried()const; + + /// Utility method to query a remote DICOM Image Store SCP only at patient level. + /// You must at least set the host and port before calling query(). + /// Results will be stored in the ctkDICOMJobResponseSet list (jobResponseSets). + Q_INVOKABLE bool queryPatients(); + + /// Utility method to query a remote DICOM Image Store SCP only at study level. + /// You must at least set the host and port before calling query(). + /// Results will be stored in the ctkDICOMJobResponseSet list (jobResponseSets). + Q_INVOKABLE bool queryStudies(const QString& patientID); + + /// Utility method to query a remote DICOM Image Store SCP only at series level + /// given a studyInstanceUID. + /// You must at least set the host and port before calling query(). + /// Results will be stored in the ctkDICOMJobResponseSet list (jobResponseSets). + Q_INVOKABLE bool querySeries(const QString& patientID, + const QString& studyInstanceUID); + + /// Utility method to query a remote DICOM Image Store SCP only at instance level + /// given a studyInstanceUID and seriesInstanceUID. + /// You must at least set the host and port before calling query(). + /// Results will be stored in the ctkDICOMJobResponseSet list (jobResponseSets). + Q_INVOKABLE bool queryInstances(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID); + + ///@{ + /// Access the list of datasets from the last queryPatients, queryStudies, + /// querySeries and queryInstances methods. + Q_INVOKABLE QList jobResponseSets() const; + QList> jobResponseSetsShared() const; + void setJobUID(const QString& jobUID); + QString jobUID() const; + ///@} Q_SIGNALS: /// Signal is emitted inside the query() function. It ranges from 0 to 100. @@ -118,6 +182,9 @@ public Q_SLOTS: void cancel(); protected: + QString applyFilters(); + bool initializeSCU(); + QScopedPointer d_ptr; private: diff --git a/Libs/DICOM/Core/ctkDICOMQueryJob.cpp b/Libs/DICOM/Core/ctkDICOMQueryJob.cpp new file mode 100644 index 0000000000..1755be4836 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryJob.cpp @@ -0,0 +1,216 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMJobResponseSet.h" // For ctkDICOMJobDetail +#include "ctkDICOMQueryJob_p.h" +#include "ctkDICOMQueryWorker.h" +#include "ctkDICOMServer.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMQueryJob"); + +//------------------------------------------------------------------------------ +// ctkDICOMQueryJobPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMQueryJobPrivate::ctkDICOMQueryJobPrivate(ctkDICOMQueryJob* object) + : q_ptr(object) +{ + this->Server = nullptr; + this->MaximumPatientsQuery = 25; +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryJobPrivate::~ctkDICOMQueryJobPrivate() = default; + +//------------------------------------------------------------------------------ +// ctkDICOMQueryJob methods + +//------------------------------------------------------------------------------ +ctkDICOMQueryJob::ctkDICOMQueryJob() + : d_ptr(new ctkDICOMQueryJobPrivate(this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryJob::ctkDICOMQueryJob(ctkDICOMQueryJobPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryJob::~ctkDICOMQueryJob() = default; + +//---------------------------------------------------------------------------- +void ctkDICOMQueryJob::setFilters(const QMap& filters) +{ + Q_D(ctkDICOMQueryJob); + d->Filters = filters; +} + +//---------------------------------------------------------------------------- +QMap ctkDICOMQueryJob::filters() const +{ + Q_D(const ctkDICOMQueryJob); + return d->Filters; +} + +//------------------------------------------------------------------------------ +void ctkDICOMQueryJob::setMaximumPatientsQuery(int maximumPatientsQuery) +{ + Q_D(ctkDICOMQueryJob); + d->MaximumPatientsQuery = maximumPatientsQuery; +} + +//------------------------------------------------------------------------------ +int ctkDICOMQueryJob::maximumPatientsQuery() +{ + Q_D(const ctkDICOMQueryJob); + return d->MaximumPatientsQuery; +} + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMQueryJob::server() const +{ + Q_D(const ctkDICOMQueryJob); + return d->Server.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMQueryJob::serverShared() const +{ + Q_D(const ctkDICOMQueryJob); + return d->Server; +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryJob::setServer(ctkDICOMServer& server) +{ + Q_D(ctkDICOMQueryJob); + d->Server = QSharedPointer(&server, skipDelete); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryJob::setServer(QSharedPointer server) +{ + Q_D(ctkDICOMQueryJob); + d->Server = server; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMQueryJob::loggerReport(const QString& status) const +{ + switch (this->dicomLevel()) + { + case ctkDICOMJob::DICOMLevels::Patients: + return QString("ctkDICOMQueryJob: query job at patients level %1.\n" + "JobUID: %2\n" + "Server: %3\n") + .arg(status) + .arg(this->jobUID()) + .arg(this->server()->connectionName()); + case ctkDICOMJob::DICOMLevels::Studies: + return QString("ctkDICOMQueryJob: query job at studies level %1.\n" + "JobUID: %2\n" + "Server: %3\n" + "PatientID: %4\n") + .arg(status) + .arg(this->jobUID()) + .arg(this->server()->connectionName()) + .arg(this->patientID()); + case ctkDICOMJob::DICOMLevels::Series: + return QString("ctkDICOMQueryJob: query job at series level %1.\n" + "JobUID: %2\n" + "Server: %3\n" + "PatientID: %4\n" + "StudyInstanceUID: %5\n") + .arg(status) + .arg(this->jobUID()) + .arg(this->server()->connectionName()) + .arg(this->patientID()) + .arg(this->studyInstanceUID()); + case ctkDICOMJob::DICOMLevels::Instances: + return QString("ctkDICOMQueryJob: query job at instances level %1.\n" + "JobUID: %2\n" + "Server: %3\n" + "PatientID: %4\n" + "StudyInstanceUID: %5\n" + "SeriesInstanceUID: %6\n") + .arg(status) + .arg(this->jobUID()) + .arg(this->server()->connectionName()) + .arg(this->patientID()) + .arg(this->studyInstanceUID()) + .arg(this->seriesInstanceUID()); + default: + return QString(""); + } +} + +//------------------------------------------------------------------------------ +ctkAbstractJob* ctkDICOMQueryJob::clone() const +{ + ctkDICOMQueryJob* newQueryJob = new ctkDICOMQueryJob; + newQueryJob->setMaximumPatientsQuery(this->maximumConcurrentJobsPerType()); + newQueryJob->setServer(this->serverShared()); + newQueryJob->setFilters(this->filters()); + newQueryJob->setDICOMLevel(this->dicomLevel()); + newQueryJob->setPatientID(this->patientID()); + newQueryJob->setStudyInstanceUID(this->studyInstanceUID()); + newQueryJob->setSeriesInstanceUID(this->seriesInstanceUID()); + newQueryJob->setSOPInstanceUID(this->sopInstanceUID()); + newQueryJob->setMaximumNumberOfRetry(this->maximumNumberOfRetry()); + newQueryJob->setRetryDelay(this->retryDelay()); + newQueryJob->setRetryCounter(this->retryCounter()); + newQueryJob->setIsPersistent(this->isPersistent()); + newQueryJob->setMaximumConcurrentJobsPerType(this->maximumConcurrentJobsPerType()); + newQueryJob->setPriority(this->priority()); + + return newQueryJob; +} + +//------------------------------------------------------------------------------ +ctkAbstractWorker* ctkDICOMQueryJob::createWorker() +{ + ctkDICOMQueryWorker* worker = + new ctkDICOMQueryWorker; + worker->setJob(*this); + return worker; +} + +//------------------------------------------------------------------------------ +QVariant ctkDICOMQueryJob::toVariant() +{ + return QVariant::fromValue(ctkDICOMJobDetail(*this, this->server()->connectionName())); +} diff --git a/Libs/DICOM/Core/ctkDICOMQueryJob.h b/Libs/DICOM/Core/ctkDICOMQueryJob.h new file mode 100644 index 0000000000..a709bb3988 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryJob.h @@ -0,0 +1,120 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMQueryJob_h +#define __ctkDICOMQueryJob_h + +// Qt includes +#include +#include +#include +#include + +// ctkCore includes +class ctkAbstractWorker; + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMJob.h" +class ctkDICOMQueryJobPrivate; +class ctkDICOMServer; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMQueryJob : public ctkDICOMJob +{ + Q_OBJECT + Q_PROPERTY(int maximumPatientsQuery READ maximumPatientsQuery WRITE setMaximumPatientsQuery); + +public: + typedef ctkDICOMJob Superclass; + explicit ctkDICOMQueryJob(); + virtual ~ctkDICOMQueryJob(); + + ///@{ + /// Filters are keyword/value pairs as generated by + /// the ctkDICOMWidgets in a human readable (and editable) + /// format. The Query is responsible for converting these + /// into the appropriate dicom syntax for the C-Find + /// Currently supports the keys: Name, Study, Series, ID, Modalities, + /// StartDate and EndDate + /// Key DICOM Tag Type Example + /// ----------------------------------------------------------- + /// Name DCM_PatientName QString JOHNDOE + /// Study DCM_StudyDescription QString + /// Series DCM_SeriesDescription QString + /// ID DCM_PatientID QString + /// Modalities DCM_ModalitiesInStudy QStringList CT, MR, MN + /// StartDate DCM_StudyDate QString 20090101 + /// EndDate DCM_StudyDate QString 20091231 + /// No filter (empty) by default. + Q_INVOKABLE void setFilters(const QMap& filters); + Q_INVOKABLE QMap filters() const; + ///@} + + ///@{ + /// maximum number of responses allowed in one query + /// when query is at Patient level. Default is 25. + void setMaximumPatientsQuery(int maximumPatientsQuery); + int maximumPatientsQuery(); + ///@} + + ///@{ + /// Server + Q_INVOKABLE ctkDICOMServer* server() const; + QSharedPointer serverShared() const; + Q_INVOKABLE void setServer(ctkDICOMServer& server); + void setServer(QSharedPointer server); + ///@} + + /// Logger report string formatting for specific task + Q_INVOKABLE QString loggerReport(const QString& status) const override; + + /// \see ctkAbstractJob::clone() + Q_INVOKABLE ctkAbstractJob* clone() const override; + + /// Generate worker for job + Q_INVOKABLE ctkAbstractWorker* createWorker() override; + + /// Return the QVariant value of this job. + /// + /// The value is set using the ctkDICOMJobDetail metatype and is used to pass + /// information between threads using Qt signals. + /// \sa ctkDICOMJobDetail + Q_INVOKABLE virtual QVariant toVariant() override; + +protected: + QScopedPointer d_ptr; + + /// Constructor allowing derived class to specify a specialized pimpl. + /// + /// \note You are responsible to call init() in the constructor of + /// derived class. Doing so ensures the derived class is fully + /// instantiated in case virtual method are called within init() itself. + ctkDICOMQueryJob(ctkDICOMQueryJobPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMQueryJob); + Q_DISABLE_COPY(ctkDICOMQueryJob); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMQueryJob_p.h b/Libs/DICOM/Core/ctkDICOMQueryJob_p.h new file mode 100644 index 0000000000..8a34f04f40 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryJob_p.h @@ -0,0 +1,53 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMQueryJobPrivate_h +#define __ctkDICOMQueryJobPrivate_h + +// Qt includes +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMQueryJob.h" + +//------------------------------------------------------------------------------ +class ctkDICOMQueryJobPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMQueryJob) + +protected: + ctkDICOMQueryJob* const q_ptr; + +public: + ctkDICOMQueryJobPrivate(ctkDICOMQueryJob* object); + virtual ~ctkDICOMQueryJobPrivate(); + + QSharedPointer Server; + QMap Filters; + int MaximumPatientsQuery; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMQueryWorker.cpp b/Libs/DICOM/Core/ctkDICOMQueryWorker.cpp new file mode 100644 index 0000000000..3c7aa721a7 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryWorker.cpp @@ -0,0 +1,218 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMQueryWorker_p.h" +#include "ctkDICOMQueryJob.h" +#include "ctkDICOMScheduler.h" +#include "ctkDICOMServer.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMQueryWorker"); + +//------------------------------------------------------------------------------ +// ctkDICOMQueryWorkerPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMQueryWorkerPrivate::ctkDICOMQueryWorkerPrivate(ctkDICOMQueryWorker* object) + : q_ptr(object) +{ + this->Query = QSharedPointer(new ctkDICOMQuery); +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryWorkerPrivate::~ctkDICOMQueryWorkerPrivate() = default; + +//------------------------------------------------------------------------------ +void ctkDICOMQueryWorkerPrivate::setQueryParameters() +{ + Q_Q(ctkDICOMQueryWorker); + + QSharedPointer queryJob = + qSharedPointerObjectCast(q->Job); + if (!queryJob) + { + return; + } + + ctkDICOMServer* server = queryJob->server(); + if (!server) + { + return; + } + + this->Query->setConnectionName(server->connectionName()); + this->Query->setCallingAETitle(server->callingAETitle()); + this->Query->setCalledAETitle(server->calledAETitle()); + this->Query->setHost(server->host()); + this->Query->setPort(server->port()); + this->Query->setConnectionTimeout(server->connectionTimeout()); + this->Query->setJobUID(queryJob->jobUID()); + this->Query->setFilters(queryJob->filters()); +} + +//------------------------------------------------------------------------------ +// ctkDICOMQueryWorker methods + +//------------------------------------------------------------------------------ +ctkDICOMQueryWorker::ctkDICOMQueryWorker() + : d_ptr(new ctkDICOMQueryWorkerPrivate(this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryWorker::ctkDICOMQueryWorker(ctkDICOMQueryWorkerPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryWorker::~ctkDICOMQueryWorker() = default; + +//---------------------------------------------------------------------------- +void ctkDICOMQueryWorker::cancel() +{ + Q_D(const ctkDICOMQueryWorker); + d->Query->cancel(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryWorker::run() +{ + Q_D(const ctkDICOMQueryWorker); + QSharedPointer queryJob = + qSharedPointerObjectCast(this->Job); + if (!queryJob) + { + return; + } + + QSharedPointer scheduler = + qSharedPointerObjectCast(this->Scheduler); + if (!scheduler) + { + emit queryJob->canceled(); + this->onJobCanceled(); + queryJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + + if (queryJob->status() == ctkAbstractJob::JobStatus::Stopped) + { + emit queryJob->canceled(); + this->onJobCanceled(); + queryJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + + queryJob->setStatus(ctkAbstractJob::JobStatus::Running); + emit queryJob->started(); + + logger.debug(QString("ctkDICOMQueryWorker : running job %1 in thread %2.\n") + .arg(queryJob->jobUID()) + .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); + + switch (queryJob->dicomLevel()) + { + case ctkDICOMJob::DICOMLevels::Patients: + if (!d->Query->queryPatients()) + { + emit queryJob->canceled(); + this->onJobCanceled(); + queryJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + case ctkDICOMJob::DICOMLevels::Studies: + if (!d->Query->queryStudies(queryJob->patientID())) + { + emit queryJob->canceled(); + this->onJobCanceled(); + queryJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + case ctkDICOMJob::DICOMLevels::Series: + if (!d->Query->querySeries(queryJob->patientID(), + queryJob->studyInstanceUID())) + { + emit queryJob->canceled(); + this->onJobCanceled(); + queryJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + case ctkDICOMJob::DICOMLevels::Instances: + if (!d->Query->queryInstances(queryJob->patientID(), + queryJob->studyInstanceUID(), + queryJob->seriesInstanceUID())) + { + emit queryJob->canceled(); + this->onJobCanceled(); + queryJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + } + + if (d->Query->jobResponseSetsShared().count() > 0 && + queryJob->status() != ctkAbstractJob::JobStatus::Stopped) + { + scheduler->insertJobResponseSets(d->Query->jobResponseSetsShared()); + } + + queryJob->setStatus(ctkAbstractJob::JobStatus::Finished); + emit queryJob->finished(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryWorker::setJob(QSharedPointer job) +{ + Q_D(ctkDICOMQueryWorker); + + QSharedPointer queryJob = + qSharedPointerObjectCast(job); + if (!queryJob) + { + return; + } + + this->Superclass::setJob(job); + d->setQueryParameters(); +} + +//------------------------------------------------------------------------------ +QSharedPointer ctkDICOMQueryWorker::querierShared() const +{ + Q_D(const ctkDICOMQueryWorker); + return d->Query; +} + +//------------------------------------------------------------------------------ +ctkDICOMQuery* ctkDICOMQueryWorker::querier() const +{ + Q_D(const ctkDICOMQueryWorker); + return d->Query.data(); +} diff --git a/Libs/DICOM/Core/ctkDICOMQueryWorker.h b/Libs/DICOM/Core/ctkDICOMQueryWorker.h new file mode 100644 index 0000000000..b49f9a7c03 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryWorker.h @@ -0,0 +1,79 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMQueryWorker_h +#define __ctkDICOMQueryWorker_h + +// Qt includes +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkAbstractWorker.h" +class ctkDICOMQuery; +class ctkDICOMQueryWorkerPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMQueryWorker : public ctkAbstractWorker +{ + Q_OBJECT + +public: + typedef ctkAbstractWorker Superclass; + explicit ctkDICOMQueryWorker(); + virtual ~ctkDICOMQueryWorker(); + + /// Execute worker + void run() override; + + /// Cancel worker + void cancel() override; + + /// Job + void setJob(QSharedPointer job) override; + using ctkAbstractWorker::setJob; + + ///@{ + /// Querier + QSharedPointer querierShared() const; + Q_INVOKABLE ctkDICOMQuery* querier() const; + ///@} + +protected: + QScopedPointer d_ptr; + + /// Constructor allowing derived class to specify a specialized pimpl. + /// + /// \note You are responsible to call init() in the constructor of + /// derived class. Doing so ensures the derived class is fully + /// instantiated in case virtual method are called within init() itself. + ctkDICOMQueryWorker(ctkDICOMQueryWorkerPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMQueryWorker); + Q_DISABLE_COPY(ctkDICOMQueryWorker); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMQueryWorker_p.h b/Libs/DICOM/Core/ctkDICOMQueryWorker_p.h new file mode 100644 index 0000000000..cba54cac5a --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryWorker_p.h @@ -0,0 +1,52 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMQueryWorkerPrivate_h +#define __ctkDICOMQueryWorkerPrivate_h + +// Qt includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMQuery.h" +#include "ctkDICOMQueryWorker.h" + +//------------------------------------------------------------------------------ +class ctkDICOMQueryWorkerPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMQueryWorker) + +protected: + ctkDICOMQueryWorker* const q_ptr; + +public: + ctkDICOMQueryWorkerPrivate(ctkDICOMQueryWorker* object); + virtual ~ctkDICOMQueryWorkerPrivate(); + + void setQueryParameters(); + + QSharedPointer Query; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMRetrieve.cpp b/Libs/DICOM/Core/ctkDICOMRetrieve.cpp index f2a0ebc497..db1646892b 100644 --- a/Libs/DICOM/Core/ctkDICOMRetrieve.cpp +++ b/Libs/DICOM/Core/ctkDICOMRetrieve.cpp @@ -1,4 +1,4 @@ -/*========================================================================= +/*========================================================================= Library: CTK @@ -24,29 +24,27 @@ // ctkDICOMCore includes #include "ctkDICOMRetrieve.h" +#include "ctkErrorLogLevel.h" #include "ctkLogger.h" +#include "ctkDICOMJobResponseSet.h" // DCMTK includes -#include "dcmtk/dcmnet/dimse.h" -#include "dcmtk/dcmnet/diutil.h" -#include "dcmtk/dcmnet/scu.h" - -#include +#include +#include +#include #include #include #include +#include #include #include #include /* for class OFStandard */ #include /* for class DicomDirInterface */ - #include /* for dcmjpeg decoders */ #include /* for dcmjpeg encoders */ #include /* for DcmRLEDecoderRegistration */ #include /* for DcmRLEEncoderRegistration */ -#include "dcmtk/oflog/oflog.h" - static ctkLogger logger("org.commontk.dicom.DICOMRetrieve"); //------------------------------------------------------------------------------ @@ -68,14 +66,13 @@ class ctkDICOMRetrieveSCUPrivate : public DcmSCU RetrieveResponse *response, OFBool &waitForNextResponse) { - if (this->retrieve) + if (this->retrieve && !this->retrieve->wasCanceled()) { emit this->retrieve->progress(ctkDICOMRetrieve::tr("Got move request")); emit this->retrieve->progress(0); return this->DcmSCU::handleMOVEResponse( - presID, response, waitForNextResponse); + presID, response, waitForNextResponse); } - //return false; return EC_IllegalCall; }; @@ -86,30 +83,65 @@ class ctkDICOMRetrieveSCUPrivate : public DcmSCU OFBool& continueCGETSession, Uint16& cStoreReturnStatus) { - if (this->retrieve) + if (!this->retrieve || this->retrieve->wasCanceled()) { - OFString instanceUID; - incomingObject->findAndGetOFString(DCM_SOPInstanceUID, instanceUID); - QString qInstanceUID(instanceUID.c_str()); - emit this->retrieve->progress( - //: %1 is an instance UID - ctkDICOMRetrieve::tr("Got STORE request for %1").arg(qInstanceUID) - ); - emit this->retrieve->progress(0); - continueCGETSession = !this->retrieve->wasCanceled(); - if (this->retrieve && this->retrieve->database()) + return EC_IllegalCall; + } + + OFString instanceUID; + incomingObject->findAndGetOFString(DCM_SOPInstanceUID, instanceUID); + QString qInstanceUID(instanceUID.c_str()); + emit this->retrieve->progress( + //: %1 is an instance UID + ctkDICOMRetrieve::tr("Got STORE request for %1").arg(qInstanceUID) + ); + emit this->retrieve->progress(0); + if (!this->retrieve->jobUID().isEmpty()) + { + QSharedPointer jobResponseSet = + QSharedPointer(new ctkDICOMJobResponseSet); + if (this->retrieve->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSOPInstance) + { + jobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance); + } + else if (this->retrieve->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSeries) + { + jobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::RetrieveSeries); + } + else if (this->retrieve->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveStudy) { - this->retrieve->database()->insert(incomingObject); - return EC_Normal; + jobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::RetrieveStudy); } - else + jobResponseSet->setPatientID(this->retrieve->patientID()); + jobResponseSet->setStudyInstanceUID(this->retrieve->studyInstanceUID()); + jobResponseSet->setSeriesInstanceUID(this->retrieve->seriesInstanceUID()); + jobResponseSet->setSOPInstanceUID(qInstanceUID); + jobResponseSet->setConnectionName(this->retrieve->connectionName()); + jobResponseSet->setDataset(incomingObject); + jobResponseSet->setJobUID(this->retrieve->jobUID()); + jobResponseSet->setCopyFile(true); + + // To Do: this should be emitted for all the RetrieveTypes, but we should change the insert in the + // ctkDICOMRetrieveWorker to happen every 10 frames (configurable). + // i.e. a slot in ctkDICOMRetrieveWorker with a counter. When the counter > batchLimit -> insert + if (this->retrieve->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSeries) { - return this->DcmSCU::handleSTORERequest( - presID, incomingObject, continueCGETSession, cStoreReturnStatus); + emit this->retrieve->progressJobDetail(jobResponseSet->toVariant()); } + + this->retrieve->addJobResponseSet(jobResponseSet); + return EC_Normal; + } + else if (this->retrieve->dicomDatabase()) + { + this->retrieve->dicomDatabase()->insert(incomingObject, true, false); + return EC_Normal; + } + else + { + return this->DcmSCU::handleSTORERequest( + presID, incomingObject, continueCGETSession, cStoreReturnStatus); } - //return false; - return EC_IllegalCall; }; // called when status information from remote server @@ -118,14 +150,12 @@ class ctkDICOMRetrieveSCUPrivate : public DcmSCU RetrieveResponse* response, OFBool& continueCGETSession) { - if (this->retrieve) + if (this->retrieve && !this->retrieve->wasCanceled()) { emit this->retrieve->progress(ctkDICOMRetrieve::tr("Got CGET response")); emit this->retrieve->progress(0); - continueCGETSession = !this->retrieve->wasCanceled(); return this->DcmSCU::handleCGETResponse(presID, response, continueCGETSession); } - //return false; return EC_IllegalCall; }; }; @@ -142,27 +172,43 @@ class ctkDICOMRetrievePrivate: public QObject public: ctkDICOMRetrievePrivate(ctkDICOMRetrieve& obj); ~ctkDICOMRetrievePrivate(); + /// Keep the currently negotiated connection to the /// peer host open unless the connection parameters change - bool WasCanceled; - bool KeepAssociationOpen; - bool ConnectionParamsChanged; - bool LastRetrieveType; + bool Canceled; + bool KeepAssociationOpen; + bool ConnectionParamsChanged; + ctkDICOMRetrieve::RetrieveType LastRetrieveType; + + QString PatientID; + QString StudyInstanceUID; + QString SeriesInstanceUID; + QString SOPInstanceUID; + QString ConnectionName; + QString JobUID; + QSharedPointer Database; - ctkDICOMRetrieveSCUPrivate SCU; + ctkDICOMRetrieveSCUPrivate SCU; + T_ASC_PresentationContextID PresentationContext; QString MoveDestinationAETitle; - // do the retrieve, handling both series and study retrieves - 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 retrieveType ); - bool get ( const QString& studyInstanceUID, - const QString& seriesInstanceUID, - const RetrieveType retrieveType ); + QList> JobResponseSets; + + bool initializeSCU(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const ctkDICOMRetrieve::RetrieveType retrieveType, + DcmDataset *retrieveParameters); + bool move(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const ctkDICOMRetrieve::RetrieveType retrieveType); + bool get(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const ctkDICOMRetrieve::RetrieveType retrieveType); }; //------------------------------------------------------------------------------ @@ -173,10 +219,16 @@ ctkDICOMRetrievePrivate::ctkDICOMRetrievePrivate(ctkDICOMRetrieve& obj) : q_ptr(&obj) { this->Database = QSharedPointer (0); - this->WasCanceled = false; + this->Canceled = false; this->KeepAssociationOpen = true; this->ConnectionParamsChanged = false; - this->LastRetrieveType = RetrieveNone; + this->LastRetrieveType = ctkDICOMRetrieve::RetrieveNone; + + this->PatientID = ""; + this->StudyInstanceUID = ""; + this->SeriesInstanceUID = ""; + this->ConnectionName = ""; + this->JobUID = ""; // Register the JPEG libraries in case we need them // (registration only happens once, so it's okay to call repeatedly) @@ -189,21 +241,25 @@ ctkDICOMRetrievePrivate::ctkDICOMRetrievePrivate(ctkDICOMRetrieve& obj) // register RLE decompression codec DcmRLEDecoderRegistration::registerCodecs(); - logger.info ( "Setting Transfer Syntaxes" ); + logger.debug("Setting Transfer Syntaxes"); OFList 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_GETStudyRootQueryRetrieveInformationModel, transferSyntaxes ); - - for (Uint16 i = 0; i < numberOfDcmLongSCUStorageSOPClassUIDs; i++) + 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_GETStudyRootQueryRetrieveInformationModel, transferSyntaxes); + + for (Uint16 index = 0; index < numberOfDcmLongSCUStorageSOPClassUIDs; index++) { - this->SCU.addPresentationContext(dcmLongSCUStorageSOPClassUIDs[i], - transferSyntaxes, ASC_SC_ROLE_SCP); + this->SCU.addPresentationContext(dcmLongSCUStorageSOPClassUIDs[index], + transferSyntaxes, ASC_SC_ROLE_SCP); } + + this->SCU.setACSETimeout(3); + this->SCU.setConnectionTimeout(3); + this->SCU.setStorageDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString().c_str()); } //------------------------------------------------------------------------------ @@ -212,21 +268,24 @@ ctkDICOMRetrievePrivate::~ctkDICOMRetrievePrivate() // At least now be kind to the server and release association if (this->SCU.isConnected()) { - this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION); + this->SCU.releaseAssociation(); } + + this->JobResponseSets.clear(); } //------------------------------------------------------------------------------ -bool ctkDICOMRetrievePrivate::initializeSCU( const QString& studyInstanceUID, - const QString& seriesInstanceUID, - const RetrieveType retrieveType, - DcmDataset *retrieveParameters) +bool ctkDICOMRetrievePrivate::initializeSCU(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const ctkDICOMRetrieve::RetrieveType retrieveType, + DcmDataset *retrieveParameters) { - // If we like to query another server than before, be sure to disconnect first if (this->SCU.isConnected() && this->ConnectionParamsChanged) { - this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION); + this->SCU.releaseAssociation(); } // Connect to server if not already connected if (!this->SCU.isConnected()) @@ -250,64 +309,116 @@ bool ctkDICOMRetrievePrivate::initializeSCU( const QString& studyInstanceUID, this->ConnectionParamsChanged = false; // Setup query about what to be received from the PACS logger.debug ( "Setting Retrieve Parameters" ); - if ( retrieveType == RetrieveSeries ) + if (retrieveType == ctkDICOMRetrieve::RetrieveSOPInstance) { - retrieveParameters->putAndInsertString ( DCM_QueryRetrieveLevel, "SERIES" ); - retrieveParameters->putAndInsertString ( DCM_SeriesInstanceUID, - seriesInstanceUID.toStdString().c_str() ); + retrieveParameters->putAndInsertString(DCM_QueryRetrieveLevel, "IMAGE"); + retrieveParameters->putAndInsertString(DCM_SOPInstanceUID, + sopInstanceUID.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()); + if (!patientID.isEmpty()) + { + retrieveParameters->putAndInsertString(DCM_PatientID, + patientID.toStdString().c_str()); + } + } + else if (retrieveType == ctkDICOMRetrieve::RetrieveSeries) + { + retrieveParameters->putAndInsertString(DCM_QueryRetrieveLevel, "SERIES"); + 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()); + if (!patientID.isEmpty()) + { + retrieveParameters->putAndInsertString(DCM_PatientID, + patientID.toStdString().c_str()); + } } else { retrieveParameters->putAndInsertString ( DCM_QueryRetrieveLevel, "STUDY" ); retrieveParameters->putAndInsertString ( DCM_StudyInstanceUID, studyInstanceUID.toStdString().c_str() ); + if (!patientID.isEmpty()) + { + retrieveParameters->putAndInsertString(DCM_PatientID, + patientID.toStdString().c_str()); + } } + + this->LastRetrieveType = retrieveType; return true; } //------------------------------------------------------------------------------ -bool ctkDICOMRetrievePrivate::move ( const QString& studyInstanceUID, - const QString& seriesInstanceUID, - const RetrieveType retrieveType ) +bool ctkDICOMRetrievePrivate::move(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const ctkDICOMRetrieve::RetrieveType retrieveType) { + Q_Q(ctkDICOMRetrieve); + + this->JobResponseSets.clear(); + this->PatientID = patientID; + this->StudyInstanceUID = studyInstanceUID; + this->SeriesInstanceUID = seriesInstanceUID; + this->SOPInstanceUID = sopInstanceUID; + + if (this->Canceled) + { + return false; + } DcmDataset *retrieveParameters = new DcmDataset(); - if (! this->initializeSCU(studyInstanceUID, seriesInstanceUID, retrieveType, retrieveParameters) ) + if (!this->initializeSCU(patientID, + studyInstanceUID, + seriesInstanceUID, + sopInstanceUID, + retrieveType, + retrieveParameters)) { delete retrieveParameters; + logger.error("MOVE Request failed: SCU initialization failed"); return false; } - // Issue request logger.debug ( "Sending Move Request" ); OFList responses; - T_ASC_PresentationContextID presID = this->SCU.findPresentationContextID( - UID_MOVEStudyRootQueryRetrieveInformationModel, - "" /* don't care about transfer syntax */ ); - if (presID == 0) + this->PresentationContext = this->SCU.findPresentationContextID( + UID_MOVEStudyRootQueryRetrieveInformationModel, + "" /* don't care about transfer syntax */); + if (this->PresentationContext == 0) { logger.error ( "MOVE Request failed: No valid Study Root MOVE Presentation Context available" ); if (!this->KeepAssociationOpen) { - this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION); + this->SCU.releaseAssociation(); } delete retrieveParameters; return false; } + if (this->Canceled) + { + return false; + } + // do the actual move request - OFCondition status = this->SCU.sendMOVERequest ( - presID, this->MoveDestinationAETitle.toStdString().c_str(), - retrieveParameters, &responses ); + OFCondition status = this->SCU.sendMOVERequest( + this->PresentationContext, this->MoveDestinationAETitle.toStdString().c_str(), + retrieveParameters, &responses); // Close association if we do not want to explicitly keep it open if (!this->KeepAssociationOpen) { - this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION); + this->SCU.releaseAssociation(); } // Free some (little) memory delete retrieveParameters; @@ -320,6 +431,11 @@ bool ctkDICOMRetrievePrivate::move ( const QString& studyInstanceUID, return false; } + if (this->Canceled) + { + 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: @@ -340,13 +456,12 @@ bool ctkDICOMRetrievePrivate::move ( const QString& studyInstanceUID, 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("MOVE request failed, server does report error"); + logger.debug("MOVE request failed, server does report error"); QString statusDetail("No details"); if (rsp->m_statusDetail != NULL) { @@ -355,8 +470,7 @@ bool ctkDICOMRetrievePrivate::move ( const QString& studyInstanceUID, statusDetail = "Status Detail: " + statusDetail.fromStdString(out.str()); } statusDetail.prepend("MOVE request failed: "); - logger.error(statusDetail); - //throw std::runtime_error( statusDetail.toStdString() ); + logger.debug(statusDetail); return false; } } @@ -377,20 +491,71 @@ bool ctkDICOMRetrievePrivate::move ( const QString& studyInstanceUID, .arg(QString::number(static_cast((*it)->m_numberOfWarningSubops))) .arg(QString::number(static_cast((*it)->m_numberOfFailedSubops))) ); + + if (this->Canceled) + { + return false; + } + + // if move was successful, add a taskResults to report it + QSharedPointer jobResponseSet = + QSharedPointer(new ctkDICOMJobResponseSet); + if (q->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSOPInstance) + { + jobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance); + } + else if (q->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSeries) + { + jobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::RetrieveSeries); + } + else if (q->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveStudy) + { + jobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::RetrieveStudy); + } + jobResponseSet->setStudyInstanceUID(q->studyInstanceUID()); + jobResponseSet->setSeriesInstanceUID(q->seriesInstanceUID()); + jobResponseSet->setConnectionName(q->connectionName()); + jobResponseSet->setJobUID(q->jobUID()); + q->addJobResponseSet(jobResponseSet); + return true; } //------------------------------------------------------------------------------ -bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, - const QString& seriesInstanceUID, - const RetrieveType retrieveType ) +bool ctkDICOMRetrievePrivate::get(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const ctkDICOMRetrieve::RetrieveType retrieveType) { Q_Q(ctkDICOMRetrieve); + this->JobResponseSets.clear(); + this->PatientID = patientID; + this->StudyInstanceUID = studyInstanceUID; + this->SeriesInstanceUID = seriesInstanceUID; + this->SOPInstanceUID = sopInstanceUID; + + if (this->Canceled) + { + return false; + } + DcmDataset *retrieveParameters = new DcmDataset(); - if (! this->initializeSCU(studyInstanceUID, seriesInstanceUID, retrieveType, retrieveParameters) ) + if (!this->initializeSCU(patientID, + studyInstanceUID, + seriesInstanceUID, + sopInstanceUID, + retrieveType, + retrieveParameters)) { delete retrieveParameters; + logger.error("MOVE Request failed: SCU initialization failed"); + return false; + } + + if (this->Canceled) + { return false; } @@ -399,26 +564,30 @@ bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, emit q->progress(ctkDICOMRetrieve::tr("Sending Get Request")); emit q->progress(0); OFList responses; - T_ASC_PresentationContextID presID = this->SCU.findPresentationContextID( + this->PresentationContext = this->SCU.findPresentationContextID( UID_GETStudyRootQueryRetrieveInformationModel, "" /* don't care about transfer syntax */ ); - if (presID == 0) + if (this->PresentationContext == 0) { logger.error ( "GET Request failed: No valid Study Root GET Presentation Context available" ); if (!this->KeepAssociationOpen) { - this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION); + this->SCU.releaseAssociation(); } delete retrieveParameters; return false; } + if (this->Canceled) + { + return false; + } + emit q->progress(ctkDICOMRetrieve::tr("Found Presentation Context")); emit q->progress(1); // do the actual move request - OFCondition status = this->SCU.sendCGETRequest ( - presID, retrieveParameters, &responses ); + OFCondition status = this->SCU.sendCGETRequest(this->PresentationContext, retrieveParameters, &responses); emit q->progress(ctkDICOMRetrieve::tr("Sent Get Request")); emit q->progress(2); @@ -426,7 +595,7 @@ bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, // Close association if we do not want to explicitly keep it open if (!this->KeepAssociationOpen) { - this->SCU.closeAssociation(DCMSCU_RELEASE_ASSOCIATION); + this->SCU.releaseAssociation(); } // Free some (little) memory delete retrieveParameters; @@ -440,6 +609,11 @@ bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, return false; } + if (this->Canceled) + { + return false; + } + emit q->progress(ctkDICOMRetrieve::tr("Got Responses")); emit q->progress(3); @@ -463,13 +637,12 @@ bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, 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"); + logger.debug("GET request failed, server does report error"); QString statusDetail("No details"); if (rsp->m_statusDetail != NULL) { @@ -478,8 +651,7 @@ bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, statusDetail = "Status Detail: " + statusDetail.fromStdString(out.str()); } statusDetail.prepend("GET request failed: "); - logger.error(statusDetail); - //throw std::runtime_error( statusDetail.toStdString() ); + logger.debug(statusDetail); return false; } } @@ -490,6 +662,7 @@ bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, { it++; } + logger.debug ( QString("GET responses report for study: %1\n" "%2 images transferred, and\n" @@ -516,6 +689,8 @@ ctkDICOMRetrieve::ctkDICOMRetrieve(QObject* parent) d_ptr(new ctkDICOMRetrievePrivate(*this)) { Q_D(ctkDICOMRetrieve); + + d->SCU.setVerbosePCMode(false); d->SCU.retrieve = this; // give the dcmtk level access to this for emitting signals } @@ -525,8 +700,21 @@ ctkDICOMRetrieve::~ctkDICOMRetrieve() } //------------------------------------------------------------------------------ -/// Set methods for connectivity -void ctkDICOMRetrieve::setCallingAETitle( const QString& callingAETitle ) +void ctkDICOMRetrieve::setConnectionName(const QString &connectionName) +{ + Q_D(ctkDICOMRetrieve); + d->ConnectionName = connectionName; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieve::connectionName() const +{ + Q_D(const ctkDICOMRetrieve); + return d->ConnectionName; +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::setCallingAETitle(const QString& callingAETitle) { Q_D(ctkDICOMRetrieve); if (strcmp(callingAETitle.toStdString().c_str(), d->SCU.getAETitle().c_str())) @@ -614,6 +802,7 @@ QString ctkDICOMRetrieve::moveDestinationAETitle()const return d->MoveDestinationAETitle; } +//------------------------------------------------------------------------------ static void skipDelete(QObject* obj) { Q_UNUSED(obj); @@ -636,12 +825,108 @@ void ctkDICOMRetrieve::setDatabase(QSharedPointer dicomDatabas } //------------------------------------------------------------------------------ -QSharedPointer ctkDICOMRetrieve::database()const +ctkDICOMDatabase* ctkDICOMRetrieve::dicomDatabase()const +{ + Q_D(const ctkDICOMRetrieve); + return d->Database.data(); +} + +//------------------------------------------------------------------------------ +QSharedPointer ctkDICOMRetrieve::dicomDatabaseShared()const { Q_D(const ctkDICOMRetrieve); return d->Database; } +//------------------------------------------------------------------------------ +QList ctkDICOMRetrieve::jobResponseSets() const +{ + Q_D(const ctkDICOMRetrieve); + QList jobResponseSets; + foreach(QSharedPointer jobResponseSet, d->JobResponseSets) + { + jobResponseSets.append(jobResponseSet.data()); + } + + return jobResponseSets; +} + +//------------------------------------------------------------------------------ +QList> ctkDICOMRetrieve::jobResponseSetsShared() const +{ + Q_D(const ctkDICOMRetrieve); + return d->JobResponseSets; +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::addJobResponseSet(ctkDICOMJobResponseSet &jobResponseSet) +{ + this->addJobResponseSet(QSharedPointer(&jobResponseSet, skipDelete)); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::addJobResponseSet(QSharedPointer jobResponseSet) +{ + Q_D(ctkDICOMRetrieve); + d->JobResponseSets.append(jobResponseSet); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::removeJobResponseSet(QSharedPointer jobResponseSet) +{ + Q_D(ctkDICOMRetrieve); + d->JobResponseSets.removeOne(jobResponseSet); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::setJobUID(const QString &jobUID) +{ + Q_D(ctkDICOMRetrieve); + d->JobUID = jobUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieve::jobUID() const +{ + Q_D(const ctkDICOMRetrieve); + return d->JobUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieve::patientID() const +{ + Q_D(const ctkDICOMRetrieve); + return d->PatientID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieve::studyInstanceUID() const +{ + Q_D(const ctkDICOMRetrieve); + return d->StudyInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieve::seriesInstanceUID() const +{ + Q_D(const ctkDICOMRetrieve); + return d->SeriesInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieve::sopInstanceUID() const +{ + Q_D(const ctkDICOMRetrieve); + return d->SOPInstanceUID; +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieve::RetrieveType ctkDICOMRetrieve::getLastRetrieveType() const +{ + Q_D(const ctkDICOMRetrieve); + return d->LastRetrieveType; +} + //------------------------------------------------------------------------------ void ctkDICOMRetrieve::setKeepAssociationOpen(const bool keepOpen) { @@ -650,27 +935,37 @@ void ctkDICOMRetrieve::setKeepAssociationOpen(const bool keepOpen) } //------------------------------------------------------------------------------ -bool ctkDICOMRetrieve::keepAssociationOpen() +bool ctkDICOMRetrieve::keepAssociationOpen() const { Q_D(const ctkDICOMRetrieve); return d->KeepAssociationOpen; } -void ctkDICOMRetrieve::setWasCanceled(const bool wasCanceled) +//----------------------------------------------------------------------------- +void ctkDICOMRetrieve::setConnectionTimeout(int timeout) { Q_D(ctkDICOMRetrieve); - d->WasCanceled = wasCanceled; + d->SCU.setACSETimeout(timeout); + d->SCU.setConnectionTimeout(timeout); } -//------------------------------------------------------------------------------ +//----------------------------------------------------------------------------- +int ctkDICOMRetrieve::connectionTimeout() const +{ + Q_D(const ctkDICOMRetrieve); + return d->SCU.getConnectionTimeout(); +} + +//----------------------------------------------------------------------------- bool ctkDICOMRetrieve::wasCanceled() { Q_D(const ctkDICOMRetrieve); - return d->WasCanceled; + return d->Canceled; } //------------------------------------------------------------------------------ -bool ctkDICOMRetrieve::moveStudy(const QString& studyInstanceUID) +bool ctkDICOMRetrieve::moveStudy(const QString& studyInstanceUID, + const QString& patientID) { if (studyInstanceUID.isEmpty()) { @@ -678,54 +973,105 @@ bool ctkDICOMRetrieve::moveStudy(const QString& studyInstanceUID) return false; } Q_D(ctkDICOMRetrieve); - logger.info ( "Starting moveStudy" ); - return d->move ( studyInstanceUID, "", ctkDICOMRetrievePrivate::RetrieveStudy ); + logger.debug("Starting moveStudy"); + return d->move(patientID, studyInstanceUID, "", "", ctkDICOMRetrieve::RetrieveStudy); } //------------------------------------------------------------------------------ -bool ctkDICOMRetrieve::getStudy(const QString& studyInstanceUID) +bool ctkDICOMRetrieve::getStudy(const QString& studyInstanceUID, + const QString& patientID) { if (studyInstanceUID.isEmpty()) { - logger.error("Cannot receive series: Study Instance UID empty."); + logger.error("Cannot receive study: Study Instance UID empty."); return false; } Q_D(ctkDICOMRetrieve); - logger.info ( "Starting getStudy" ); - return d->get ( studyInstanceUID, "", ctkDICOMRetrievePrivate::RetrieveStudy ); + logger.debug("Starting getStudy"); + return d->get(patientID, studyInstanceUID, "", "", ctkDICOMRetrieve::RetrieveStudy); } //------------------------------------------------------------------------------ bool ctkDICOMRetrieve::moveSeries(const QString& studyInstanceUID, - const QString& seriesInstanceUID) + const QString& seriesInstanceUID, + const QString& patientID) { - if (studyInstanceUID.isEmpty() || seriesInstanceUID.isEmpty()) + if (studyInstanceUID.isEmpty() || + seriesInstanceUID.isEmpty()) { - logger.error("Cannot receive series: Either Study or Series Instance UID empty."); + logger.error("Cannot receive series: Study or Series Instance UID empty."); return false; } Q_D(ctkDICOMRetrieve); - logger.info ( "Starting moveSeries" ); - return d->move ( studyInstanceUID, seriesInstanceUID, ctkDICOMRetrievePrivate::RetrieveSeries ); + logger.debug("Starting moveSeries"); + return d->move(patientID, studyInstanceUID, seriesInstanceUID, "", ctkDICOMRetrieve::RetrieveSeries); } //------------------------------------------------------------------------------ bool ctkDICOMRetrieve::getSeries(const QString& studyInstanceUID, - const QString& seriesInstanceUID) + const QString& seriesInstanceUID, + const QString& patientID) { - if (studyInstanceUID.isEmpty() || seriesInstanceUID.isEmpty()) + if (studyInstanceUID.isEmpty() || + seriesInstanceUID.isEmpty()) { - logger.error("Cannot receive series: Either Study or Series Instance UID empty."); + logger.error("Cannot receive series: Study or Series Instance UID empty."); return false; } Q_D(ctkDICOMRetrieve); - logger.info ( "Starting getSeries" ); - return d->get ( studyInstanceUID, seriesInstanceUID, ctkDICOMRetrievePrivate::RetrieveSeries ); + logger.debug("Starting getSeries"); + return d->get(patientID, studyInstanceUID, seriesInstanceUID, "", ctkDICOMRetrieve::RetrieveSeries); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMRetrieve::moveSOPInstance(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& SOPInstanceUID, + const QString& patientID) +{ + Q_D(ctkDICOMRetrieve); + + if (studyInstanceUID.isEmpty() || + seriesInstanceUID.isEmpty() || + SOPInstanceUID.isEmpty()) + { + logger.error("Cannot receive SOPInstance: Study, Series or SOP Instance UID empty."); + return false; + } + + logger.debug("Starting moveSOPInstance"); + return d->move(patientID, studyInstanceUID, seriesInstanceUID, SOPInstanceUID, ctkDICOMRetrieve::RetrieveSOPInstance); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMRetrieve::getSOPInstance(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& SOPInstanceUID, + const QString& patientID) +{ + Q_D(ctkDICOMRetrieve); + + if (studyInstanceUID.isEmpty() || + seriesInstanceUID.isEmpty() || + SOPInstanceUID.isEmpty()) + { + logger.error("Cannot receive SOPInstance: Study, Series or SOP Instance UID empty."); + return false; + } + + logger.debug("Starting getSOPInstance"); + return d->get(patientID, studyInstanceUID, seriesInstanceUID, SOPInstanceUID, ctkDICOMRetrieve::RetrieveSOPInstance); } //------------------------------------------------------------------------------ void ctkDICOMRetrieve::cancel() { Q_D(ctkDICOMRetrieve); - d->WasCanceled = true; + d->Canceled = true; + + if (d->PresentationContext != 0) + { + d->SCU.sendCANCELRequest(d->PresentationContext); + d->PresentationContext = 0; + } } diff --git a/Libs/DICOM/Core/ctkDICOMRetrieve.h b/Libs/DICOM/Core/ctkDICOMRetrieve.h index a2723b249f..5e7dadfc0b 100644 --- a/Libs/DICOM/Core/ctkDICOMRetrieve.h +++ b/Libs/DICOM/Core/ctkDICOMRetrieve.h @@ -31,70 +31,152 @@ // CTK Core includes #include "ctkDICOMDatabase.h" +#include "ctkErrorLogLevel.h" class ctkDICOMRetrievePrivate; +class ctkDICOMJobResponse; /// \ingroup DICOM_Core class CTK_DICOM_CORE_EXPORT ctkDICOMRetrieve : public QObject { Q_OBJECT + Q_PROPERTY(QString connectionName READ connectionName WRITE setConnectionName); Q_PROPERTY(QString callingAETitle READ callingAETitle WRITE setCallingAETitle); Q_PROPERTY(QString calledAETitle READ calledAETitle WRITE setCalledAETitle); Q_PROPERTY(QString host READ host WRITE setHost); Q_PROPERTY(int port READ port WRITE setPort); Q_PROPERTY(QString moveDestinationAETitle READ moveDestinationAETitle WRITE setMoveDestinationAETitle); Q_PROPERTY(bool keepAssociationOpen READ keepAssociationOpen WRITE setKeepAssociationOpen); - Q_PROPERTY(bool wasCanceled READ wasCanceled WRITE setWasCanceled); + Q_PROPERTY(int connectionTimeout READ connectionTimeout WRITE setConnectionTimeout); + Q_PROPERTY(QString seriesInstanceUID READ seriesInstanceUID); + Q_PROPERTY(QString studyInstanceUID READ studyInstanceUID); + Q_PROPERTY(QString jobUID READ jobUID WRITE setJobUID); public: explicit ctkDICOMRetrieve(QObject* parent = 0); virtual ~ctkDICOMRetrieve(); - /// Set methods for connectivity + ///@{ + /// Name identifying the server + void setConnectionName(const QString& connectionName); + QString connectionName() const; + ///@} + + ///@{ /// CTK_AE - the AE string by which the peer host might /// recognize your request - Q_INVOKABLE void setCallingAETitle( const QString& callingAETitle ); - Q_INVOKABLE QString callingAETitle() const; + void setCallingAETitle(const QString& callingAETitle); + QString callingAETitle() const; + ///@} + + ///@{ /// CTK_AE - the AE of the service of peer host that you are calling /// which tells the host what you are requesting - Q_INVOKABLE void setCalledAETitle( const QString& calledAETitle ); - Q_INVOKABLE QString calledAETitle() const; - /// peer hostname being connected to - Q_INVOKABLE void setHost( const QString& host ); - Q_INVOKABLE QString host() const; + void setCalledAETitle(const QString& calledAETitle); + QString calledAETitle() const; + ///@} + + ///@{ + /// Peer hostname being connected to + void setHost(const QString& host); + QString host() const; + ///@} + + ///@{ /// [0, 65365] port on peer host - e.g. 11112 - Q_INVOKABLE void setPort( int port ); - Q_INVOKABLE int port() const; + 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 - Q_INVOKABLE void setMoveDestinationAETitle( const QString& moveDestinationAETitle ); - Q_INVOKABLE QString moveDestinationAETitle() const; + void setMoveDestinationAETitle(const QString& moveDestinationAETitle); + QString moveDestinationAETitle() const; + ///@} + + ///@{ /// prefer to keep using the existing association to peer host when doing /// multiple requests (default true) - Q_INVOKABLE void setKeepAssociationOpen(const bool keepOpen); - Q_INVOKABLE bool keepAssociationOpen(); - /// did someone cancel us during operation? - /// (default false) - Q_INVOKABLE void setWasCanceled(const bool wasCanceled); + void setKeepAssociationOpen(const bool keepOpen); + bool keepAssociationOpen() const; + ///@} + + ///@{ + /// connection timeout, default 3 sec. + void setConnectionTimeout(int timeout); + int connectionTimeout() const; + ///@} + + /// operation is canceled? Q_INVOKABLE bool wasCanceled(); + + ///@{ /// where to insert new data sets obtained via get (must be set for /// get to succeed) Q_INVOKABLE void setDatabase(ctkDICOMDatabase& dicomDatabase); void setDatabase(QSharedPointer dicomDatabase); - Q_INVOKABLE QSharedPointer database()const; + Q_INVOKABLE ctkDICOMDatabase* dicomDatabase() const; + QSharedPointer dicomDatabaseShared() const; + ///@} + + ///@{ + /// Access the list of datasets from the last operation. + Q_INVOKABLE QList jobResponseSets() const; + QList> jobResponseSetsShared() const; + Q_INVOKABLE void addJobResponseSet(ctkDICOMJobResponseSet& jobResponseSet); + void addJobResponseSet(QSharedPointer jobResponseSet); + void removeJobResponseSet(QSharedPointer jobResponseSet); + Q_INVOKABLE void setJobUID(const QString& jobUID); + Q_INVOKABLE QString jobUID() const; + ///@} + + /// Patient ID from from the last operation. + QString patientID() const; + /// Study instance UID from from the last operation. + QString studyInstanceUID() const; + /// Series instance UID from from the last operation. + QString seriesInstanceUID() const; + /// SOP instance UID from from the last operation. + QString sopInstanceUID() const; + + enum RetrieveType + { + RetrieveNone, + RetrieveSOPInstance, + RetrieveSeries, + RetrieveStudy + }; + + /// last retrieve type + RetrieveType getLastRetrieveType() const; public Q_SLOTS: /// Use CMOVE to ask peer host to store data to move destination - Q_INVOKABLE bool moveSeries( const QString& studyInstanceUID, - const QString& seriesInstanceUID ); + Q_INVOKABLE bool moveSOPInstance(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& SOPInstanceUID, + const QString& patientID = ""); /// Use CMOVE to ask peer host to store data to move destination - Q_INVOKABLE bool moveStudy( const QString& studyInstanceUID ); + Q_INVOKABLE bool moveSeries(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& patientID = ""); + /// Use CMOVE to ask peer host to store data to move destination + Q_INVOKABLE bool moveStudy(const QString& studyInstanceUID, + const QString& patientID = ""); + /// Use CGET to ask peer host to store data to us + Q_INVOKABLE bool getSOPInstance(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& SOPInstanceUID, + const QString& patientID = ""); /// Use CGET to ask peer host to store data to us - Q_INVOKABLE bool getSeries( const QString& studyInstanceUID, - const QString& seriesInstanceUID ); + Q_INVOKABLE bool getSeries(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& patientID = ""); /// Use CGET to ask peer host to store data to us - Q_INVOKABLE bool getStudy( const QString& studyInstanceUID ); + Q_INVOKABLE bool getStudy(const QString& studyInstanceUID, + const QString& patientID = ""); /// Cancel the current operation Q_INVOKABLE void cancel(); @@ -113,6 +195,8 @@ public Q_SLOTS: /// Signal is emitted inside the retrieve() function when finished with value /// true for success or false for error void done(const bool& error); + /// Signal is emitted inside the retrieve() function when a frame has been fetched + void progressJobDetail(QVariant data); protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveJob.cpp b/Libs/DICOM/Core/ctkDICOMRetrieveJob.cpp new file mode 100644 index 0000000000..5621e9d961 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveJob.cpp @@ -0,0 +1,192 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMJobResponseSet.h" // For ctkDICOMJobDetail +#include "ctkDICOMRetrieveJob_p.h" +#include "ctkDICOMRetrieveWorker.h" +#include "ctkDICOMServer.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMRetrieveJob"); + +//------------------------------------------------------------------------------ +// ctkDICOMRetrieveJobPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveJobPrivate::ctkDICOMRetrieveJobPrivate(ctkDICOMRetrieveJob* object) + : q_ptr(object) +{ + this->Server = nullptr; +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveJobPrivate::~ctkDICOMRetrieveJobPrivate() = default; + +//------------------------------------------------------------------------------ +// ctkDICOMRetrieveJob methods + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveJob::ctkDICOMRetrieveJob() + : d_ptr(new ctkDICOMRetrieveJobPrivate(this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveJob::ctkDICOMRetrieveJob(ctkDICOMRetrieveJobPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveJob::~ctkDICOMRetrieveJob() = default; + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMRetrieveJob::server() const +{ + Q_D(const ctkDICOMRetrieveJob); + return d->Server.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMRetrieveJob::serverShared() const +{ + Q_D(const ctkDICOMRetrieveJob); + return d->Server; +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveJob::setServer(ctkDICOMServer& server) +{ + Q_D(ctkDICOMRetrieveJob); + d->Server = QSharedPointer(&server, skipDelete); +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveJob::setServer(QSharedPointer server) +{ + Q_D(ctkDICOMRetrieveJob); + d->Server = server; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMRetrieveJob::loggerReport(const QString& status) const +{ + switch (this->dicomLevel()) + { + case ctkDICOMJob::DICOMLevels::Patients: + return QString("ctkDICOMRetrieveJob: retrieve task at patients level %1.\n" + "JobUID: %2\n" + "Server: %3\n" + "PatientID: %4\n") + .arg(status) + .arg(this->jobUID()) + .arg(this->server()->connectionName()) + .arg(this->patientID()); + case ctkDICOMJob::DICOMLevels::Studies: + return QString("ctkDICOMRetrieveJob: retrieve task at studies level %1.\n" + "JobUID: %2\n" + "Server: %3\n" + "PatientID: %4\n" + "StudyInstanceUID: %5\n") + .arg(status) + .arg(this->jobUID()) + .arg(this->server()->connectionName()) + .arg(this->patientID()) + .arg(this->studyInstanceUID()); + case ctkDICOMJob::DICOMLevels::Series: + return QString("ctkDICOMRetrieveJob: retrieve task at series level %1.\n" + "JobUID: %2\n" + "Server: %3\n" + "PatientID: %4\n" + "StudyInstanceUID: %5\n" + "SeriesInstanceUID: %6\n") + .arg(status) + .arg(this->jobUID()) + .arg(this->server()->connectionName()) + .arg(this->patientID()) + .arg(this->studyInstanceUID()) + .arg(this->seriesInstanceUID()); + case ctkDICOMJob::DICOMLevels::Instances: + return QString("ctkDICOMRetrieveJob: retrieve task at instances level %1.\n" + "JobUID: %2\n" + "Server: %3\n" + "PatientID: %4\n" + "StudyInstanceUID: %5\n" + "SeriesInstanceUID: %6\n" + "SOPInstanceUID: %7\n") + .arg(status) + .arg(this->jobUID()) + .arg(this->server()->connectionName()) + .arg(this->patientID()) + .arg(this->studyInstanceUID()) + .arg(this->seriesInstanceUID()) + .arg(this->sopInstanceUID()); + default: + return QString(""); + } +} +//------------------------------------------------------------------------------ +ctkAbstractJob* ctkDICOMRetrieveJob::clone() const +{ + ctkDICOMRetrieveJob* newRetrieveJob = new ctkDICOMRetrieveJob; + newRetrieveJob->setServer(this->serverShared()); + newRetrieveJob->setDICOMLevel(this->dicomLevel()); + newRetrieveJob->setPatientID(this->patientID()); + newRetrieveJob->setStudyInstanceUID(this->studyInstanceUID()); + newRetrieveJob->setSeriesInstanceUID(this->seriesInstanceUID()); + newRetrieveJob->setSOPInstanceUID(this->sopInstanceUID()); + newRetrieveJob->setMaximumNumberOfRetry(this->maximumNumberOfRetry()); + newRetrieveJob->setRetryDelay(this->retryDelay()); + newRetrieveJob->setRetryCounter(this->retryCounter()); + newRetrieveJob->setIsPersistent(this->isPersistent()); + newRetrieveJob->setMaximumConcurrentJobsPerType(this->maximumConcurrentJobsPerType()); + newRetrieveJob->setPriority(this->priority()); + + return newRetrieveJob; +} + +//------------------------------------------------------------------------------ +ctkAbstractWorker* ctkDICOMRetrieveJob::createWorker() +{ + ctkDICOMRetrieveWorker* worker = + new ctkDICOMRetrieveWorker; + worker->setJob(*this); + return worker; +} + +//------------------------------------------------------------------------------ +QVariant ctkDICOMRetrieveJob::toVariant() +{ + return QVariant::fromValue(ctkDICOMJobDetail(*this, this->server()->connectionName())); +} diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveJob.h b/Libs/DICOM/Core/ctkDICOMRetrieveJob.h new file mode 100644 index 0000000000..bea19abec4 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveJob.h @@ -0,0 +1,89 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMRetrieveJob_h +#define __ctkDICOMRetrieveJob_h + +// Qt includes +#include +#include + +// ctkCore includes +class ctkAbstractWorker; + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMJob.h" +class ctkDICOMRetrieveJobPrivate; +class ctkDICOMServer; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMRetrieveJob : public ctkDICOMJob +{ + Q_OBJECT + +public: + typedef ctkDICOMJob Superclass; + explicit ctkDICOMRetrieveJob(); + virtual ~ctkDICOMRetrieveJob(); + + ///@{ + /// Server + Q_INVOKABLE ctkDICOMServer* server() const; + QSharedPointer serverShared() const; + Q_INVOKABLE void setServer(ctkDICOMServer& server); + void setServer(QSharedPointer server); + ///@} + + /// Logger report string formatting for specific task + Q_INVOKABLE QString loggerReport(const QString& status) const override; + + /// \see ctkAbstractJob::clone() + Q_INVOKABLE ctkAbstractJob* clone() const override; + + /// Generate worker for job + Q_INVOKABLE ctkAbstractWorker* createWorker() override; + + /// Return the QVariant value of this job. + /// + /// The value is set using the ctkDICOMJobDetail metatype and is used to pass + /// information between threads using Qt signals. + /// \sa ctkDICOMJobDetail + Q_INVOKABLE virtual QVariant toVariant() override; + +protected: + QScopedPointer d_ptr; + + /// Constructor allowing derived class to specify a specialized pimpl. + /// + /// \note You are responsible to call init() in the constructor of + /// derived class. Doing so ensures the derived class is fully + /// instantiated in case virtual method are called within init() itself. + ctkDICOMRetrieveJob(ctkDICOMRetrieveJobPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMRetrieveJob); + Q_DISABLE_COPY(ctkDICOMRetrieveJob); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveJob_p.h b/Libs/DICOM/Core/ctkDICOMRetrieveJob_p.h new file mode 100644 index 0000000000..20b7ea9580 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveJob_p.h @@ -0,0 +1,51 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMRetrieveJobPrivate_h +#define __ctkDICOMRetrieveJobPrivate_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMRetrieveJob.h" +#include "ctkDICOMServer.h" + +//------------------------------------------------------------------------------ +class ctkDICOMRetrieveJobPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMRetrieveJob) + +protected: + ctkDICOMRetrieveJob* const q_ptr; + +public: + ctkDICOMRetrieveJobPrivate(ctkDICOMRetrieveJob* object); + virtual ~ctkDICOMRetrieveJobPrivate(); + + QSharedPointer Server; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveWorker.cpp b/Libs/DICOM/Core/ctkDICOMRetrieveWorker.cpp new file mode 100644 index 0000000000..41db62dddd --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveWorker.cpp @@ -0,0 +1,300 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMJobResponseSet.h" +#include "ctkDICOMRetrieveWorker_p.h" +#include "ctkDICOMRetrieveJob.h" +#include "ctkDICOMScheduler.h" +#include "ctkDICOMServer.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMRetrieveWorker"); + +//------------------------------------------------------------------------------ +// ctkDICOMRetrieveWorkerPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveWorkerPrivate::ctkDICOMRetrieveWorkerPrivate(ctkDICOMRetrieveWorker* object) + : q_ptr(object) +{ + this->Retrieve = QSharedPointer(new ctkDICOMRetrieve); +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveWorkerPrivate::~ctkDICOMRetrieveWorkerPrivate() +{ + Q_Q(ctkDICOMRetrieveWorker); + + QSharedPointer retrieveJob = + qSharedPointerObjectCast(q->Job); + if (!retrieveJob) + { + return; + } + + QObject::disconnect(this->Retrieve.data(), SIGNAL(progressJobDetail(QVariant)), + retrieveJob.data(), SIGNAL(progressJobDetail(QVariant))); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieveWorkerPrivate::setRetrieveParameters() +{ + Q_Q(ctkDICOMRetrieveWorker); + + QSharedPointer retrieveJob = + qSharedPointerObjectCast(q->Job); + if (!retrieveJob) + { + return; + } + + ctkDICOMServer* server = retrieveJob->server(); + if (!server) + { + return; + } + + this->Retrieve->setConnectionName(server->connectionName()); + this->Retrieve->setCallingAETitle(server->callingAETitle()); + this->Retrieve->setCalledAETitle(server->calledAETitle()); + this->Retrieve->setHost(server->host()); + this->Retrieve->setPort(server->port()); + this->Retrieve->setConnectionTimeout(server->connectionTimeout()); + this->Retrieve->setMoveDestinationAETitle(server->moveDestinationAETitle()); + this->Retrieve->setKeepAssociationOpen(server->keepAssociationOpen()); + this->Retrieve->setJobUID(retrieveJob->jobUID()); + + QObject::connect(this->Retrieve.data(), SIGNAL(progressJobDetail(QVariant)), + retrieveJob.data(), SIGNAL(progressJobDetail(QVariant)), Qt::DirectConnection); + +} + +//------------------------------------------------------------------------------ +// ctkDICOMRetrieveWorker methods + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveWorker::ctkDICOMRetrieveWorker() + : d_ptr(new ctkDICOMRetrieveWorkerPrivate(this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveWorker::ctkDICOMRetrieveWorker(ctkDICOMRetrieveWorkerPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveWorker::~ctkDICOMRetrieveWorker() = default; + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveWorker::cancel() +{ + Q_D(const ctkDICOMRetrieveWorker); + d->Retrieve->cancel(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveWorker::run() +{ + Q_D(const ctkDICOMRetrieveWorker); + QSharedPointer retrieveJob = + qSharedPointerObjectCast(this->Job); + if (!retrieveJob) + { + return; + } + + QSharedPointer scheduler = + qSharedPointerObjectCast(this->Scheduler); + ctkDICOMServer* server = retrieveJob->server(); + if (!scheduler + || !server + || retrieveJob->status() == ctkAbstractJob::JobStatus::Stopped) + { + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Running); + emit retrieveJob->started(); + + logger.debug(QString("ctkDICOMRetrieveWorker : running job %1 in thread %2.\n") + .arg(retrieveJob->jobUID()) + .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); + + switch (server->retrieveProtocol()) + { + case ctkDICOMServer::CGET: + switch(retrieveJob->dicomLevel()) + { + case ctkDICOMJob::DICOMLevels::Patients: + logger.info("ctkDICOMRetrieveTask : get operation for a full patient is not implemented."); + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + case ctkDICOMJob::DICOMLevels::Studies: + if (!d->Retrieve->getStudy(retrieveJob->studyInstanceUID(), + retrieveJob->patientID())) + { + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + case ctkDICOMJob::DICOMLevels::Series: + if (!d->Retrieve->getSeries(retrieveJob->studyInstanceUID(), + retrieveJob->seriesInstanceUID(), + retrieveJob->patientID())) + { + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + case ctkDICOMJob::DICOMLevels::Instances: + if (!d->Retrieve->getSOPInstance(retrieveJob->studyInstanceUID(), + retrieveJob->seriesInstanceUID(), + retrieveJob->sopInstanceUID(), + retrieveJob->patientID())) + { + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + } + break; + case ctkDICOMServer::CMOVE: + switch(retrieveJob->dicomLevel()) + { + case ctkDICOMJob::DICOMLevels::Patients: + logger.info("ctkDICOMRetrieveTask : move operation for a full patient is not implemented."); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + emit retrieveJob->failed(); + return; + case ctkDICOMJob::DICOMLevels::Studies: + if (!d->Retrieve->moveStudy(retrieveJob->studyInstanceUID(), + retrieveJob->patientID())) + { + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + case ctkDICOMJob::DICOMLevels::Series: + if (!d->Retrieve->moveSeries(retrieveJob->studyInstanceUID(), + retrieveJob->seriesInstanceUID(), + retrieveJob->patientID())) + { + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + case ctkDICOMJob::DICOMLevels::Instances: + if (!d->Retrieve->moveSOPInstance(retrieveJob->studyInstanceUID(), + retrieveJob->seriesInstanceUID(), + retrieveJob->sopInstanceUID(), + retrieveJob->patientID())) + { + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + break; + } + break; + //case ctkDICOMServer::WADO: // To Do + } + + if (retrieveJob->status() == ctkAbstractJob::JobStatus::Stopped) + { + emit retrieveJob->canceled(); + this->onJobCanceled(); + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + + ctkDICOMServer* proxyServer = server->proxyServer(); + if (proxyServer && proxyServer->queryRetrieveEnabled()) + { + ctkDICOMRetrieveJob* newJob = qobject_cast(retrieveJob->clone()); + newJob->setRetryCounter(0); + newJob->setServer(*proxyServer); + scheduler->addJob(newJob); + } + else if (d->Retrieve->jobResponseSetsShared().count() > 0) + { + // To Do: this insert should happen in batch of 10 frames (configurable), + // instead of at the end of operation (all frames requested)). + // This would avoid memory usage spikes when requesting a series or study with a lot of frames. + // NOTE: the memory release should happen as soon as we insert the response. + scheduler->insertJobResponseSets(d->Retrieve->jobResponseSetsShared()); + } + + retrieveJob->setStatus(ctkAbstractJob::JobStatus::Finished); + emit retrieveJob->finished(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveWorker::setJob(QSharedPointer job) +{ + Q_D(ctkDICOMRetrieveWorker); + + QSharedPointer retrieveJob = + qSharedPointerObjectCast(job); + if (!retrieveJob) + { + return; + } + + this->Superclass::setJob(job); + d->setRetrieveParameters(); +} + +//---------------------------------------------------------------------------- +ctkDICOMRetrieve* ctkDICOMRetrieveWorker::retriever() const +{ + Q_D(const ctkDICOMRetrieveWorker); + return d->Retrieve.data(); +} + +//------------------------------------------------------------------------------ +QSharedPointer ctkDICOMRetrieveWorker::retrieverShared() const +{ + Q_D(const ctkDICOMRetrieveWorker); + return d->Retrieve; +} diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveWorker.h b/Libs/DICOM/Core/ctkDICOMRetrieveWorker.h new file mode 100644 index 0000000000..1eacc8cce0 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveWorker.h @@ -0,0 +1,78 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMRetrieveWorker_h +#define __ctkDICOMRetrieveWorker_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkAbstractWorker.h" +class ctkDICOMRetrieve; +class ctkDICOMRetrieveWorkerPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMRetrieveWorker : public ctkAbstractWorker +{ + Q_OBJECT + +public: + typedef ctkAbstractWorker Superclass; + explicit ctkDICOMRetrieveWorker(); + virtual ~ctkDICOMRetrieveWorker(); + + /// Execute worker + void run() override; + + /// Cancel worker + void cancel() override; + + /// Job + void setJob(QSharedPointer job) override; + using ctkAbstractWorker::setJob; + + ///@{ + /// Retriever + QSharedPointer retrieverShared() const; + Q_INVOKABLE ctkDICOMRetrieve* retriever() const; + ///@} + +protected: + QScopedPointer d_ptr; + + /// Constructor allowing derived class to specify a specialized pimpl. + /// + /// \note You are responsible to call init() in the constructor of + /// derived class. Doing so ensures the derived class is fully + /// instantiated in case virtual method are called within init() itself. + ctkDICOMRetrieveWorker(ctkDICOMRetrieveWorkerPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMRetrieveWorker); + Q_DISABLE_COPY(ctkDICOMRetrieveWorker); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveWorker_p.h b/Libs/DICOM/Core/ctkDICOMRetrieveWorker_p.h new file mode 100644 index 0000000000..a0ba9e6d8c --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveWorker_p.h @@ -0,0 +1,53 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMRetrieveWorkerPrivate_h +#define __ctkDICOMRetrieveWorkerPrivate_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMRetrieve.h" +#include "ctkDICOMRetrieveWorker.h" + +//------------------------------------------------------------------------------ +class ctkDICOMRetrieveWorkerPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMRetrieveWorker) + +protected: + ctkDICOMRetrieveWorker* const q_ptr; + +public: + ctkDICOMRetrieveWorkerPrivate(ctkDICOMRetrieveWorker* object); + virtual ~ctkDICOMRetrieveWorkerPrivate(); + + void setRetrieveParameters(); + + QSharedPointer Retrieve; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMScheduler.cpp b/Libs/DICOM/Core/ctkDICOMScheduler.cpp new file mode 100644 index 0000000000..13b229728a --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMScheduler.cpp @@ -0,0 +1,753 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkCore includes +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMInserterJob.h" +#include "ctkDICOMJobResponseSet.h" +#include "ctkDICOMQueryJob.h" +#include "ctkDICOMRetrieveJob.h" +#include "ctkDICOMScheduler.h" +#include "ctkDICOMScheduler_p.h" +#include "ctkDICOMServer.h" +#include "ctkDICOMStorageListenerJob.h" +#include "ctkDICOMUtil.h" + +// dcmtk includes +#include + + +static ctkLogger logger("org.commontk.dicom.DICOMJobPool"); + +//------------------------------------------------------------------------------ +// ctkDICOMSchedulerPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMSchedulerPrivate::ctkDICOMSchedulerPrivate(ctkDICOMScheduler& obj) + : ctkJobSchedulerPrivate(obj) +{ + ctk::setDICOMLogLevel(ctkErrorLogLevel::Warning); +} + +//------------------------------------------------------------------------------ +ctkDICOMSchedulerPrivate::~ctkDICOMSchedulerPrivate() +{ + Q_Q(ctkDICOMScheduler); + q->removeAllServers(); +} + +//------------------------------------------------------------------------------ +ctkDICOMServer* ctkDICOMSchedulerPrivate::getServerFromProxyServersByConnectionName(const QString& connectionName) +{ + foreach (QSharedPointer server, this->Servers) + { + ctkDICOMServer* proxyServer = server->proxyServer(); + if (proxyServer && proxyServer->connectionName() == connectionName) + { + return proxyServer; + } + } + + return nullptr; +} + +//------------------------------------------------------------------------------ +// ctkDICOMScheduler methods + +//------------------------------------------------------------------------------ +ctkDICOMScheduler::ctkDICOMScheduler(QObject* parentObject) + : Superclass(new ctkDICOMSchedulerPrivate(*this), parentObject) +{ + Q_D(ctkDICOMScheduler); + d->init(); +} + +// -------------------------------------------------------------------------- +ctkDICOMScheduler::ctkDICOMScheduler(ctkDICOMSchedulerPrivate* pimpl, QObject* parentObject) + : Superclass(pimpl, parentObject) +{ + // derived classes must call init manually. Calling init() here may results in + // actions on a derived public class not yet finished to be created +} + +//------------------------------------------------------------------------------ +ctkDICOMScheduler::~ctkDICOMScheduler() +{ + this->stopAllJobs(true); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::queryPatients(QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + foreach (QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + QSharedPointer job = + QSharedPointer(new ctkDICOMQueryJob); + job->setServer(server); + job->setMaximumPatientsQuery(d->MaximumPatientsQuery); + job->setFilters(d->Filters); + job->setDICOMLevel(ctkDICOMQueryJob::DICOMLevels::Patients); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setPriority(priority); + + d->insertJob(job); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::queryStudies(const QString& patientID, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + foreach (QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + QSharedPointer job = + QSharedPointer(new ctkDICOMQueryJob); + job->setServer(server); + job->setFilters(d->Filters); + job->setDICOMLevel(ctkDICOMQueryJob::DICOMLevels::Studies); + job->setPatientID(patientID); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setPriority(priority); + + d->insertJob(job); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::querySeries(const QString& patientID, + const QString& studyInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + foreach (QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + QSharedPointer job = + QSharedPointer(new ctkDICOMQueryJob); + job->setServer(server); + job->setFilters(d->Filters); + job->setDICOMLevel(ctkDICOMQueryJob::DICOMLevels::Series); + job->setPatientID(patientID); + job->setStudyInstanceUID(studyInstanceUID); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setPriority(priority); + + d->insertJob(job); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::queryInstances(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + foreach (QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + QSharedPointer job = + QSharedPointer(new ctkDICOMQueryJob); + job->setServer(server); + job->setFilters(d->Filters); + job->setDICOMLevel(ctkDICOMQueryJob::DICOMLevels::Instances); + job->setPatientID(patientID); + job->setStudyInstanceUID(studyInstanceUID); + job->setSeriesInstanceUID(seriesInstanceUID); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setPriority(priority); + + d->insertJob(job); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::retrieveStudy(const QString& patientID, + const QString& studyInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + foreach (QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + QSharedPointer job = + QSharedPointer(new ctkDICOMRetrieveJob); + job->setServer(server); + job->setDICOMLevel(ctkDICOMRetrieveJob::DICOMLevels::Studies); + job->setPatientID(patientID); + job->setStudyInstanceUID(studyInstanceUID); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setPriority(priority); + + d->insertJob(job); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::retrieveSeries(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + foreach (QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + QSharedPointer job = + QSharedPointer(new ctkDICOMRetrieveJob); + job->setServer(server); + job->setDICOMLevel(ctkDICOMRetrieveJob::DICOMLevels::Series); + job->setPatientID(patientID); + job->setStudyInstanceUID(studyInstanceUID); + job->setSeriesInstanceUID(seriesInstanceUID); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setPriority(priority); + + d->insertJob(job); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::retrieveSOPInstance(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& SOPInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + foreach (QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + QSharedPointer job = + QSharedPointer(new ctkDICOMRetrieveJob); + job->setServer(server); + job->setDICOMLevel(ctkDICOMRetrieveJob::DICOMLevels::Instances); + job->setPatientID(patientID); + job->setStudyInstanceUID(studyInstanceUID); + job->setSeriesInstanceUID(seriesInstanceUID); + job->setSOPInstanceUID(SOPInstanceUID); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setPriority(priority); + + d->insertJob(job); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::startListener(int port, + const QString& AETitle, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + QSharedPointer job = + QSharedPointer(new ctkDICOMStorageListenerJob); + job->setPort(port); + job->setAETitle(AETitle); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setPriority(priority); + + d->insertJob(job); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::insertJobResponseSet(const QSharedPointer& jobResponseSet, + QThread::Priority priority) +{ + QList> jobResponseSets; + jobResponseSets.append(jobResponseSet); + this->insertJobResponseSets(jobResponseSets, priority); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::insertJobResponseSets(const QList>& jobResponseSets, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + QSharedPointer job = + QSharedPointer(new ctkDICOMInserterJob); + job->copyJobResponseSets(jobResponseSets); + job->setMaximumNumberOfRetry(d->MaximumNumberOfRetry); + job->setRetryDelay(d->RetryDelay); + job->setDatabaseFilename(d->DicomDatabase->databaseFilename()); + job->setTagsToPrecache(d->DicomDatabase->tagsToPrecache()); + job->setTagsToExcludeFromStorage(d->DicomDatabase->tagsToExcludeFromStorage()); + job->setPriority(priority); + + d->insertJob(job); +} + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +ctkDICOMDatabase* ctkDICOMScheduler::dicomDatabase() const +{ + Q_D(const ctkDICOMScheduler); + return d->DicomDatabase.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMScheduler::dicomDatabaseShared() const +{ + Q_D(const ctkDICOMScheduler); + return d->DicomDatabase; +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::setDicomDatabase(ctkDICOMDatabase& dicomDatabase) +{ + Q_D(ctkDICOMScheduler); + d->DicomDatabase = QSharedPointer(&dicomDatabase, skipDelete); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::setDicomDatabase(QSharedPointer dicomDatabase) +{ + Q_D(ctkDICOMScheduler); + d->DicomDatabase = dicomDatabase; +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::setFilters(const QMap& filters) +{ + Q_D(ctkDICOMScheduler); + d->Filters = filters; +} + +//---------------------------------------------------------------------------- +QMap ctkDICOMScheduler::filters() const +{ + Q_D(const ctkDICOMScheduler); + return d->Filters; +} + +//---------------------------------------------------------------------------- +int ctkDICOMScheduler::getNumberOfServers() +{ + Q_D(ctkDICOMScheduler); + return d->Servers.size(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMScheduler::getNumberOfQueryRetrieveServers() +{ + Q_D(ctkDICOMScheduler); + int numberOfServers = 0; + foreach (QSharedPointer server, d->Servers) + { + if (server && server->queryRetrieveEnabled()) + { + numberOfServers++; + } + } + return numberOfServers; +} + +//---------------------------------------------------------------------------- +int ctkDICOMScheduler::getNumberOfStorageServers() +{ + Q_D(ctkDICOMScheduler); + int numberOfServers = 0; + foreach (QSharedPointer server, d->Servers) + { + if (server && server->storageEnabled()) + { + numberOfServers++; + } + } + return numberOfServers; +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMScheduler::getNthServer(int id) +{ + Q_D(ctkDICOMScheduler); + if (id < 0 || id > d->Servers.size() - 1) + { + return nullptr; + } + return d->Servers.at(id).data(); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMScheduler::getServer(const QString& connectionName) +{ + Q_D(ctkDICOMScheduler); + ctkDICOMServer* server = this->getNthServer(this->getServerIndexFromName(connectionName)); + if (!server) + { + server = d->getServerFromProxyServersByConnectionName(connectionName); + } + return server; +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::addServer(ctkDICOMServer& server) +{ + Q_D(ctkDICOMScheduler); + QSharedPointer QSharedServer = QSharedPointer(&server, skipDelete); + d->Servers.append(QSharedServer); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::addServer(QSharedPointer server) +{ + Q_D(ctkDICOMScheduler); + d->Servers.append(server); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::removeServer(const QString& connectionName) +{ + this->removeNthServer(this->getServerIndexFromName(connectionName)); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::removeNthServer(int id) +{ + Q_D(ctkDICOMScheduler); + if (id < 0 || id > d->Servers.size() - 1) + { + return; + } + + d->Servers.removeAt(id); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::removeAllServers() +{ + Q_D(ctkDICOMScheduler); + d->Servers.clear(); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMScheduler::getServerNameFromIndex(int id) +{ + Q_D(ctkDICOMScheduler); + if (id < 0 || id > d->Servers.size() - 1) + { + return ""; + } + + QSharedPointer server = d->Servers.at(id); + if (!server) + { + return ""; + } + + return server->connectionName(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMScheduler::getServerIndexFromName(const QString& connectionName) +{ + Q_D(ctkDICOMScheduler); + for (int serverIndex = 0; serverIndex < d->Servers.size(); ++serverIndex) + { + QSharedPointer server = d->Servers.at(serverIndex); + if (server && server->connectionName() == connectionName) + { + // found + return serverIndex; + } + } + return -1; +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::waitForFinishByUIDs(const QStringList& patientIDs, + const QStringList& studyInstanceUIDs, + const QStringList& seriesInstanceUIDs, + const QStringList& sopInstanceUIDs) +{ + Q_D(ctkDICOMScheduler); + + if (patientIDs.count() == 0 && + studyInstanceUIDs.count() == 0 && + seriesInstanceUIDs.count() == 0 && + sopInstanceUIDs.count() == 0) + { + return; + } + + bool wait = true; + while (wait) + { + QCoreApplication::processEvents(); + d->ThreadPool->waitForDone(300); + + wait = false; + foreach (QSharedPointer job, d->JobsQueue) + { + if (!job) + { + continue; + } + + if (job->isPersistent()) + { + continue; + } + ctkDICOMJob* dicomJob = qobject_cast(job.data()); + if (!dicomJob) + { + continue; + } + + if ((!dicomJob->patientID().isEmpty() && patientIDs.contains(dicomJob->patientID())) || + (!dicomJob->studyInstanceUID().isEmpty() && studyInstanceUIDs.contains(dicomJob->studyInstanceUID())) || + (!dicomJob->seriesInstanceUID().isEmpty() && seriesInstanceUIDs.contains(dicomJob->seriesInstanceUID())) || + (!dicomJob->sopInstanceUID().isEmpty() && sopInstanceUIDs.contains(dicomJob->sopInstanceUID()))) + { + if (job->status() != ctkAbstractJob::JobStatus::Finished) + { + wait = true; + break; + } + } + } + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::stopJobsByUIDs(const QStringList& patientIDs, + const QStringList& studyInstanceUIDs, + const QStringList& seriesInstanceUIDs, + const QStringList& sopInstanceUIDs) +{ + Q_D(ctkDICOMScheduler); + + if (patientIDs.count() == 0 && + studyInstanceUIDs.count() == 0 && + seriesInstanceUIDs.count() == 0 && + sopInstanceUIDs.count() == 0) + { + return; + } + + QMutexLocker ml(&d->mMutex); + + // Stops jobs without a worker (in waiting) + foreach (QSharedPointer job, d->JobsQueue) + { + if (!job) + { + continue; + } + + if (job->isPersistent()) + { + continue; + } + + if (job->status() != ctkAbstractJob::JobStatus::Initialized) + { + continue; + } + + ctkDICOMJob* dicomJob = qobject_cast(job.data()); + if (!dicomJob) + { + qCritical() << Q_FUNC_INFO << " failed: unexpected type of job"; + continue; + } + + if ((!dicomJob->patientID().isEmpty() && patientIDs.contains(dicomJob->patientID())) || + (!dicomJob->studyInstanceUID().isEmpty() && studyInstanceUIDs.contains(dicomJob->studyInstanceUID())) || + (!dicomJob->seriesInstanceUID().isEmpty() && seriesInstanceUIDs.contains(dicomJob->seriesInstanceUID())) || + (!dicomJob->sopInstanceUID().isEmpty() && sopInstanceUIDs.contains(dicomJob->sopInstanceUID()))) + { + job->setStatus(ctkAbstractJob::JobStatus::Stopped); + this->deleteJob(job->jobUID()); + } + } + + // Stops queued and running jobs + foreach (QSharedPointer worker, d->Workers) + { + QSharedPointer job = worker->jobShared(); + if (!job) + { + continue; + } + + if (job->isPersistent()) + { + continue; + } + + ctkDICOMJob* dicomJob = qobject_cast(job.data()); + if (!dicomJob) + { + qCritical() << Q_FUNC_INFO << " failed: unexpected type of job"; + continue; + } + + if ((!dicomJob->patientID().isEmpty() && patientIDs.contains(dicomJob->patientID())) || + (!dicomJob->studyInstanceUID().isEmpty() && studyInstanceUIDs.contains(dicomJob->studyInstanceUID())) || + (!dicomJob->seriesInstanceUID().isEmpty() && seriesInstanceUIDs.contains(dicomJob->seriesInstanceUID())) || + (!dicomJob->sopInstanceUID().isEmpty() && sopInstanceUIDs.contains(dicomJob->sopInstanceUID()))) + { + job->setStatus(ctkAbstractJob::JobStatus::Stopped); + worker->cancel(); + } + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::raiseJobsPriorityForSeries(const QStringList& selectedSeriesInstanceUIDs, + QThread::Priority priority) +{ + Q_D(ctkDICOMScheduler); + + if (selectedSeriesInstanceUIDs.count() == 0) + { + return; + } + + QMutexLocker ml(&d->mMutex); + foreach (QSharedPointer job, d->JobsQueue) + { + if (job->isPersistent()) + { + continue; + } + + ctkDICOMJob* dicomJob = qobject_cast(job.data()); + if (!dicomJob) + { + qCritical() << Q_FUNC_INFO << " failed: unexpected type of job"; + continue; + } + + if (!selectedSeriesInstanceUIDs.contains(dicomJob->seriesInstanceUID())) + { + priority = QThread::Priority::LowPriority; + } + + job->setPriority(priority); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMScheduler::setMaximumPatientsQuery(int maximumPatientsQuery) +{ + Q_D(ctkDICOMScheduler); + d->MaximumPatientsQuery = maximumPatientsQuery; +} + +//------------------------------------------------------------------------------ +int ctkDICOMScheduler::maximumPatientsQuery() +{ + Q_D(const ctkDICOMScheduler); + return d->MaximumPatientsQuery; +} + +//---------------------------------------------------------------------------- +ctkDICOMStorageListenerJob* ctkDICOMScheduler::listenerJob() +{ + Q_D(ctkDICOMScheduler); + QMutexLocker ml(&d->mMutex); + foreach (QSharedPointer job, d->JobsQueue) + { + QSharedPointer listenerJob = + qSharedPointerObjectCast(job); + if (listenerJob) + { + return listenerJob.data(); + } + } + + return nullptr; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMScheduler::isStorageListenerActive() +{ + ctkDICOMStorageListenerJob* listenerJob = this->listenerJob(); + if (listenerJob && + listenerJob->status() == ctkAbstractJob::JobStatus::Running) + { + return true; + } + return false; +} diff --git a/Libs/DICOM/Core/ctkDICOMScheduler.h b/Libs/DICOM/Core/ctkDICOMScheduler.h new file mode 100644 index 0000000000..8f8d971a36 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMScheduler.h @@ -0,0 +1,198 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMScheduler_h +#define __ctkDICOMScheduler_h + +// Qt includes +#include +#include + +// ctkCore includes +#include +class ctkAbstractJob; + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMDatabase.h" +class ctkDICOMJob; +class ctkDICOMIndexer; +class ctkDICOMSchedulerPrivate; +class ctkDICOMServer; +class ctkDICOMStorageListenerJob; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMScheduler : public ctkJobScheduler +{ + Q_OBJECT + Q_PROPERTY(int maximumThreadCount READ maximumThreadCount WRITE setMaximumThreadCount); + Q_PROPERTY(int maximumNumberOfRetry READ maximumNumberOfRetry WRITE setMaximumNumberOfRetry); + Q_PROPERTY(int retryDelay READ retryDelay WRITE setRetryDelay); + Q_PROPERTY(int maximumPatientsQuery READ maximumPatientsQuery WRITE setMaximumPatientsQuery); + +public: + typedef ctkJobScheduler Superclass; + explicit ctkDICOMScheduler(QObject* parent = 0); + virtual ~ctkDICOMScheduler(); + + /// Query Patients applying filters on all servers. + /// The method spans a ctkDICOMQueryJob for each server. + Q_INVOKABLE void queryPatients(QThread::Priority priority = QThread::LowPriority); + + /// Query Studies applying filters on all servers. + /// The method spans a ctkDICOMQueryJob for each server. + Q_INVOKABLE void queryStudies(const QString& patientID, + QThread::Priority priority = QThread::LowPriority); + + /// Query Series applying filters on all servers. + /// The method spans a ctkDICOMQueryJob for each server. + Q_INVOKABLE void querySeries(const QString& patientID, + const QString& studyInstanceUID, + QThread::Priority priority = QThread::LowPriority); + + /// Query Instances applying filters on all servers. + /// The method spans a ctkDICOMQueryJob for each server. + Q_INVOKABLE void queryInstances(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + QThread::Priority priority = QThread::LowPriority); + + /// Retrieve Study. + /// The method spans a ctkDICOMRetrieveJob for each server. + Q_INVOKABLE void retrieveStudy(const QString& patientID, + const QString& studyInstanceUID, + QThread::Priority priority = QThread::LowPriority); + + /// Retrieve Series. + /// The method spans a ctkDICOMRetrieveJob for each server. + Q_INVOKABLE void retrieveSeries(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + QThread::Priority priority = QThread::LowPriority); + + /// Retrieve SOPInstance. + /// The method spans a ctkDICOMRetrieveJob for each server. + Q_INVOKABLE void retrieveSOPInstance(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& SOPInstanceUID, + QThread::Priority priority = QThread::LowPriority); + + /// Start a storage listener + Q_INVOKABLE void startListener(int port, + const QString &AETitle, + QThread::Priority priority = QThread::LowPriority); + + ///@{ + /// Insert results from a job + void insertJobResponseSet(const QSharedPointer& jobResponseSet, + QThread::Priority priority = QThread::HighPriority); + void insertJobResponseSets(const QList>& jobResponseSets, + QThread::Priority priority = QThread::HighPriority); + ///@} + + /// Return the Dicom Database. + Q_INVOKABLE ctkDICOMDatabase* dicomDatabase() const; + /// Return Dicom Database as a shared pointer + /// (not Python-wrappable). + QSharedPointer dicomDatabaseShared() const; + + /// Set the Dicom Database. + Q_INVOKABLE void setDicomDatabase(ctkDICOMDatabase& dicomDatabase); + /// Set the Dicom Database as a shared pointer + /// (not Python-wrappable). + void setDicomDatabase(QSharedPointer dicomDatabase); + + ///@{ + /// Filters are keyword/value pairs as generated by + /// the ctkDICOMWidgets in a human readable (and editable) + /// format. The Query is responsible for converting these + /// into the appropriate dicom syntax for the C-Find + /// Currently supports the keys: Name, Study, Series, ID, Modalities, + /// StartDate and EndDate + /// Key DICOM Tag Type Example + /// ----------------------------------------------------------- + /// Name DCM_PatientName QString JOHNDOE + /// Study DCM_StudyDescription QString + /// Series DCM_SeriesDescription QString + /// ID DCM_PatientID QString + /// Modalities DCM_ModalitiesInStudy QStringList CT, MR, MN + /// StartDate DCM_StudyDate QString 20090101 + /// EndDate DCM_StudyDate QString 20091231 + /// No filter (empty) by default. + Q_INVOKABLE void setFilters(const QMap& filters); + Q_INVOKABLE QMap filters() const; + ///@} + + ///@{ + /// Servers + Q_INVOKABLE int getNumberOfServers(); + Q_INVOKABLE int getNumberOfQueryRetrieveServers(); + Q_INVOKABLE int getNumberOfStorageServers(); + Q_INVOKABLE ctkDICOMServer* getNthServer(int id); + Q_INVOKABLE ctkDICOMServer* getServer(const QString& connectionName); + Q_INVOKABLE void addServer(ctkDICOMServer& server); + void addServer(QSharedPointer server); + Q_INVOKABLE void removeServer(const QString& connectionName); + Q_INVOKABLE void removeNthServer(int id); + Q_INVOKABLE void removeAllServers(); + Q_INVOKABLE QString getServerNameFromIndex(int id); + Q_INVOKABLE int getServerIndexFromName(const QString& connectionName); + ///@} + + ///@{ + /// Jobs managment + Q_INVOKABLE void waitForFinishByUIDs(const QStringList& patientIDs = {}, + const QStringList& studyInstanceUIDs = {}, + const QStringList& seriesInstanceUIDs = {}, + const QStringList& sopInstanceUIDs = {}); + Q_INVOKABLE void stopJobsByUIDs(const QStringList& patientIDs = {}, + const QStringList& studyInstanceUIDs = {}, + const QStringList& seriesInstanceUIDs = {}, + const QStringList& sopInstanceUIDs = {}); + Q_INVOKABLE void raiseJobsPriorityForSeries(const QStringList& selectedSeriesInstanceUIDs, + QThread::Priority priority = QThread::HighestPriority); + ///@} + + ///@{ + /// maximum number of responses allowed in one query + /// when query is at Patient level. Default is 25. + void setMaximumPatientsQuery(int maximumPatientsQuery); + int maximumPatientsQuery(); + ///@} + + ///@{ + /// Return the listener Job. + Q_INVOKABLE ctkDICOMStorageListenerJob* listenerJob(); + Q_INVOKABLE bool isStorageListenerActive(); + ///@} + +protected: + ctkDICOMScheduler(ctkDICOMSchedulerPrivate* pimpl, QObject* parent); + +private: + Q_DECLARE_PRIVATE(ctkDICOMScheduler); + Q_DISABLE_COPY(ctkDICOMScheduler); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMScheduler_p.h b/Libs/DICOM/Core/ctkDICOMScheduler_p.h new file mode 100644 index 0000000000..710a506ab4 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMScheduler_p.h @@ -0,0 +1,71 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMQueryJobPrivate_h +#define __ctkDICOMQueryJobPrivate_h + +// Qt includes +#include +#include +#include +#include +class QVariant; + +// ctkCore includes +#include +class ctkAbstractJob; +class ctkAbstractWorker; +class ctkDICOMDatabase; +class ctkDICOMServer; + +// ctkDICOMCore includes +#include "ctkDICOMScheduler.h" + +//------------------------------------------------------------------------------ +struct ThumbnailUID +{ + QString studyInstanceUID; + QString seriesInstanceUID; + QString SOPInstanceUID; +} ; + +//------------------------------------------------------------------------------ +class ctkDICOMSchedulerPrivate : public ctkJobSchedulerPrivate +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMScheduler); + +public: + ctkDICOMSchedulerPrivate(ctkDICOMScheduler& obj); + virtual ~ctkDICOMSchedulerPrivate(); + + ctkDICOMServer* getServerFromProxyServersByConnectionName(const QString&); + + QSharedPointer DicomDatabase; + QList> Servers; + QMap Filters; + + int MaximumPatientsQuery{25}; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMServer.cpp b/Libs/DICOM/Core/ctkDICOMServer.cpp new file mode 100644 index 0000000000..b1edfb9ad0 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMServer.cpp @@ -0,0 +1,329 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMServer.h" + +static ctkLogger logger("org.commontk.dicom.DICOMServer"); + +//------------------------------------------------------------------------------ +class ctkDICOMServerPrivate : public QObject +{ + Q_DECLARE_PUBLIC(ctkDICOMServer); + +protected: + ctkDICOMServer* const q_ptr; + +public: + ctkDICOMServerPrivate(ctkDICOMServer& obj); + ~ctkDICOMServerPrivate() = default; + + QString ConnectionName; + bool QueryRetrieveEnabled; + bool StorageEnabled; + QString CallingAETitle; + QString CalledAETitle; + QString Host; + int Port; + ctkDICOMServer::RetrieveProtocol RetrieveProtocol; + bool KeepAssociationOpen; + QString MoveDestinationAETitle; + int ConnectionTimeout; + QSharedPointer ProxyServer; +}; + +//------------------------------------------------------------------------------ +// ctkDICOMServerPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMServerPrivate::ctkDICOMServerPrivate(ctkDICOMServer& obj) + : q_ptr(&obj) +{ + this->ConnectionName = ""; + this->CallingAETitle = ""; + this->CalledAETitle = ""; + this->Host = ""; + this->MoveDestinationAETitle = ""; + this->QueryRetrieveEnabled = true; + this->StorageEnabled = true; + this->KeepAssociationOpen = false; + this->ConnectionTimeout = 10; + this->Port = 80; + this->RetrieveProtocol = ctkDICOMServer::RetrieveProtocol::CGET; + this->ProxyServer = nullptr; +} + +//------------------------------------------------------------------------------ +// ctkDICOMServer methods + +//------------------------------------------------------------------------------ +ctkDICOMServer::ctkDICOMServer(QObject* parent) + : QObject(parent), + d_ptr(new ctkDICOMServerPrivate(*this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMServer::~ctkDICOMServer() = default; + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setConnectionName(const QString& connectionName) +{ + Q_D(ctkDICOMServer); + d->ConnectionName = connectionName; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMServer::connectionName() const +{ + Q_D(const ctkDICOMServer); + return d->ConnectionName; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setQueryRetrieveEnabled(bool queryRetrieveEnabled) +{ + Q_D(ctkDICOMServer); + d->QueryRetrieveEnabled = queryRetrieveEnabled; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMServer::queryRetrieveEnabled() const +{ + Q_D(const ctkDICOMServer); + return d->QueryRetrieveEnabled; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setStorageEnabled(bool storageEnabled) +{ + Q_D(ctkDICOMServer); + d->StorageEnabled = storageEnabled; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMServer::storageEnabled() const +{ + Q_D(const ctkDICOMServer); + return d->StorageEnabled; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setCallingAETitle(const QString& callingAETitle) +{ + Q_D(ctkDICOMServer); + d->CallingAETitle = callingAETitle; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMServer::callingAETitle() const +{ + Q_D(const ctkDICOMServer); + return d->CallingAETitle; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setCalledAETitle(const QString& calledAETitle) +{ + Q_D(ctkDICOMServer); + d->CalledAETitle = calledAETitle; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMServer::calledAETitle() const +{ + Q_D(const ctkDICOMServer); + return d->CalledAETitle; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setHost(const QString& host) +{ + Q_D(ctkDICOMServer); + d->Host = host; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMServer::host() const +{ + Q_D(const ctkDICOMServer); + return d->Host; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setPort(int port) +{ + Q_D(ctkDICOMServer); + d->Port = port; +} + +//------------------------------------------------------------------------------ +int ctkDICOMServer::port() const +{ + Q_D(const ctkDICOMServer); + return d->Port; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setRetrieveProtocol(RetrieveProtocol protocol) +{ + Q_D(ctkDICOMServer); + d->RetrieveProtocol = protocol; +} + +//------------------------------------------------------------------------------ +ctkDICOMServer::RetrieveProtocol ctkDICOMServer::retrieveProtocol() const +{ + Q_D(const ctkDICOMServer); + return d->RetrieveProtocol; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setRetrieveProtocolAsString(const QString& protocolString) +{ + Q_D(ctkDICOMServer); + + if (protocolString == "CGET") + { + d->RetrieveProtocol = RetrieveProtocol::CGET; + } + else if (protocolString == "CMOVE") + { + d->RetrieveProtocol = RetrieveProtocol::CMOVE; + } + /*else if (protocolString == "WADO") To Do + { + d->RetrieveProtocol = RetrieveProtocol::WADO; + }*/ +} + +//------------------------------------------------------------------------------ +QString ctkDICOMServer::retrieveProtocolAsString() const +{ + Q_D(const ctkDICOMServer); + + QString protocolString = ""; + switch (d->RetrieveProtocol) + { + case RetrieveProtocol::CGET: + protocolString = "CGET"; + break; + case RetrieveProtocol::CMOVE: + protocolString = "CMOVE"; + break; + /*case RetrieveProtocol::WADO: To Do + protocolString = "WADO"; + break; */ + default: + break; + } + + return protocolString; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setMoveDestinationAETitle(const QString& moveDestinationAETitle) +{ + Q_D(ctkDICOMServer); + if (moveDestinationAETitle != d->MoveDestinationAETitle) + { + d->MoveDestinationAETitle = moveDestinationAETitle; + } +} +//------------------------------------------------------------------------------ +QString ctkDICOMServer::moveDestinationAETitle() const +{ + Q_D(const ctkDICOMServer); + return d->MoveDestinationAETitle; +} + +//------------------------------------------------------------------------------ +void ctkDICOMServer::setKeepAssociationOpen(bool keepOpen) +{ + Q_D(ctkDICOMServer); + d->KeepAssociationOpen = keepOpen; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMServer::keepAssociationOpen() +{ + Q_D(const ctkDICOMServer); + return d->KeepAssociationOpen; +} + +//----------------------------------------------------------------------------- +void ctkDICOMServer::setConnectionTimeout(int timeout) +{ + Q_D(ctkDICOMServer); + d->ConnectionTimeout = timeout; +} + +//----------------------------------------------------------------------------- +int ctkDICOMServer::connectionTimeout() +{ + Q_D(const ctkDICOMServer); + return d->ConnectionTimeout; +} + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMServer::proxyServer() const +{ + Q_D(const ctkDICOMServer); + return d->ProxyServer.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMServer::proxyServerShared() const +{ + Q_D(const ctkDICOMServer); + return d->ProxyServer; +} + +//---------------------------------------------------------------------------- +void ctkDICOMServer::setProxyServer(ctkDICOMServer& proxyServer) +{ + Q_D(ctkDICOMServer); + d->ProxyServer = QSharedPointer(&proxyServer, skipDelete); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServer::setProxyServer(QSharedPointer proxyServer) +{ + Q_D(ctkDICOMServer); + d->ProxyServer = proxyServer; +} diff --git a/Libs/DICOM/Core/ctkDICOMServer.h b/Libs/DICOM/Core/ctkDICOMServer.h new file mode 100644 index 0000000000..ee62acc8b5 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMServer.h @@ -0,0 +1,154 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMServer_h +#define __ctkDICOMServer_h + +// Qt includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +class ctkDICOMServerPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMServer : public QObject +{ + Q_OBJECT + Q_ENUMS(RetrieveProtocol) + Q_PROPERTY(QString connectionName READ connectionName WRITE setConnectionName); + Q_PROPERTY(bool queryRetrieveEnabled READ queryRetrieveEnabled WRITE setQueryRetrieveEnabled); + Q_PROPERTY(bool storageEnabled READ storageEnabled WRITE setStorageEnabled); + Q_PROPERTY(QString callingAETitle READ callingAETitle WRITE setCallingAETitle); + Q_PROPERTY(QString calledAETitle READ calledAETitle WRITE setCalledAETitle); + Q_PROPERTY(QString host READ host WRITE setHost); + Q_PROPERTY(int port READ port WRITE setPort); + Q_PROPERTY(RetrieveProtocol retrieveProtocol READ retrieveProtocol WRITE setRetrieveProtocol); + Q_PROPERTY(QString moveDestinationAETitle READ moveDestinationAETitle WRITE setMoveDestinationAETitle); + Q_PROPERTY(bool keepAssociationOpen READ keepAssociationOpen WRITE setKeepAssociationOpen); + Q_PROPERTY(int connectionTimeout READ connectionTimeout WRITE setConnectionTimeout); + +public: + explicit ctkDICOMServer(QObject* parent = 0); + virtual ~ctkDICOMServer(); + + ///@{ + /// Name identifying the server + void setConnectionName(const QString& connectionName); + QString connectionName() const; + ///}@ + + ///@{ + /// Query/Retrieve operations + /// true as default + void setQueryRetrieveEnabled(bool queryRetrieveEnabled); + bool queryRetrieveEnabled() const; + ///}@ + + ///@{ + /// Storage operations + /// true as default + void setStorageEnabled(bool storageEnabled); + bool storageEnabled() const; + ///}@ + + ///@{ + /// CTK_AE - the AE string by which the peer host might + /// recognize your request + void setCallingAETitle(const QString& callingAETitle); + QString callingAETitle() const; + ///}@ + + ///@{ + /// CTK_AE - the AE of the service of peer host that you are calling + /// which tells the host what you are requesting + void setCalledAETitle(const QString& calledAETitle); + QString calledAETitle() const; + ///}@ + + ///@{ + /// Peer hostname being connected to + void setHost(const QString& host); + QString host() const; + ///}@ + + ///@{ + /// [0, 65365] port on peer host + /// 80 as default + void setPort(int port); + int port() const; + ///}@ + + ///@{ + /// Protocol for retrieval of query results. + /// CGET by default + enum RetrieveProtocol + { + CGET = 0, + CMOVE + // WADO // To Do + }; + void setRetrieveProtocol(RetrieveProtocol protocol); + RetrieveProtocol retrieveProtocol() const; + Q_INVOKABLE void setRetrieveProtocolAsString(const QString& protocolString); + Q_INVOKABLE QString retrieveProtocolAsString() 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(bool keepOpen); + bool keepAssociationOpen(); + ///}@ + + ///@{ + /// connection timeout in seconds, default 10 s. + void setConnectionTimeout(int timeout); + int connectionTimeout(); + ///}@ + + ///@{ + /// proxy server + Q_INVOKABLE ctkDICOMServer* proxyServer() const; + QSharedPointer proxyServerShared() const; + Q_INVOKABLE void setProxyServer(ctkDICOMServer& proxyServer); + void setProxyServer(QSharedPointer proxyServer); + ///}@ + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMServer); + Q_DISABLE_COPY(ctkDICOMServer); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMStorageListener.cpp b/Libs/DICOM/Core/ctkDICOMStorageListener.cpp new file mode 100644 index 0000000000..a4785e61f0 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListener.cpp @@ -0,0 +1,419 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMJobResponseSet.h" +#include "ctkDICOMStorageListener.h" + +// DCMTK includes +#include /* for DcmStorageSCP */ + +static ctkLogger logger("org.commontk.dicom.ctkDICOMStorageListener"); + +//------------------------------------------------------------------------------ +// A customized implementation so that Qt signals can be emitted +// when query results are obtained +class ctkDICOMStorageListenerSCUPrivate : public DcmStorageSCP +{ +public: + ctkDICOMStorageListener* listener; + ctkDICOMStorageListenerSCUPrivate() + { + this->listener = 0; + }; + ~ctkDICOMStorageListenerSCUPrivate() = default; + + virtual OFCondition acceptAssociations() + { + return DcmSCP::acceptAssociations(); + } + + virtual OFBool stopAfterCurrentAssociation(); + virtual OFBool stopAfterConnectionTimeout(); + + virtual OFCondition handleIncomingCommand(T_DIMSE_Message* incomingMsg, + const DcmPresentationContextInfo& presInfo); +}; + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerSCUPrivate methods + +//------------------------------------------------------------------------------ +OFBool ctkDICOMStorageListenerSCUPrivate::stopAfterCurrentAssociation() +{ + if (!this->listener || this->listener->wasCanceled()) + { + return OFTrue; + } + return OFFalse; +} + +//------------------------------------------------------------------------------ +OFBool ctkDICOMStorageListenerSCUPrivate::stopAfterConnectionTimeout() +{ + if (!this->listener || this->listener->wasCanceled()) + { + return OFTrue; + } + return OFFalse; +} + +//------------------------------------------------------------------------------ +OFCondition ctkDICOMStorageListenerSCUPrivate::handleIncomingCommand(T_DIMSE_Message* incomingMsg, + const DcmPresentationContextInfo& presInfo) +{ + OFCondition status = EC_IllegalParameter; + if (incomingMsg != NULL && !this->listener->wasCanceled()) + { + // check whether we've received a supported command + if (incomingMsg->CommandField == DIMSE_C_ECHO_RQ) + { + // handle incoming C-ECHO request + status = handleECHORequest(incomingMsg->msg.CEchoRQ, presInfo.presentationContextID); + } + else if (incomingMsg->CommandField == DIMSE_C_STORE_RQ) + { + // handle incoming C-STORE request + T_DIMSE_C_StoreRQ& storeReq = incomingMsg->msg.CStoreRQ; + Uint16 rspStatusCode = STATUS_STORE_Error_CannotUnderstand; + DcmDataset* reqDataset = new DcmDataset; + // receive dataset in memory + status = receiveSTORERequest(storeReq, presInfo.presentationContextID, reqDataset); + if (status.good()) + { + rspStatusCode = STATUS_Success; + } + + OFString instanceUID; + reqDataset->findAndGetOFString(DCM_SOPInstanceUID, instanceUID); + OFString seriesUID; + reqDataset->findAndGetOFString(DCM_SeriesInstanceUID, seriesUID); + OFString studyUID; + reqDataset->findAndGetOFString(DCM_StudyInstanceUID, studyUID); + emit this->listener->progress( + ctkDICOMStorageListener::tr("Got STORE request for %1").arg(instanceUID.c_str())); + emit this->listener->progress(0); + if (!this->listener->jobUID().isEmpty() && !this->listener->wasCanceled()) + { + QSharedPointer jobResponseSet = + QSharedPointer(new ctkDICOMJobResponseSet); + jobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::StoreSOPInstance); + jobResponseSet->setStudyInstanceUID(studyUID.c_str()); + jobResponseSet->setSeriesInstanceUID(seriesUID.c_str()); + jobResponseSet->setSOPInstanceUID(instanceUID.c_str()); + jobResponseSet->setConnectionName(this->listener->AETitle()); + jobResponseSet->setDataset(reqDataset); + jobResponseSet->setJobUID(this->listener->jobUID()); + jobResponseSet->setCopyFile(true); + + this->listener->addJobResponseSet(jobResponseSet); + + emit this->listener->progressJobDetail(jobResponseSet->toVariant()); + } + // send C-STORE response (with DIMSE status code) + if (status.good()) + { + status = sendSTOREResponse(presInfo.presentationContextID, storeReq, rspStatusCode); + } + else if (status == DIMSE_OUTOFRESOURCES) + { + // do not overwrite the previous error status + sendSTOREResponse(presInfo.presentationContextID, storeReq, STATUS_STORE_Refused_OutOfResources); + } + } + else + { + // unsupported command + OFString tempStr; + DCMNET_ERROR("cannot handle this kind of DIMSE command (0x" + << STD_NAMESPACE hex << STD_NAMESPACE setfill('0') << STD_NAMESPACE setw(4) + << OFstatic_cast(unsigned int, incomingMsg->CommandField) + << "), we are a Storage SCP only"); + DCMNET_DEBUG(DIMSE_dumpMessage(tempStr, *incomingMsg, DIMSE_INCOMING)); + status = DIMSE_BADCOMMANDTYPE; + } + } + return status; +} + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerPrivate + +//------------------------------------------------------------------------------ +class ctkDICOMStorageListenerPrivate +{ +public: + ctkDICOMStorageListenerPrivate(); + ~ctkDICOMStorageListenerPrivate() = default; + + QString findFile(const QStringList& nameFilters, const QString& subDir) const; + QString defaultConfigFile() const; + + QString ConnectionName; + QString AETitle; + int Port; + QString JobUID; + QList> JobResponseSets; + + ctkDICOMStorageListenerSCUPrivate SCU; + bool Canceled; +}; + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerPrivate::ctkDICOMStorageListenerPrivate() +{ + this->Port = 11112; + this->AETitle = "CTKSTORE"; + this->Canceled = false; + + this->SCU.setConnectionBlockingMode(DUL_NOBLOCK); + this->SCU.setACSETimeout(1); + this->SCU.setConnectionTimeout(1); + this->SCU.setRespondWithCalledAETitle(false); + this->SCU.setHostLookupEnabled(true); + this->SCU.setVerbosePCMode(false); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStorageListenerPrivate::defaultConfigFile() const +{ + QString data; + QString fileName(":/dicom/storescp.cfg"); + + QFile readFile(fileName); + if (readFile.open(QIODevice::ReadOnly)) + { + data = readFile.readAll(); + } + else + { + logger.error("Failed to find listener configuration file"); + return ""; + } + + readFile.close(); + + QString tmpDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + QString configFile = tmpDir + "/storescp.cfg"; + QFile writeFile(configFile); + + if (writeFile.open(QFile::WriteOnly | QFile::Text)) + { + QTextStream stream(&writeFile); + stream << data; + } + else + { + logger.error("Failed to find listener configuration file"); + return ""; + } + writeFile.close(); + + return configFile; +} + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListener methods + +//------------------------------------------------------------------------------ +ctkDICOMStorageListener::ctkDICOMStorageListener(QObject* parentObject) + : QObject(parentObject) + , d_ptr(new ctkDICOMStorageListenerPrivate) +{ + Q_D(ctkDICOMStorageListener); + d->SCU.listener = this; // give the dcmtk level access to this for emitting signals +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListener::~ctkDICOMStorageListener() +{ + Q_D(ctkDICOMStorageListener); + d->JobResponseSets.clear(); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMStorageListener::listen() +{ + Q_D(ctkDICOMStorageListener); + if (!this->initializeSCU()) + { + return false; + } + + OFCondition status = d->SCU.listen(); + if (status.bad() || d->Canceled) + { + logger.error(QString("SCP stopped, it was listening on port %1 : %2 ") + .arg(QString::number(d->Port)) + .arg(status.text())); + return false; + } + return true; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMStorageListener::wasCanceled() +{ + Q_D(const ctkDICOMStorageListener); + return d->Canceled; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::cancel() +{ + Q_D(ctkDICOMStorageListener); + d->Canceled = true; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMStorageListener::initializeSCU() +{ + Q_D(ctkDICOMStorageListener); + d->SCU.setPort(this->port()); + d->SCU.setAETitle(OFString(this->AETitle().toStdString().c_str())); + + /* load association negotiation profile from configuration file (if specified) */ + OFCondition status = d->SCU.loadAssociationConfiguration( + OFString(d->defaultConfigFile().toStdString().c_str()), "alldicom"); + if (status.bad()) + { + logger.error(QString("Cannot load association configuration: %1").arg(status.text())); + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::setAETitle(const QString& AETitle) +{ + Q_D(ctkDICOMStorageListener); + d->AETitle = AETitle; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStorageListener::AETitle() const +{ + Q_D(const ctkDICOMStorageListener); + return d->AETitle; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::setPort(int port) +{ + Q_D(ctkDICOMStorageListener); + d->Port = port; +} + +//------------------------------------------------------------------------------ +int ctkDICOMStorageListener::port() const +{ + Q_D(const ctkDICOMStorageListener); + return d->Port; +} + +//----------------------------------------------------------------------------- +void ctkDICOMStorageListener::setConnectionTimeout(int timeout) +{ + Q_D(ctkDICOMStorageListener); + d->SCU.setACSETimeout(timeout); + d->SCU.setConnectionTimeout(timeout); +} + +//----------------------------------------------------------------------------- +int ctkDICOMStorageListener::connectionTimeout() +{ + Q_D(const ctkDICOMStorageListener); + return d->SCU.getConnectionTimeout(); +} + +//------------------------------------------------------------------------------ +QList ctkDICOMStorageListener::jobResponseSets() const +{ + Q_D(const ctkDICOMStorageListener); + QList jobResponseSets; + foreach (QSharedPointer jobResponseSet, d->JobResponseSets) + { + jobResponseSets.append(jobResponseSet.data()); + } + + return jobResponseSets; +} + +//------------------------------------------------------------------------------ +QList> ctkDICOMStorageListener::jobResponseSetsShared() const +{ + Q_D(const ctkDICOMStorageListener); + return d->JobResponseSets; +} + +//------------------------------------------------------------------------------ +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::addJobResponseSet(ctkDICOMJobResponseSet& jobResponseSet) +{ + this->addJobResponseSet(QSharedPointer(&jobResponseSet, skipDelete)); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::addJobResponseSet(QSharedPointer jobResponseSet) +{ + Q_D(ctkDICOMStorageListener); + d->JobResponseSets.append(jobResponseSet); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::removeJobResponseSet(QSharedPointer jobResponseSet) +{ + Q_D(ctkDICOMStorageListener); + d->JobResponseSets.removeOne(jobResponseSet); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::setJobUID(const QString& jobUID) +{ + Q_D(ctkDICOMStorageListener); + d->JobUID = jobUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStorageListener::jobUID() const +{ + Q_D(const ctkDICOMStorageListener); + return d->JobUID; +} diff --git a/Libs/DICOM/Core/ctkDICOMStorageListener.h b/Libs/DICOM/Core/ctkDICOMStorageListener.h new file mode 100644 index 0000000000..d7f3c19af3 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListener.h @@ -0,0 +1,121 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +=========================================================================*/ + +#ifndef __ctkDICOMStorageListener_h +#define __ctkDICOMStorageListener_h + +// Qt includes +#include +#include +#include +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +class ctkDICOMJobResponseSet; +class ctkDICOMStorageListenerPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMStorageListener : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString AETitle READ AETitle WRITE setAETitle); + Q_PROPERTY(int port READ port WRITE setPort); + Q_PROPERTY(int connectionTimeout READ connectionTimeout WRITE setConnectionTimeout); + +public: + explicit ctkDICOMStorageListener(QObject* parent = 0); + virtual ~ctkDICOMStorageListener(); + + ///@{ + /// Storage AE title + /// "CTKSTORE" by default + void setAETitle(const QString& AETitle); + QString AETitle() const; + ///@} + + ///@{ + /// Storage port + /// 11112 by default + void setPort(int port); + int port() const; + ///@} + + ///@{ + /// Connection timeout + /// 1 sec by default + void setConnectionTimeout(int timeout); + int connectionTimeout(); + ///@} + + ///@{ + /// Access the list of datasets from the last operation. + Q_INVOKABLE QList jobResponseSets() const; + QList> jobResponseSetsShared() const; + Q_INVOKABLE void addJobResponseSet(ctkDICOMJobResponseSet& jobResponseSet); + void addJobResponseSet(QSharedPointer jobResponseSet); + void removeJobResponseSet(QSharedPointer jobResponseSet); + Q_INVOKABLE void setJobUID(const QString& jobUID); + Q_INVOKABLE QString jobUID() const; + ///@} + + /// Start listen connection. + bool listen(); + + /// operation is canceled? + Q_INVOKABLE bool wasCanceled(); + +Q_SIGNALS: + /// Signal is emitted inside the listener() function. It ranges from 0 to 100. + /// In case of an error, you are assured that the progress value 100 is fired + void progress(int progress); + /// Signal is emitted inside the listener() function. It sends the different step + /// the function is at. + void progress(const QString& message); + /// Signal is emitted inside the listener() function. It sends + /// detailed feedback for debugging + void debug(const QString& message); + /// Signal is emitted inside the listener() function. It send any error messages + void error(const QString& message); + /// Signal is emitted inside the listener() function when finished with value + /// true for success or false for error + void done(bool error); + /// Signal is emitted inside the listener() function when a frame has been fetched + void progressJobDetail(QVariant); + +public Q_SLOTS: + void cancel(); + +protected: + bool initializeSCU(); + + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMStorageListener); + Q_DISABLE_COPY(ctkDICOMStorageListener); + + friend class ctkDICOMStorageListenerSCUPrivate; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerJob.cpp b/Libs/DICOM/Core/ctkDICOMStorageListenerJob.cpp new file mode 100644 index 0000000000..c1715ee4ad --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerJob.cpp @@ -0,0 +1,141 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMStorageListenerJob_p.h" +#include "ctkDICOMStorageListenerWorker.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMStorageListenerJob"); + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerJobPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerJobPrivate::ctkDICOMStorageListenerJobPrivate(ctkDICOMStorageListenerJob* object) + : q_ptr(object) +{ + this->AETitle = "CTKSTORE"; + this->Port = 11112; + this->ConnectionTimeout = 1; +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerJobPrivate::~ctkDICOMStorageListenerJobPrivate() = default; + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerJob methods + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerJob::ctkDICOMStorageListenerJob() + : d_ptr(new ctkDICOMStorageListenerJobPrivate(this)) +{ + this->Persistent = true; +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerJob::ctkDICOMStorageListenerJob(ctkDICOMStorageListenerJobPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerJob::~ctkDICOMStorageListenerJob() = default; + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerJob::setPort(int port) +{ + Q_D(ctkDICOMStorageListenerJob); + d->Port = port; +} + +//---------------------------------------------------------------------------- +int ctkDICOMStorageListenerJob::port() const +{ + Q_D(const ctkDICOMStorageListenerJob); + return d->Port; +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerJob::setAETitle(const QString& AETitle) +{ + Q_D(ctkDICOMStorageListenerJob); + d->AETitle = AETitle; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMStorageListenerJob::AETitle() const +{ + Q_D(const ctkDICOMStorageListenerJob); + return d->AETitle; +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerJob::setConnectionTimeout(int timeout) +{ + Q_D(ctkDICOMStorageListenerJob); + d->ConnectionTimeout = timeout; +} + +//---------------------------------------------------------------------------- +int ctkDICOMStorageListenerJob::connectionTimeout() const +{ + Q_D(const ctkDICOMStorageListenerJob); + return d->ConnectionTimeout; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMStorageListenerJob::loggerReport(const QString& status) const +{ + return QString("ctkDICOMStorageListenerJob: listener job %1.\n" + "JobUID: %2\n") + .arg(status) + .arg(this->jobUID()); +} +//------------------------------------------------------------------------------ +ctkAbstractJob* ctkDICOMStorageListenerJob::clone() const +{ + ctkDICOMStorageListenerJob* newListenerJob = new ctkDICOMStorageListenerJob; + newListenerJob->setAETitle(this->AETitle()); + newListenerJob->setPort(this->port()); + newListenerJob->setConnectionTimeout(this->connectionTimeout()); + newListenerJob->setMaximumNumberOfRetry(this->maximumNumberOfRetry()); + newListenerJob->setRetryDelay(this->retryDelay()); + newListenerJob->setRetryCounter(this->retryCounter()); + newListenerJob->setIsPersistent(this->isPersistent()); + newListenerJob->setMaximumConcurrentJobsPerType(this->maximumConcurrentJobsPerType()); + newListenerJob->setPriority(this->priority()); + + return newListenerJob; +} + +//------------------------------------------------------------------------------ +ctkAbstractWorker* ctkDICOMStorageListenerJob::createWorker() +{ + ctkDICOMStorageListenerWorker* worker = + new ctkDICOMStorageListenerWorker; + worker->setJob(*this); + return worker; +} diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerJob.h b/Libs/DICOM/Core/ctkDICOMStorageListenerJob.h new file mode 100644 index 0000000000..1ea17734e3 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerJob.h @@ -0,0 +1,95 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMStorageListenerJob_h +#define __ctkDICOMStorageListenerJob_h + +// Qt includes +#include +#include +#include + +// ctkCore includes +class ctkAbstractWorker; + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMJob.h" +class ctkDICOMStorageListenerJobPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMStorageListenerJob : public ctkDICOMJob +{ + Q_OBJECT + Q_PROPERTY(int port READ port WRITE setPort); + Q_PROPERTY(QString AETitle READ AETitle WRITE setAETitle); + Q_PROPERTY(int connectionTimeout READ connectionTimeout WRITE setConnectionTimeout); + +public: + typedef ctkDICOMJob Superclass; + explicit ctkDICOMStorageListenerJob(); + virtual ~ctkDICOMStorageListenerJob(); + + ///@{ + /// Port, default: 11112 + void setPort(int port); + int port() const; + ///@} + + ///@{ + /// AETitle, default: CTKSTORE + void setAETitle(const QString& AETitle); + QString AETitle() const; + ///@} + + ///@{ + /// Connection timeout, default 1 sec. + void setConnectionTimeout(int timeout); + int connectionTimeout() const; + ///@} + + /// Logger report string formatting for specific task + Q_INVOKABLE QString loggerReport(const QString& status) const override; + + /// \see ctkAbstractJob::clone() + Q_INVOKABLE ctkAbstractJob* clone() const override; + + /// Generate worker for job + Q_INVOKABLE ctkAbstractWorker* createWorker() override; + +protected: + QScopedPointer d_ptr; + + /// Constructor allowing derived class to specify a specialized pimpl. + /// + /// \note You are responsible to call init() in the constructor of + /// derived class. Doing so ensures the derived class is fully + /// instantiated in case virtual method are called within init() itself. + ctkDICOMStorageListenerJob(ctkDICOMStorageListenerJobPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMStorageListenerJob); + Q_DISABLE_COPY(ctkDICOMStorageListenerJob); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerJob_p.h b/Libs/DICOM/Core/ctkDICOMStorageListenerJob_p.h new file mode 100644 index 0000000000..00edd4512b --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerJob_p.h @@ -0,0 +1,48 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMStorageListenerJobPrivate_h +#define __ctkDICOMStorageListenerJobPrivate_h + +// ctkDICOMCore includes +#include "ctkDICOMStorageListenerJob.h" + +//------------------------------------------------------------------------------ +class ctkDICOMStorageListenerJobPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMStorageListenerJob) + +protected: + ctkDICOMStorageListenerJob* const q_ptr; + +public: + ctkDICOMStorageListenerJobPrivate(ctkDICOMStorageListenerJob* object); + virtual ~ctkDICOMStorageListenerJobPrivate(); + + QString AETitle; + int Port; + int ConnectionTimeout; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.cpp b/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.cpp new file mode 100644 index 0000000000..2641e81419 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.cpp @@ -0,0 +1,206 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMJobResponseSet.h" +#include "ctkDICOMScheduler.h" +#include "ctkDICOMStorageListenerJob.h" +#include "ctkDICOMStorageListenerWorker_p.h" + +static ctkLogger logger("org.commontk.dicom.ctkDICOMStorageListenerWorker"); + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerWorkerPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerWorkerPrivate::ctkDICOMStorageListenerWorkerPrivate(ctkDICOMStorageListenerWorker* object) + : q_ptr(object) +{ + this->StorageListener = QSharedPointer(new ctkDICOMStorageListener); +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerWorkerPrivate::~ctkDICOMStorageListenerWorkerPrivate() = default; + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListenerWorkerPrivate::setStorageListenerParameters() +{ + Q_Q(ctkDICOMStorageListenerWorker); + + QSharedPointer storageListenerJob = + qSharedPointerObjectCast(q->Job); + if (!storageListenerJob) + { + return; + } + + this->StorageListener->setAETitle(storageListenerJob->AETitle()); + this->StorageListener->setPort(storageListenerJob->port()); + this->StorageListener->setConnectionTimeout(storageListenerJob->connectionTimeout()); + this->StorageListener->setJobUID(storageListenerJob->jobUID()); + + QObject::connect(this->StorageListener.data(), SIGNAL(progressJobDetail(QVariant)), + storageListenerJob.data(), SIGNAL(progressJobDetail(QVariant)), + Qt::DirectConnection); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListenerWorkerPrivate::init() +{ + Q_Q(ctkDICOMStorageListenerWorker); + // To Do: this insert should happen in batch of 10 frames (configurable), + // instead of every 1 sec. + // This would avoid memory usage spikes when requesting a series or study with a lot of frames. + // i.e. the slot should be connected to progressJobDetail from this->StorageListener. + // The slot should have a counter. When the counter > batchLimit -> insert + // NOTE: the memory release should happen as soon as we insert the response. + QTimer* timer = new QTimer(this); + connect(timer, SIGNAL(timeout()), q, SLOT(onInsertJobDetail())); + timer->start(1000); +} + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerWorker methods + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerWorker::ctkDICOMStorageListenerWorker() + : d_ptr(new ctkDICOMStorageListenerWorkerPrivate(this)) +{ + Q_D(ctkDICOMStorageListenerWorker); + d->init(); +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerWorker::ctkDICOMStorageListenerWorker(ctkDICOMStorageListenerWorkerPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerWorker::~ctkDICOMStorageListenerWorker() = default; + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerWorker::cancel() +{ + Q_D(const ctkDICOMStorageListenerWorker); + d->StorageListener->cancel(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerWorker::run() +{ + Q_D(const ctkDICOMStorageListenerWorker); + QSharedPointer storageListenerJob = + qSharedPointerObjectCast(this->Job); + if (!storageListenerJob) + { + return; + } + + QSharedPointer scheduler = + qSharedPointerObjectCast(this->Scheduler); + if (!scheduler + || storageListenerJob->status() == ctkAbstractJob::JobStatus::Stopped) + { + emit storageListenerJob->canceled(); + this->onJobCanceled(); + storageListenerJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + + storageListenerJob->setStatus(ctkAbstractJob::JobStatus::Running); + emit storageListenerJob->started(); + + logger.debug(QString("ctkDICOMStorageListenerWorker : running job %1 in thread %2.\n") + .arg(storageListenerJob->jobUID()) + .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); + + + if (!d->StorageListener->listen()) + { + emit storageListenerJob->canceled(); + this->onJobCanceled(); + storageListenerJob->setStatus(ctkAbstractJob::JobStatus::Finished); + return; + } + + storageListenerJob->setStatus(ctkAbstractJob::JobStatus::Finished); + emit storageListenerJob->finished(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerWorker::setJob(QSharedPointer job) +{ + Q_D(ctkDICOMStorageListenerWorker); + + QSharedPointer storageListenerJob = + qSharedPointerObjectCast(job); + if (!storageListenerJob) + { + return; + } + + this->Superclass::setJob(job); + d->setStorageListenerParameters(); +} + +//---------------------------------------------------------------------------- +ctkDICOMStorageListener* ctkDICOMStorageListenerWorker::storageListener() const +{ + Q_D(const ctkDICOMStorageListenerWorker); + return d->StorageListener.data(); +} + +//------------------------------------------------------------------------------ +QSharedPointer ctkDICOMStorageListenerWorker::storageListenerShared() const +{ + Q_D(const ctkDICOMStorageListenerWorker); + return d->StorageListener; +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerWorker::onInsertJobDetail() +{ + Q_D(ctkDICOMStorageListenerWorker); + + QSharedPointer scheduler = + qSharedPointerObjectCast(this->Scheduler); + if (!scheduler) + { + return; + } + + QList> jobResponseSets = + d->StorageListener->jobResponseSetsShared(); + scheduler->insertJobResponseSets(jobResponseSets); + foreach (QSharedPointer jobResponseSet, jobResponseSets) + { + d->StorageListener->removeJobResponseSet(jobResponseSet); + } +} diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.h b/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.h new file mode 100644 index 0000000000..e890054dbe --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.h @@ -0,0 +1,82 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMStorageListenerWorker_h +#define __ctkDICOMStorageListenerWorker_h + +// Qt includes +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkAbstractWorker.h" +class ctkDICOMStorageListener; +class ctkDICOMStorageListenerWorkerPrivate; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMStorageListenerWorker : public ctkAbstractWorker +{ + Q_OBJECT + +public: + typedef ctkAbstractWorker Superclass; + explicit ctkDICOMStorageListenerWorker(); + virtual ~ctkDICOMStorageListenerWorker(); + + /// Execute worker + void run() override; + + /// Cancel worker + void cancel() override; + + /// Job + void setJob(QSharedPointer job) override; + using ctkAbstractWorker::setJob; + + ///@{ + /// Retriever + QSharedPointer storageListenerShared() const; + Q_INVOKABLE ctkDICOMStorageListener* storageListener() const; + ///@} + +public slots: + void onInsertJobDetail(); + +protected: + QScopedPointer d_ptr; + + /// Constructor allowing derived class to specify a specialized pimpl. + /// + /// \note You are responsible to call init() in the constructor of + /// derived class. Doing so ensures the derived class is fully + /// instantiated in case virtual method are called within init() itself. + ctkDICOMStorageListenerWorker(ctkDICOMStorageListenerWorkerPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMStorageListenerWorker); + Q_DISABLE_COPY(ctkDICOMStorageListenerWorker); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerWorker_p.h b/Libs/DICOM/Core/ctkDICOMStorageListenerWorker_p.h new file mode 100644 index 0000000000..e15daaf4c7 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerWorker_p.h @@ -0,0 +1,50 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMStorageListenerWorkerPrivate_h +#define __ctkDICOMStorageListenerWorkerPrivate_h + +// ctkDICOMCore includes +#include "ctkDICOMStorageListener.h" +#include "ctkDICOMStorageListenerWorker.h" + +//------------------------------------------------------------------------------ +class ctkDICOMStorageListenerWorkerPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMStorageListenerWorker) + +protected: + ctkDICOMStorageListenerWorker* const q_ptr; + +public: + ctkDICOMStorageListenerWorkerPrivate(ctkDICOMStorageListenerWorker* object); + virtual ~ctkDICOMStorageListenerWorkerPrivate(); + + void init(); + void setStorageListenerParameters(); + + QSharedPointer StorageListener; +}; + +#endif diff --git a/Libs/DICOM/Widgets/CMakeLists.txt b/Libs/DICOM/Widgets/CMakeLists.txt index 7c55927199..c238cab113 100644 --- a/Libs/DICOM/Widgets/CMakeLists.txt +++ b/Libs/DICOM/Widgets/CMakeLists.txt @@ -33,8 +33,16 @@ set(KIT_SRCS ctkDICOMQueryWidget.h ctkDICOMObjectListWidget.cpp ctkDICOMObjectListWidget.h + ctkDICOMPatientItemWidget.cpp + ctkDICOMPatientItemWidget.h + ctkDICOMSeriesItemWidget.cpp + ctkDICOMSeriesItemWidget.h ctkDICOMServerNodeWidget.cpp ctkDICOMServerNodeWidget.h + ctkDICOMServerNodeWidget2.cpp + ctkDICOMServerNodeWidget2.h + ctkDICOMStudyItemWidget.cpp + ctkDICOMStudyItemWidget.h ctkDICOMTableManager.h ctkDICOMTableManager.cpp ctkDICOMTableView.cpp @@ -43,6 +51,8 @@ set(KIT_SRCS ctkDICOMThumbnailGenerator.h ctkDICOMThumbnailListWidget.cpp ctkDICOMThumbnailListWidget.h + ctkDICOMVisualBrowserWidget.cpp + ctkDICOMVisualBrowserWidget.h ) # Headers that should run through moc @@ -57,11 +67,16 @@ set(KIT_MOC_SRCS ctkDICOMObjectModel.h ctkDICOMQueryRetrieveWidget.h ctkDICOMQueryWidget.h + ctkDICOMPatientItemWidget.h + ctkDICOMSeriesItemWidget.h ctkDICOMServerNodeWidget.h + ctkDICOMServerNodeWidget2.h + ctkDICOMStudyItemWidget.h ctkDICOMTableManager.h ctkDICOMTableView.h ctkDICOMThumbnailGenerator.h ctkDICOMThumbnailListWidget.h + ctkDICOMVisualBrowserWidget.h ) # UI files - includes new widgets @@ -74,19 +89,27 @@ set(KIT_UI_FORMS Resources/UI/ctkDICOMQueryRetrieveWidget.ui Resources/UI/ctkDICOMQueryWidget.ui Resources/UI/ctkDICOMObjectListWidget.ui + Resources/UI/ctkDICOMPatientItemWidget.ui + Resources/UI/ctkDICOMSeriesItemWidget.ui Resources/UI/ctkDICOMServerNodeWidget.ui + Resources/UI/ctkDICOMServerNodeWidget2.ui + Resources/UI/ctkDICOMStudyItemWidget.ui Resources/UI/ctkDICOMTableManager.ui Resources/UI/ctkDICOMTableView.ui + Resources/UI/ctkDICOMVisualBrowserWidget.ui ) # Resources set(KIT_resources + Resources/UI/ctkDICOMWidget.qrc ) # Target libraries - See CMake/ctkFunctionGetTargetLibraries.cmake # The following macro will read the target libraries from the file 'target_libraries.cmake' ctkFunctionGetTargetLibraries(KIT_target_libraries) +list(APPEND KIT_target_libraries Qt${CTK_QT_VERSION}::Svg) + ctkMacroBuildLib( NAME ${PROJECT_NAME} EXPORT_DIRECTIVE ${KIT_export_directive} diff --git a/Libs/DICOM/Widgets/Plugins/CMakeLists.txt b/Libs/DICOM/Widgets/Plugins/CMakeLists.txt index d3eadcde58..2ef9160efd 100644 --- a/Libs/DICOM/Widgets/Plugins/CMakeLists.txt +++ b/Libs/DICOM/Widgets/Plugins/CMakeLists.txt @@ -20,6 +20,9 @@ set(PLUGIN_SRCS ctkDICOMTableManagerPlugin.h ctkDICOMTableViewPlugin.cpp ctkDICOMTableViewPlugin.h + + ctkDICOMVisualBrowserWidgetPlugin.cpp + ctkDICOMVisualBrowserWidgetPlugin.h ) # Headers that should run through moc @@ -29,6 +32,7 @@ set(PLUGIN_MOC_SRCS ctkDICOMQueryRetrieveWidgetPlugin.h ctkDICOMTableManagerPlugin.h ctkDICOMTableViewPlugin.h + ctkDICOMVisualBrowserWidgetPlugin.h ) # Resources diff --git a/Libs/DICOM/Widgets/Plugins/ctkDICOMVisualBrowserWidgetPlugin.cpp b/Libs/DICOM/Widgets/Plugins/ctkDICOMVisualBrowserWidgetPlugin.cpp new file mode 100644 index 0000000000..f3a00a3a83 --- /dev/null +++ b/Libs/DICOM/Widgets/Plugins/ctkDICOMVisualBrowserWidgetPlugin.cpp @@ -0,0 +1,71 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// CTK includes +#include "ctkDICOMVisualBrowserWidgetPlugin.h" +#include "ctkDICOMVisualBrowserWidget.h" + +//----------------------------------------------------------------------------- +ctkDICOMVisualBrowserWidgetPlugin::ctkDICOMVisualBrowserWidgetPlugin(QObject* pluginParent) + : QObject(pluginParent) +{ +} + +//----------------------------------------------------------------------------- +QWidget* ctkDICOMVisualBrowserWidgetPlugin::createWidget(QWidget* parentForWidget) +{ + ctkDICOMVisualBrowserWidget* newWidget = new ctkDICOMVisualBrowserWidget(parentForWidget); + return newWidget; +} + +//----------------------------------------------------------------------------- +QString ctkDICOMVisualBrowserWidgetPlugin::domXml() const +{ + return "\n" + "\n"; +} + +// -------------------------------------------------------------------------- +QIcon ctkDICOMVisualBrowserWidgetPlugin::icon() const +{ + return QIcon(":/Icons/listview.png"); +} + +//----------------------------------------------------------------------------- +QString ctkDICOMVisualBrowserWidgetPlugin::includeFile() const +{ + return "ctkDICOMVisualBrowserWidget.h"; +} + +//----------------------------------------------------------------------------- +bool ctkDICOMVisualBrowserWidgetPlugin::isContainer() const +{ + return false; +} + +//----------------------------------------------------------------------------- +QString ctkDICOMVisualBrowserWidgetPlugin::name() const +{ + return "ctkDICOMVisualBrowserWidget"; +} diff --git a/Libs/DICOM/Widgets/Plugins/ctkDICOMVisualBrowserWidgetPlugin.h b/Libs/DICOM/Widgets/Plugins/ctkDICOMVisualBrowserWidgetPlugin.h new file mode 100644 index 0000000000..4059bc44c0 --- /dev/null +++ b/Libs/DICOM/Widgets/Plugins/ctkDICOMVisualBrowserWidgetPlugin.h @@ -0,0 +1,48 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMVisualBrowserWidgetPlugin_h +#define __ctkDICOMVisualBrowserWidgetPlugin_h + +// CTK includes +#include "ctkDICOMWidgetsAbstractPlugin.h" + +class CTK_DICOM_WIDGETS_PLUGINS_EXPORT ctkDICOMVisualBrowserWidgetPlugin + : public QObject + , public ctkDICOMWidgetsAbstractPlugin +{ + Q_OBJECT + +public: + ctkDICOMVisualBrowserWidgetPlugin(QObject* _parent = 0); + + QWidget* createWidget(QWidget* _parent); + QString domXml() const; + QIcon icon() const; + QString includeFile() const; + bool isContainer() const; + QString name() const; + +}; + +#endif diff --git a/Libs/DICOM/Widgets/Plugins/ctkDICOMWidgetsPlugins.h b/Libs/DICOM/Widgets/Plugins/ctkDICOMWidgetsPlugins.h index 60bc85014a..36866b5a9c 100644 --- a/Libs/DICOM/Widgets/Plugins/ctkDICOMWidgetsPlugins.h +++ b/Libs/DICOM/Widgets/Plugins/ctkDICOMWidgetsPlugins.h @@ -30,6 +30,7 @@ #include "ctkDICOMQueryRetrieveWidgetPlugin.h" #include "ctkDICOMTableManagerPlugin.h" #include "ctkDICOMTableViewPlugin.h" +#include "ctkDICOMVisualBrowserWidgetPlugin.h" /// \class Group the plugins in one library class CTK_DICOM_WIDGETS_PLUGINS_EXPORT ctkDICOMWidgetsPlugins @@ -47,6 +48,7 @@ class CTK_DICOM_WIDGETS_PLUGINS_EXPORT ctkDICOMWidgetsPlugins plugins << new ctkDICOMQueryRetrieveWidgetPlugin; plugins << new ctkDICOMTableManagerPlugin; plugins << new ctkDICOMTableViewPlugin; + plugins << new ctkDICOMVisualBrowserWidgetPlugin; return plugins; } }; diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/accept.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/accept.svg new file mode 100644 index 0000000000..bdeb9c44ba --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/accept.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/add.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/add.svg new file mode 100644 index 0000000000..0b2996f8db --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/add.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/cancel.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/cancel.svg new file mode 100644 index 0000000000..a22fdd0469 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/cancel.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/cloud.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/cloud.svg new file mode 100644 index 0000000000..73b2b2e15d --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/cloud.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/delete.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/delete.svg new file mode 100644 index 0000000000..de34deff91 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/delete.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/dns.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/dns.svg new file mode 100644 index 0000000000..3d4d3dc1b3 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/dns.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/downloading.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/downloading.svg new file mode 100644 index 0000000000..54395ad12b --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/downloading.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/import.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/import.svg new file mode 100644 index 0000000000..31f5598fdb --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/import.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/load.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/load.svg new file mode 100644 index 0000000000..48afa908b9 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/load.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/loaded.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/loaded.svg new file mode 100644 index 0000000000..9df0ec7bab --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/loaded.svg @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg new file mode 100644 index 0000000000..6d94a46582 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/patient.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/patient.svg new file mode 100644 index 0000000000..58f30a95c9 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/patient.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/query.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/query.svg new file mode 100644 index 0000000000..2f1acace02 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/query.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/save.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/save.svg new file mode 100644 index 0000000000..198d3b8d56 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/save.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/text_document.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/text_document.svg new file mode 100644 index 0000000000..88455731b1 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/text_document.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/visible.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/visible.svg new file mode 100644 index 0000000000..bb86d8fa4a --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/visible.svg @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg new file mode 100644 index 0000000000..69e6392135 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/warning.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/warning.svg new file mode 100644 index 0000000000..4d12f769cb --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/warning.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui new file mode 100644 index 0000000000..d9d087cf4e --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui @@ -0,0 +1,332 @@ + + + ctkDICOMPatientItemWidget + + + + 0 + 0 + 1041 + 400 + + + + Patient Item + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Patient Information + + + false + + + false + + + 14 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + + + 100 + 0 + + + + ID + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 3 + + + + + + + + 100 + 0 + + + + Sex + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 3 + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 3 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 100 + 0 + + + + Birth Date + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 3 + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 3 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + 0 + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 3 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 100 + 0 + + + + Name + + + 3 + + + + + + + + + + 3 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + 0 + 2 + + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + true + + + + true + + + + 0 + 0 + 996 + 191 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 20 + 1 + + + + + + + + + + + + ctkCollapsibleGroupBox + QGroupBox +
ctkCollapsibleGroupBox.h
+ 1 +
+
+ + + + + +
diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMQueryWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMQueryWidget.ui index f38938066d..c5de6f31ef 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMQueryWidget.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMQueryWidget.ui @@ -7,7 +7,7 @@ 0 0 279 - 296 + 348 diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMSeriesItemWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMSeriesItemWidget.ui new file mode 100644 index 0000000000..e5ccee5702 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMSeriesItemWidget.ui @@ -0,0 +1,95 @@ + + + ctkDICOMSeriesItemWidget + + + + 0 + 0 + 153 + 151 + + + + Series Item + + + + 7 + + + 7 + + + 7 + + + 7 + + + 7 + + + + + + 0 + 0 + + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::AlignBottom|Qt::AlignHCenter + + + false + + + + 255 + 255 + 255 + + + + + + + + + + + + ctkThumbnailLabel + QWidget +
ctkThumbnailLabel.h
+
+
+ + + + +
diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMServerNodeWidget2.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMServerNodeWidget2.ui new file mode 100644 index 0000000000..2f617941c8 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMServerNodeWidget2.ui @@ -0,0 +1,406 @@ + + + ctkDICOMServerNodeWidget2 + + + + 0 + 0 + 1679 + 271 + + + + Server List + + + + 1 + + + 1 + + + 1 + + + 1 + + + 5 + + + + + + 1 + 0 + + + + Storage + + + true + + + false + + + + + + Port: + + + + + + + 11112 + + + + + + + false + + + + + + + Enable: + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + Status: + + + + + + + + 0 + 0 + + + + + 70 + 0 + + + + Inactive + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + AETitle: + + + + + + + CTKSTORE + + + true + + + + + + + + + + 3 + + + + + + 0 + 0 + + + + Add host + + + + :/Icons/add.svg:/Icons/add.svg + + + + + + + + 0 + 0 + + + + Verify host + + + + :/Icons/dns.svg:/Icons/dns.svg + + + + + + + + 0 + 0 + + + + Remove host + + + + :/Icons/delete.svg:/Icons/delete.svg + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 20 + 30 + + + + + + + + + 0 + 0 + + + + + 0 + 70 + + + + Qt::Vertical + + + QDialogButtonBox::Discard|QDialogButtonBox::Save + + + true + + + + + + + + + Servers + + + + + + + 1 + 0 + + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::ElideRight + + + 10 + + + true + + + false + + + 165 + + + 165 + + + true + + + false + + + true + + + false + + + false + + + + Name + + + + + Query/Retrieve + + + + + Storage + + + + + Calling AETitle + + + + + Called AETitle + + + + + Address + + + + + Port + + + + + Timeout + + + + + Protocol + + + + + Proxy + + + + + + + + + + + + ctkCheckBox + QCheckBox +
ctkCheckBox.h
+
+ + ctkCollapsibleGroupBox + QGroupBox +
ctkCollapsibleGroupBox.h
+ 1 +
+ + ctkPushButton + QPushButton +
ctkPushButton.h
+
+
+ + + + +
diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui new file mode 100644 index 0000000000..20803e1cb7 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui @@ -0,0 +1,218 @@ + + + ctkDICOMStudyItemWidget + + + + 0 + 0 + 378 + 534 + + + + Study Item + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 20 + 20 + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Study ID 1234 --- Date + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + false + + + true + + + + 7 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:12pt;"><br /></p></body></html> + + + + + + + + 0 + 1 + + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + false + + + Qt::NoPen + + + 6 + + + false + + + false + + + false + + + false + + + + + + + + + + + + + + + + + + + + + + ctkCheckBox + QCheckBox +
ctkCheckBox.h
+
+ + ctkCollapsibleGroupBox + QGroupBox +
ctkCollapsibleGroupBox.h
+ 1 +
+ + ctkFittedTextBrowser + QTextBrowser +
ctkFittedTextBrowser.h
+
+
+ + + + +
diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui new file mode 100644 index 0000000000..07820eb749 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui @@ -0,0 +1,942 @@ + + + ctkDICOMVisualBrowserWidget + + + Qt::WindowModal + + + + 0 + 0 + 1201 + 678 + + + + Select data to load + + + + :/Icons/patient.svg:/Icons/patient.svg + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + color: rgb(0, 0, 0);background-color: rgb(245, 245, 170); + + + QFrame::Box + + + + + + + 0 + 0 + + + + Warning + + + + + + + Update database + + + + + + + Create new database + + + + + + + Select database folder + + + + + + + + + + + + Patients Search + + + false + + + + 0 + + + 2 + + + 0 + + + 2 + + + 2 + + + + + 3 + + + + + 3 + + + + + 3 + + + + + + 0 + 0 + + + + ID + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Filter by patient ID + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + 3 + + + + + + 0 + 0 + + + + Name + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Filter by patient name + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + 3 + + + + + + 0 + 0 + + + + Study + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Filter by study description + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + 3 + + + + + + 0 + 0 + + + + Series + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Filter by series description + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + 3 + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + Modality + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + Filter by modality + + + Any + + + 0 + + + QComboBox::AdjustToContents + + + + + + true + + + 0 + + + + Any + + + + + CR + + + + + CT + + + + + MR + + + + + NM + + + + + US + + + + + PT + + + + + XA + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + 3 + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + Date + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + Filter by date + + + + Any + + + + + Today + + + + + Yesterday + + + + + Last week + + + + + Last month + + + + + Last year + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Search the database. If servers has been provided, the widget will also query and retrieve the data. + + + + + + + :/Icons/query.svg:/Icons/query.svg + + + + 48 + 48 + + + + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + Actions + + + false + + + + 0 + + + 2 + + + 0 + + + 2 + + + 2 + + + + + Close + + + + :/Icons/cancel.svg:/Icons/cancel.svg + + + + 24 + 24 + + + + + + + + Import + + + + :/Icons/import.svg:/Icons/import.svg + + + + 24 + 24 + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 70 + + + + Qt::StrongFocus + + + No filters have been set and no patients have been found in the local database. +Please set at least one filter to query the servers + + + + :/Icons/warning.svg:/Icons/warning.svg + + + + 48 + 48 + + + + false + + + false + + + false + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 1 + + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + + 24 + 24 + + + + Qt::ElideNone + + + true + + + false + + + true + + + false + + + + + :/Icons/patient.svg:/Icons/patient.svg + + + Patient 1 + + + + + + :/Icons/patient.svg:/Icons/patient.svg + + + Patient 2 + + + + + + + + QFrame::Box + + + QFrame::Raised + + + + + + 0 + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + Progress + + + Qt::AlignCenter + + + + + + + false + + + 300 + + + false + + + true + + + + + + + + + + Settings + + + false + + + true + + + + 0 + + + 2 + + + 0 + + + 2 + + + 2 + + + + + + + + + ctkCheckableComboBox + QComboBox +
ctkCheckableComboBox.h
+
+ + ctkCollapsibleGroupBox + QGroupBox +
ctkCollapsibleGroupBox.h
+ 1 +
+ + ctkComboBox + QComboBox +
ctkComboBox.h
+
+ + ctkPushButton + QPushButton +
ctkPushButton.h
+
+ + ctkSearchBox + QLineEdit +
ctkSearchBox.h
+
+
+ + + + +
diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMWidget.qrc b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMWidget.qrc new file mode 100644 index 0000000000..5f282c68f9 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMWidget.qrc @@ -0,0 +1,22 @@ + + + Icons/accept.svg + Icons/cancel.svg + Icons/cloud.svg + Icons/import.svg + Icons/loaded.svg + Icons/patient.svg + Icons/visible.svg + Icons/warning.svg + Icons/load.svg + Icons/text_document.svg + Icons/delete.svg + Icons/more_vert.svg + Icons/add.svg + Icons/dns.svg + Icons/save.svg + Icons/query.svg + Icons/wait.svg + Icons/downloading.svg + + diff --git a/Libs/DICOM/Widgets/Testing/Cpp/CMakeLists.txt b/Libs/DICOM/Widgets/Testing/Cpp/CMakeLists.txt index 1430d1a149..14b3b9046a 100644 --- a/Libs/DICOM/Widgets/Testing/Cpp/CMakeLists.txt +++ b/Libs/DICOM/Widgets/Testing/Cpp/CMakeLists.txt @@ -11,10 +11,15 @@ create_test_sourcelist(Tests ${KIT}CppTests.cpp ctkDICOMListenerWidgetTest1.cpp ctkDICOMModelTest2.cpp ctkDICOMObjectModelTest1.cpp + ctkDICOMPatientItemWidgetTest1.cpp ctkDICOMQueryResultsTabWidgetTest1.cpp ctkDICOMQueryRetrieveWidgetTest1.cpp + ctkDICOMSeriesItemWidgetTest1.cpp + ctkDICOMStudyItemWidgetTest1.cpp ctkDICOMServerNodeWidgetTest1.cpp + ctkDICOMServerNodeWidget2Test1.cpp ctkDICOMThumbnailListWidgetTest1.cpp + ctkDICOMVisualBrowserWidgetTest1.cpp ) set(Tests_MOC_CPPS @@ -54,8 +59,12 @@ SIMPLE_TEST(ctkDICOMModelTest2 ${CMAKE_CURRENT_BINARY_DIR}/Testing/Temporary/ctkDICOMModelTest2-dicom.db ${CMAKE_CURRENT_SOURCE_DIR}/../../../Core/Resources/dicom-sample.sql ) +SIMPLE_TEST(ctkDICOMPatientItemWidgetTest1) SIMPLE_TEST(ctkDICOMQueryRetrieveWidgetTest1) SIMPLE_TEST(ctkDICOMQueryResultsTabWidgetTest1) +SIMPLE_TEST(ctkDICOMSeriesItemWidgetTest1) +SIMPLE_TEST(ctkDICOMStudyItemWidgetTest1) +SIMPLE_TEST(ctkDICOMServerNodeWidget2Test1) SIMPLE_TEST(ctkDICOMThumbnailListWidgetTest1 ${CMAKE_CURRENT_BINARY_DIR}/Testing/Temporary/ctkDICOMThumbnailListWidgetTest1-dicom.db ${CMAKE_CURRENT_SOURCE_DIR}/../../../Core/Resources/dicom-sample.sql @@ -71,4 +80,5 @@ if(EXISTS "${CTKData_DIR}") SIMPLE_TEST(ctkDICOMBrowserTest1 ${CTKData_DIR}/Data/DICOM/MRHEAD) SIMPLE_TEST(ctkDICOMItemViewTest1 ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA) SIMPLE_TEST(ctkDICOMImageTest1 ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA) + SIMPLE_TEST(ctkDICOMVisualBrowserWidgetTest1 ${CTKData_DIR}/Data/DICOM/MRHEAD) endif() diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest.cpp index 953a02ffbc..4cbe6e80c2 100644 --- a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest.cpp +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest.cpp @@ -27,7 +27,6 @@ #include "ctkDICOMDatabase.h" #include "ctkDICOMBrowser.h" #include "ctkFileDialog.h" -#include "ctkScopedCurrentDir.h" #include "ctkTest.h" #include "ctkUtils.h" diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest1.cpp index 74ea1c6789..2355ad61a8 100644 --- a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest1.cpp +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest1.cpp @@ -56,13 +56,13 @@ int ctkDICOMBrowserTest1( int argc, char * argv [] ) QFileInfo tempFileInfo(QDir::tempPath() + QString("/ctkDICOMBrowserTest1-db")); QString dbDir = tempFileInfo.absoluteFilePath(); - qDebug() << "\n\nUsing directory: " << dbDir; + qDebug().noquote() << "\n\n" << testName << ": Using directory: " << dbDir; if (tempFileInfo.exists()) { - qDebug() << "\n\nRemoving directory: " << dbDir; + qDebug().noquote() << "\n\n" << testName << ": Removing directory: " << dbDir; ctk::removeDirRecursively(dbDir); } - qDebug() << "\n\nMaking directory: " << dbDir; + qDebug().noquote() << "\n\n" << testName << ": Making directory: " << dbDir; QDir dir(dbDir); dir.mkdir(dbDir); @@ -72,7 +72,7 @@ int ctkDICOMBrowserTest1( int argc, char * argv [] ) browser.show(); browser.setDisplayImportSummary(false); - qDebug() << "Importing directory " << dicomDirectory; + qDebug().noquote() << testName << ": Importing directory " << dicomDirectory; // [Deprecated] // make sure copy/link dialog doesn't pop up, always copy on import @@ -97,7 +97,8 @@ int ctkDICOMBrowserTest1( int argc, char * argv [] ) browser.importFiles(files); browser.waitForImportFinished(); - qDebug() << browser.patientsAddedDuringImport() + qDebug().noquote() << testName << ":" + << " " << browser.patientsAddedDuringImport() << " " << browser.studiesAddedDuringImport() << " " << browser.seriesAddedDuringImport() << " " << browser.instancesAddedDuringImport(); @@ -110,7 +111,7 @@ int ctkDICOMBrowserTest1( int argc, char * argv [] ) browser.importDirectories(QStringList() << dicomDirectory); browser.waitForImportFinished(); - qDebug() << "\n\nAdded to database directory: " << files; + qDebug().noquote() << "\n\n" << testName << ": Added to database directory: " << files; // [Deprecated] // reset to the original copy/import setting @@ -122,7 +123,7 @@ int ctkDICOMBrowserTest1( int argc, char * argv [] ) CHECK_INT(browser.seriesAddedDuringImport(), 1); CHECK_INT(browser.instancesAddedDuringImport(), 100); - qDebug() << "\n\nAdded to database directory: " << dbDir; + qDebug().noquote() << "\n\n" << testName << ": Added to database directory: " << dbDir; if (!interactive) { diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMPatientItemWidgetTest1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMPatientItemWidgetTest1.cpp new file mode 100644 index 0000000000..00f1351953 --- /dev/null +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMPatientItemWidgetTest1.cpp @@ -0,0 +1,80 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include + +// ctkCore includes +#include + +// ctkDICOMWidget includes +#include "ctkDICOMPatientItemWidget.h" + +// Test visual browser import functionality +int ctkDICOMPatientItemWidgetTest1(int argc, char* argv[]) +{ + QApplication app(argc, argv); + + QStringList arguments = app.arguments(); + QString testName = arguments.takeFirst(); + Q_UNUSED(testName); + + bool interactive = arguments.removeOne("-I"); + + ctkDICOMPatientItemWidget widget; + + // Test the default values + CHECK_QSTRING(widget.patientItem(), ""); + CHECK_QSTRING(widget.patientID(), ""); + CHECK_QSTRING(widget.filteringStudyDescription(), ""); + CHECK_QSTRING(widget.filteringSeriesDescription(), ""); + CHECK_INT(widget.filteringDate(), ctkDICOMPatientItemWidget::DateType::Any); + CHECK_INT(widget.numberOfStudiesPerPatient(), 2); + CHECK_INT(widget.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium); + + // Test setting and getting + widget.setPatientItem("1"); + CHECK_QSTRING(widget.patientItem(), "1"); + widget.setPatientID("123456"); + CHECK_QSTRING(widget.patientID(), "123456"); + widget.setFilteringStudyDescription("study"); + CHECK_QSTRING(widget.filteringStudyDescription(), "study"); + widget.setFilteringSeriesDescription("series"); + CHECK_QSTRING(widget.filteringSeriesDescription(), "series"); + widget.setFilteringDate(ctkDICOMPatientItemWidget::DateType::LastYear); + CHECK_INT(widget.filteringDate(), ctkDICOMPatientItemWidget::DateType::LastYear); + widget.setNumberOfStudiesPerPatient(6); + CHECK_INT(widget.numberOfStudiesPerPatient(), 6); + widget.setThumbnailSize(ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); + CHECK_INT(widget.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); + + if (!interactive) + { + QTimer::singleShot(200, &app, SLOT(quit())); + } + + return app.exec(); +} diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMSeriesItemWidgetTest1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMSeriesItemWidgetTest1.cpp new file mode 100644 index 0000000000..4c42bf1e6a --- /dev/null +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMSeriesItemWidgetTest1.cpp @@ -0,0 +1,92 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include + +// ctkCore includes +#include + +// ctkDICOMWidget includes +#include "ctkDICOMSeriesItemWidget.h" + +// Test visual browser import functionality +int ctkDICOMSeriesItemWidgetTest1(int argc, char* argv[]) +{ + QApplication app(argc, argv); + + QStringList arguments = app.arguments(); + QString testName = arguments.takeFirst(); + Q_UNUSED(testName); + + bool interactive = arguments.removeOne("-I"); + + ctkDICOMSeriesItemWidget widget; + + // Test the default values + CHECK_QSTRING(widget.seriesItem(), ""); + CHECK_QSTRING(widget.patientID(), ""); + CHECK_QSTRING(widget.studyInstanceUID(), ""); + CHECK_QSTRING(widget.seriesInstanceUID(), ""); + CHECK_QSTRING(widget.seriesNumber(), ""); + CHECK_QSTRING(widget.modality(), ""); + CHECK_QSTRING(widget.seriesDescription(), ""); + CHECK_BOOL(widget.stopJobs(), false); + CHECK_BOOL(widget.raiseJobsPriority(), false); + CHECK_BOOL(widget.isCloud(), false); + CHECK_BOOL(widget.IsLoaded(), false); + CHECK_BOOL(widget.IsVisible(), false); + CHECK_INT(widget.thumbnailSizePixel(), 200); + + // Test setting and getting + widget.setSeriesItem("1"); + CHECK_QSTRING(widget.seriesItem(), "1"); + widget.setPatientID("123456"); + CHECK_QSTRING(widget.patientID(), "123456"); + widget.setStudyInstanceUID("123456.123"); + CHECK_QSTRING(widget.studyInstanceUID(), "123456.123"); + widget.setSeriesInstanceUID("123456.456"); + CHECK_QSTRING(widget.seriesInstanceUID(), "123456.456"); + widget.setSeriesNumber("1"); + CHECK_QSTRING(widget.seriesNumber(), "1"); + widget.setModality("CT"); + CHECK_QSTRING(widget.modality(), "CT"); + widget.setSeriesDescription("description"); + CHECK_QSTRING(widget.seriesDescription(), "description"); + widget.setStopJobs(true); + CHECK_BOOL(widget.stopJobs(), true); + widget.setRaiseJobsPriority(true); + CHECK_BOOL(widget.raiseJobsPriority(), true); + widget.setThumbnailSizePixel(100); + CHECK_INT(widget.thumbnailSizePixel(), 100); + + if (!interactive) + { + QTimer::singleShot(200, &app, SLOT(quit())); + } + + return app.exec(); +} diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMServerNodeWidget2Test1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMServerNodeWidget2Test1.cpp new file mode 100644 index 0000000000..02d292a704 --- /dev/null +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMServerNodeWidget2Test1.cpp @@ -0,0 +1,105 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include +#include + +// ctkCore includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMScheduler.h" +#include "ctkDICOMServer.h" +#include "ctkDICOMServerNodeWidget2.h" + +int ctkDICOMServerNodeWidget2Test1(int argc, char* argv[]) +{ + QApplication app(argc, argv); + + QStringList arguments = app.arguments(); + QString testName = arguments.takeFirst(); + Q_UNUSED(testName); + + bool interactive = arguments.removeOne("-I"); + + ctkDICOMServerNodeWidget2 widget; + + // Test the default values + // Check the default values of storage AE title and port + CHECK_QSTRING(widget.storageAETitle(), "CTKSTORE"); + CHECK_INT(widget.storagePort(), 11112); + + // Test setting and getting storage AE title + widget.setStorageAETitle("MyStorage"); + CHECK_QSTRING(widget.storageAETitle(), "MyStorage"); + + // Test setting and getting storage port + widget.setStoragePort(12345); + CHECK_INT(widget.storagePort(), 12345); + + // Test default servers + ctkDICOMScheduler scheduler; + widget.setScheduler(scheduler); + CHECK_QSTRING(widget.getServerNameFromIndex(0), "ExampleHost"); + CHECK_BOOL(widget.getServer("ExampleHost")->queryRetrieveEnabled(), false); + CHECK_BOOL(widget.getServer("ExampleHost")->storageEnabled(), false); + CHECK_QSTRING(widget.getServer("ExampleHost")->callingAETitle(), "CTK"); + CHECK_QSTRING(widget.getServer("ExampleHost")->calledAETitle(), "AETITLE"); + CHECK_QSTRING(widget.getServer("ExampleHost")->host(), "dicom.example.com"); + CHECK_INT(widget.getServer("ExampleHost")->port(), 11112); + CHECK_QSTRING(widget.getServer("ExampleHost")->retrieveProtocolAsString(), "CGET"); + CHECK_INT(widget.getServer("ExampleHost")->connectionTimeout(), 30); + + CHECK_QSTRING(widget.getServerNameFromIndex(1), "MedicalConnections"); + CHECK_BOOL(widget.getServer("MedicalConnections")->queryRetrieveEnabled(), false); + CHECK_BOOL(widget.getServer("MedicalConnections")->storageEnabled(), false); + CHECK_QSTRING(widget.getServer("MedicalConnections")->callingAETitle(), "CTK"); + CHECK_QSTRING(widget.getServer("MedicalConnections")->calledAETitle(), "ANYAE"); + CHECK_QSTRING(widget.getServer("MedicalConnections")->host(), "dicomserver.co.uk"); + CHECK_INT(widget.getServer("MedicalConnections")->port(), 104); + CHECK_QSTRING(widget.getServer("MedicalConnections")->retrieveProtocolAsString(), "CGET"); + CHECK_INT(widget.getServer("MedicalConnections")->connectionTimeout(), 30); + + // Test adding and removing servers + ctkDICOMServer* server = new ctkDICOMServer(); + server->setConnectionName("server"); + widget.addServer(server); + CHECK_INT(widget.getNumberOfServers(), 3); + CHECK_INT(widget.getServerIndexFromName("server"), 2); + CHECK_QSTRING(widget.getServerNameFromIndex(2), "server"); + CHECK_QSTRING(widget.getServer("server")->connectionName(), server->connectionName()); + widget.removeServer("server"); + CHECK_INT(widget.getNumberOfServers(), 2); + delete server; + + if (!interactive) + { + QTimer::singleShot(200, &app, SLOT(quit())); + } + + return app.exec(); +} diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMStudyItemWidgetTest1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMStudyItemWidgetTest1.cpp new file mode 100644 index 0000000000..39f0870c34 --- /dev/null +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMStudyItemWidgetTest1.cpp @@ -0,0 +1,86 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include + +// ctkCore includes +#include + +// ctkDICOMWidget includes +#include "ctkDICOMStudyItemWidget.h" + +// Test visual browser import functionality +int ctkDICOMStudyItemWidgetTest1(int argc, char* argv[]) +{ + QApplication app(argc, argv); + + QStringList arguments = app.arguments(); + QString testName = arguments.takeFirst(); + Q_UNUSED(testName); + + bool interactive = arguments.removeOne("-I"); + + ctkDICOMStudyItemWidget widget; + + // Test the default values + CHECK_QSTRING(widget.studyItem(), ""); + CHECK_QSTRING(widget.patientID(), ""); + CHECK_QSTRING(widget.studyInstanceUID(), ""); + CHECK_QSTRING(widget.title(), "Study ID 1234 --- Date"); + CHECK_QSTRING(widget.description(), ""); + CHECK_QSTRING(widget.filteringSeriesDescription(), ""); + CHECK_BOOL(widget.collapsed(), false) + CHECK_BOOL(widget.selection(), false) + CHECK_INT(widget.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium); + + // Test setting and getting + widget.setStudyItem("1"); + CHECK_QSTRING(widget.studyItem(), "1"); + widget.setPatientID("123456"); + CHECK_QSTRING(widget.patientID(), "123456"); + widget.setStudyInstanceUID("123456.123"); + CHECK_QSTRING(widget.studyInstanceUID(), "123456.123"); + widget.setTitle("title"); + CHECK_QSTRING(widget.title(), "title"); + widget.setDescription("description"); + CHECK_QSTRING(widget.description(), "description"); + widget.setFilteringSeriesDescription("seriesDescription"); + CHECK_QSTRING(widget.filteringSeriesDescription(), "seriesDescription"); + widget.setCollapsed(true); + CHECK_BOOL(widget.collapsed(), true); + widget.setSelection(true); + CHECK_BOOL(widget.selection(), true); + widget.setThumbnailSize(ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); + CHECK_INT(widget.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); + + if (!interactive) + { + QTimer::singleShot(200, &app, SLOT(quit())); + } + + return app.exec(); +} diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMVisualBrowserWidgetTest1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMVisualBrowserWidgetTest1.cpp new file mode 100644 index 0000000000..66b30f6e68 --- /dev/null +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMVisualBrowserWidgetTest1.cpp @@ -0,0 +1,163 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include + +// ctkCore includes +#include +#include + +// ctkDICOMWidget includes +#include "ctkDICOMVisualBrowserWidget.h" + +int ctkDICOMVisualBrowserWidgetTest1(int argc, char* argv[]) +{ + QApplication app(argc, argv); + + QStringList arguments = app.arguments(); + QString testName = arguments.takeFirst(); + + bool interactive = arguments.removeOne("-I"); + + if (arguments.count() != 1) + { + std::cerr << "Usage: " << qPrintable(testName) + << " [-I] " << std::endl; + return EXIT_FAILURE; + } + + QString dicomDirectory(arguments.at(0)); + + ctkDICOMVisualBrowserWidget browser; + + // Test the default values + CHECK_QSTRING(browser.storageAETitle(), "CTKSTORE"); + CHECK_INT(browser.storagePort(), 11112); + CHECK_QSTRING(browser.filteringPatientID(), ""); + CHECK_QSTRING(browser.filteringPatientName(), ""); + CHECK_QSTRING(browser.filteringStudyDescription(), ""); + CHECK_QSTRING(browser.filteringSeriesDescription(), ""); + CHECK_QSTRING(browser.filteringModalities().at(0), "Any"); + CHECK_INT(browser.filteringDate(), ctkDICOMPatientItemWidget::DateType::Any); + CHECK_INT(browser.numberOfStudiesPerPatient(), 2); + CHECK_INT(browser.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium); + CHECK_BOOL(browser.isSendActionVisible(), false); + CHECK_BOOL(browser.isDeleteActionVisible(), true); + + // Test visual browser import functionality + QFileInfo tempFileInfo(QDir::tempPath() + QString("/ctkDICOMVisualBrowserWidgetTest1-db")); + QString dbDir = tempFileInfo.absoluteFilePath(); + qDebug().noquote() << "\n\n" + << testName << ": Using directory: " << dbDir; + if (tempFileInfo.exists()) + { + qDebug().noquote() << "\n\n" + << testName << ": Removing directory: " << dbDir; + ctk::removeDirRecursively(dbDir); + } + qDebug().noquote() << "\n\n" + << testName << ": Making directory: " << dbDir; + QDir dir(dbDir); + dir.mkdir(dbDir); + + browser.setDatabaseDirectory(dbDir); + browser.show(); + + qDebug().noquote() << testName << ": Importing directory " << dicomDirectory; + + // Test import of a few specific files + QDirIterator it(dicomDirectory, QStringList() << "*.IMA", QDir::Files, QDirIterator::Subdirectories); + // Skip a few files + it.next(); + it.next(); + // Add 3 files + QStringList files; + files << it.next(); + files << it.next(); + files << it.next(); + browser.importFiles(files); + browser.waitForImportFinished(); + + qDebug().noquote() << testName << ":" + << " " << browser.patientsAddedDuringImport() + << " " << browser.studiesAddedDuringImport() + << " " << browser.seriesAddedDuringImport() + << " " << browser.instancesAddedDuringImport(); + + CHECK_INT(browser.patientsAddedDuringImport(), 1); + CHECK_INT(browser.studiesAddedDuringImport(), 1); + CHECK_INT(browser.seriesAddedDuringImport(), 1); + CHECK_INT(browser.instancesAddedDuringImport(), 3); + + browser.importDirectories(QStringList() << argv[1]); + browser.waitForImportFinished(); + + qDebug().noquote() << "\n\n" + << testName << ": Added to database directory: " << files; + + CHECK_INT(browser.patientsAddedDuringImport(), 1); + CHECK_INT(browser.studiesAddedDuringImport(), 1); + CHECK_INT(browser.seriesAddedDuringImport(), 1); + CHECK_INT(browser.instancesAddedDuringImport(), 100); + + qDebug().noquote() << "\n\n" + << testName << ": Added to database directory: " << dbDir; + + // Test setting and getting + browser.setStorageAETitle("storage"); + CHECK_QSTRING(browser.storageAETitle(), "storage"); + browser.setStoragePort(2014); + CHECK_INT(browser.storagePort(), 2014); + browser.setFilteringPatientID("123456"); + CHECK_QSTRING(browser.filteringPatientID(), "123456"); + browser.setFilteringPatientName("Name"); + CHECK_QSTRING(browser.filteringPatientName(), "Name"); + browser.setFilteringStudyDescription("StudyDescription"); + CHECK_QSTRING(browser.filteringStudyDescription(), "StudyDescription"); + browser.setFilteringSeriesDescription("SeriesDescription"); + CHECK_QSTRING(browser.filteringSeriesDescription(), "SeriesDescription"); + QStringList filteringModalities = {"CT"}; + browser.setFilteringModalities(filteringModalities); + CHECK_QSTRING(browser.filteringModalities().at(0), "CT"); + browser.setFilteringDate(ctkDICOMPatientItemWidget::DateType::LastYear); + CHECK_INT(browser.filteringDate(), ctkDICOMPatientItemWidget::DateType::LastYear); + browser.setNumberOfStudiesPerPatient(6); + CHECK_INT(browser.numberOfStudiesPerPatient(), 6); + browser.setThumbnailSize(ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); + CHECK_INT(browser.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); + browser.setSendActionVisible(true); + CHECK_BOOL(browser.isSendActionVisible(), true); + browser.setDeleteActionVisible(false); + CHECK_BOOL(browser.isDeleteActionVisible(), false); + + if (!interactive) + { + QTimer::singleShot(200, &app, SLOT(quit())); + } + + return app.exec(); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp b/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp index 22dda34897..ace876886f 100644 --- a/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp @@ -818,7 +818,6 @@ void ctkDICOMBrowser::onQueryRetrieveFinished() //---------------------------------------------------------------------------- void ctkDICOMBrowser::onRemoveAction() { - Q_D(ctkDICOMBrowser); this->removeSelectedItems(ctkDICOMModel::SeriesType); } @@ -1089,7 +1088,7 @@ bool ctkDICOMBrowser::confirmDeleteSelectedUIDs(QStringList uids) return false; } - ctkMessageBox confirmDeleteDialog; + ctkMessageBox confirmDeleteDialog(this); QString message = tr("Do you want to delete the following selected items?"); // calculate maximum number of rows that fit in the browser widget to have a reasonable limit @@ -1441,7 +1440,7 @@ void ctkDICOMBrowser::exportSeries(QString dirPath, QStringList uids) QString errorString = tr("Unable to create export destination directory:\n\n%1" "\n\nHalting export.") .arg(destinationDir); - ctkMessageBox createDirectoryErrorMessageBox; + ctkMessageBox createDirectoryErrorMessageBox(this); createDirectoryErrorMessageBox.setText(errorString); createDirectoryErrorMessageBox.setIcon(QMessageBox::Warning); createDirectoryErrorMessageBox.exec(); @@ -1491,7 +1490,7 @@ void ctkDICOMBrowser::exportSeries(QString dirPath, QStringList uids) QString errorString = tr("Export destination file already exists:\n\n%1" "\n\nHalting export.") .arg(destinationFileName); - ctkMessageBox copyErrorMessageBox; + ctkMessageBox copyErrorMessageBox(this); copyErrorMessageBox.setText(errorString); copyErrorMessageBox.setIcon(QMessageBox::Warning); copyErrorMessageBox.exec(); @@ -1507,7 +1506,7 @@ void ctkDICOMBrowser::exportSeries(QString dirPath, QStringList uids) "\n\nHalting export.") .arg(filePath) .arg(destinationFileName); - ctkMessageBox copyErrorMessageBox; + ctkMessageBox copyErrorMessageBox(this); copyErrorMessageBox.setText(errorString); copyErrorMessageBox.setIcon(QMessageBox::Warning); copyErrorMessageBox.exec(); diff --git a/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp index c28dc4a6b0..7e1d5f32ff 100644 --- a/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp @@ -321,10 +321,18 @@ void ctkDICOMObjectListWidget::updateWidget() // only update the thumbnail if visible for better update performance ctkDICOMThumbnailGenerator thumbnailGenerator; QImage thumbnailImage; - if (!thumbnailGenerator.generateThumbnail(d->currentFile, thumbnailImage)) + if (d->currentFile.isEmpty()) { thumbnailGenerator.generateBlankThumbnail(thumbnailImage); } + else + { + if (!thumbnailGenerator.generateThumbnail(d->currentFile, thumbnailImage)) + { + thumbnailGenerator.generateBlankThumbnail(thumbnailImage); + } + } + d->thumbnailLabel->setPixmap(QPixmap::fromImage(thumbnailImage)); } } diff --git a/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp new file mode 100644 index 0000000000..0ca07b9ef3 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp @@ -0,0 +1,815 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include + +// CTK includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMDatabase.h" +#include "ctkDICOMScheduler.h" +#include "ctkDICOMJobResponseSet.h" + +// ctkDICOMWidgets includes +#include "ctkDICOMSeriesItemWidget.h" +#include "ctkDICOMPatientItemWidget.h" +#include "ui_ctkDICOMPatientItemWidget.h" + +static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMPatientItemWidget"); + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +class ctkDICOMPatientItemWidgetPrivate : public Ui_ctkDICOMPatientItemWidget +{ + Q_DECLARE_PUBLIC(ctkDICOMPatientItemWidget); + +protected: + ctkDICOMPatientItemWidget* const q_ptr; + +public: + ctkDICOMPatientItemWidgetPrivate(ctkDICOMPatientItemWidget& obj); + ~ctkDICOMPatientItemWidgetPrivate(); + + void init(QWidget* parentWidget); + + QString getPatientItemFromPatientID(const QString& patientID); + QString formatDate(const QString&); + bool isStudyItemAlreadyAdded(const QString& studyItem); + void clearLayout(QLayout* layout, bool deleteWidgets = true); + void createStudies(); + + QSharedPointer DicomDatabase; + QSharedPointer Scheduler; + QSharedPointer VisualDICOMBrowser; + + int NumberOfStudiesPerPatient; + ctkDICOMStudyItemWidget::ThumbnailSizeOption ThumbnailSize; + + QString PatientItem; + QString PatientID; + + QString FilteringStudyDescription; + ctkDICOMPatientItemWidget::DateType FilteringDate; + + QString FilteringSeriesDescription; + QStringList FilteringModalities; + + QList StudyItemWidgetsList; +}; + +//---------------------------------------------------------------------------- +// ctkDICOMPatientItemWidgetPrivate methods + +//---------------------------------------------------------------------------- +ctkDICOMPatientItemWidgetPrivate::ctkDICOMPatientItemWidgetPrivate(ctkDICOMPatientItemWidget& obj) + : q_ptr(&obj) +{ + this->FilteringDate = ctkDICOMPatientItemWidget::DateType::Any; + this->NumberOfStudiesPerPatient = 2; + this->ThumbnailSize = ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium; + this->PatientItem = ""; + this->PatientID = ""; + this->FilteringStudyDescription = ""; + this->FilteringSeriesDescription = ""; + + this->DicomDatabase = nullptr; + this->Scheduler = nullptr; + this->VisualDICOMBrowser = nullptr; +} + +//---------------------------------------------------------------------------- +ctkDICOMPatientItemWidgetPrivate::~ctkDICOMPatientItemWidgetPrivate() +{ + QLayout* StudiesListWidgetLayout = this->StudiesListWidget->layout(); + this->clearLayout(StudiesListWidgetLayout); +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidgetPrivate::init(QWidget* parentWidget) +{ + Q_Q(ctkDICOMPatientItemWidget); + this->setupUi(q); + + this->VisualDICOMBrowser = QSharedPointer(parentWidget, skipDelete); + + this->PatientNameValueLabel->setWordWrap(true); + this->PatientIDValueLabel->setWordWrap(true); + this->PatientBirthDateValueLabel->setWordWrap(true); + this->PatientSexValueLabel->setWordWrap(true); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMPatientItemWidgetPrivate::getPatientItemFromPatientID(const QString& patientID) +{ + if (!this->DicomDatabase) + { + return ""; + } + + QStringList patientList = this->DicomDatabase->patients(); + foreach (QString patientItem, patientList) + { + QString patientItemID = this->DicomDatabase->fieldForPatient("PatientID", patientItem); + + if (patientID == patientItemID) + { + return patientItem; + } + } + + return ""; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMPatientItemWidgetPrivate::formatDate(const QString& date) +{ + QString formattedDate = date; + formattedDate.replace(QString("-"), QString("")); + return QDate::fromString(formattedDate, "yyyyMMdd").toString(); +} + +//---------------------------------------------------------------------------- +bool ctkDICOMPatientItemWidgetPrivate::isStudyItemAlreadyAdded(const QString& studyItem) +{ + bool alreadyAdded = false; + foreach (ctkDICOMStudyItemWidget* studyItemWidget, this->StudyItemWidgetsList) + { + if (!studyItemWidget) + { + continue; + } + + if (studyItemWidget->studyItem() == studyItem) + { + alreadyAdded = true; + break; + } + } + + return alreadyAdded; +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidgetPrivate::clearLayout(QLayout* layout, bool deleteWidgets) +{ + Q_Q(ctkDICOMPatientItemWidget); + + if (!layout) + { + return; + } + + this->StudyItemWidgetsList.clear(); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, this->StudyItemWidgetsList) + { + if (!studyItemWidget) + { + continue; + } + + q->disconnect(studyItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + this->VisualDICOMBrowser.data(), SLOT(showStudyContextMenu(const QPoint&))); + + } + + while (QLayoutItem* item = layout->takeAt(0)) + { + if (deleteWidgets) + { + if (QWidget* widget = item->widget()) + { + widget->deleteLater(); + } + } + + if (QLayout* childLayout = item->layout()) + { + clearLayout(childLayout, deleteWidgets); + } + + delete item; + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidgetPrivate::createStudies() +{ + Q_Q(ctkDICOMPatientItemWidget); + + if (!this->DicomDatabase) + { + logger.error("createStudies failed, no DICOM Database has been set. \n"); + return; + } + + QLayout* studiesListWidgetLayout = this->StudiesListWidget->layout(); + if (this->PatientItem.isEmpty()) + { + this->PatientNameValueLabel->setText(""); + this->PatientIDValueLabel->setText(""); + this->PatientSexValueLabel->setText(""); + this->PatientBirthDateValueLabel->setText(""); + return; + } + else + { + QString patientName = this->DicomDatabase->fieldForPatient("PatientsName", this->PatientItem); + patientName.replace(R"(^)", R"( )"); + this->PatientNameValueLabel->setText(patientName); + this->PatientIDValueLabel->setText(this->DicomDatabase->fieldForPatient("PatientID", this->PatientItem)); + this->PatientSexValueLabel->setText(this->DicomDatabase->fieldForPatient("PatientsSex", this->PatientItem)); + this->PatientBirthDateValueLabel->setText(this->formatDate(this->DicomDatabase->fieldForPatient("PatientsBirthDate", this->PatientItem))); + } + + QStringList studiesList = this->DicomDatabase->studiesForPatient(this->PatientItem); + + if (studiesList.count() == 0) + { + return; + } + + // Filter with studyDescription and studyDate and sort by Date + QMap studiesMap; + foreach (QString studyItem, studiesList) + { + if (this->isStudyItemAlreadyAdded(studyItem)) + { + continue; + } + + QString studyInstanceUID = this->DicomDatabase->fieldForStudy("StudyInstanceUID", studyItem); + if (studyInstanceUID.isEmpty()) + { + continue; + } + + QString studyDateString = this->DicomDatabase->fieldForStudy("StudyDate", studyItem); + studyDateString.replace(QString("-"), QString("")); + QString studyDescription = this->DicomDatabase->fieldForStudy("StudyDescription", studyItem); + + if (studyDateString.isEmpty()) + { + studyDateString = this->DicomDatabase->fieldForPatient("PatientsBirthDate", this->PatientItem); + if (studyDateString.isEmpty()) + { + studyDateString = "19000101"; + } + } + + if ((!this->FilteringStudyDescription.isEmpty() && + !studyDescription.contains(this->FilteringStudyDescription, Qt::CaseInsensitive))) + { + continue; + } + + int nDays = ctkDICOMPatientItemWidget::getNDaysFromFilteringDate(this->FilteringDate); + QDate studyDate = QDate::fromString(studyDateString, "yyyyMMdd"); + if (nDays != -1) + { + QDate endDate = QDate::currentDate(); + QDate startDate = endDate.addDays(-nDays); + if (studyDate < startDate || studyDate > endDate) + { + continue; + } + } + long long date = studyDate.toJulianDay(); + while (studiesMap.contains(date)) + { + date++; + } + // QMap automatically sort in ascending with the key, + // but we want descending (latest study should be in the first row) + long long key = LLONG_MAX - date; + studiesMap[key] = studyItem; + } + + foreach (QString studyItem, studiesMap) + { + q->addStudyItemWidget(studyItem); + } + + QSpacerItem* verticalSpacer = new QSpacerItem(0, 5, QSizePolicy::Fixed, QSizePolicy::Expanding); + studiesListWidgetLayout->addItem(verticalSpacer); +} + +//---------------------------------------------------------------------------- +// ctkDICOMPatientItemWidget methods + +//---------------------------------------------------------------------------- +ctkDICOMPatientItemWidget::ctkDICOMPatientItemWidget(QWidget* parentWidget) + : Superclass(parentWidget) + , d_ptr(new ctkDICOMPatientItemWidgetPrivate(*this)) +{ + Q_D(ctkDICOMPatientItemWidget); + d->init(parentWidget); +} + +//---------------------------------------------------------------------------- +ctkDICOMPatientItemWidget::~ctkDICOMPatientItemWidget() +{ +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setPatientItem(const QString& patientItem) +{ + Q_D(ctkDICOMPatientItemWidget); + d->PatientItem = patientItem; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMPatientItemWidget::patientItem() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->PatientItem; +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setPatientID(const QString& patientID) +{ + Q_D(ctkDICOMPatientItemWidget); + d->PatientID = patientID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMPatientItemWidget::patientID() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->PatientID; +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setFilteringStudyDescription(const QString& filteringStudyDescription) +{ + Q_D(ctkDICOMPatientItemWidget); + d->FilteringStudyDescription = filteringStudyDescription; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMPatientItemWidget::filteringStudyDescription() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->FilteringStudyDescription; +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setFilteringDate(const ctkDICOMPatientItemWidget::DateType& filteringDate) +{ + Q_D(ctkDICOMPatientItemWidget); + d->FilteringDate = filteringDate; +} + +//------------------------------------------------------------------------------ +ctkDICOMPatientItemWidget::DateType ctkDICOMPatientItemWidget::filteringDate() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->FilteringDate; +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setFilteringSeriesDescription(const QString& filteringSeriesDescription) +{ + Q_D(ctkDICOMPatientItemWidget); + d->FilteringSeriesDescription = filteringSeriesDescription; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMPatientItemWidget::filteringSeriesDescription() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->FilteringSeriesDescription; +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setFilteringModalities(const QStringList& filteringModalities) +{ + Q_D(ctkDICOMPatientItemWidget); + d->FilteringModalities = filteringModalities; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMPatientItemWidget::filteringModalities() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->FilteringModalities; +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setNumberOfStudiesPerPatient(int numberOfStudiesPerPatient) +{ + Q_D(ctkDICOMPatientItemWidget); + d->NumberOfStudiesPerPatient = numberOfStudiesPerPatient; +} + +//------------------------------------------------------------------------------ +int ctkDICOMPatientItemWidget::numberOfStudiesPerPatient() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->NumberOfStudiesPerPatient; +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setThumbnailSize(const ctkDICOMStudyItemWidget::ThumbnailSizeOption& thumbnailSize) +{ + Q_D(ctkDICOMPatientItemWidget); + d->ThumbnailSize = thumbnailSize; +} + +//------------------------------------------------------------------------------ +ctkDICOMStudyItemWidget::ThumbnailSizeOption ctkDICOMPatientItemWidget::thumbnailSize() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->ThumbnailSize; +} + +//---------------------------------------------------------------------------- +ctkDICOMScheduler* ctkDICOMPatientItemWidget::scheduler() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->Scheduler.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMPatientItemWidget::schedulerShared() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->Scheduler; +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::setScheduler(ctkDICOMScheduler& scheduler) +{ + Q_D(ctkDICOMPatientItemWidget); + if (d->Scheduler) + { + QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + } + + d->Scheduler = QSharedPointer(&scheduler, skipDelete); + + if (d->Scheduler) + { + QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::setScheduler(QSharedPointer scheduler) +{ + Q_D(ctkDICOMPatientItemWidget); + if (d->Scheduler) + { + QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + } + + d->Scheduler = scheduler; + + if (d->Scheduler) + { + QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + } +} + +//---------------------------------------------------------------------------- +ctkDICOMDatabase* ctkDICOMPatientItemWidget::dicomDatabase() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->DicomDatabase.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMPatientItemWidget::dicomDatabaseShared() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->DicomDatabase; +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::setDicomDatabase(ctkDICOMDatabase& dicomDatabase) +{ + Q_D(ctkDICOMPatientItemWidget); + d->DicomDatabase = QSharedPointer(&dicomDatabase, skipDelete); +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::setDicomDatabase(QSharedPointer dicomDatabase) +{ + Q_D(ctkDICOMPatientItemWidget); + d->DicomDatabase = dicomDatabase; +} + +//------------------------------------------------------------------------------ +QList ctkDICOMPatientItemWidget::studyItemWidgetsList() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->StudyItemWidgetsList; +} + +//------------------------------------------------------------------------------ +int ctkDICOMPatientItemWidget::getNDaysFromFilteringDate(DateType FilteringDate) +{ + int nDays = 0; + switch (FilteringDate) + { + case ctkDICOMPatientItemWidget::DateType::Any: + nDays = -1; + break; + case ctkDICOMPatientItemWidget::DateType::Today: + nDays = 0; + break; + case ctkDICOMPatientItemWidget::DateType::Yesterday: + nDays = 1; + break; + case ctkDICOMPatientItemWidget::DateType::LastWeek: + nDays = 7; + break; + case ctkDICOMPatientItemWidget::DateType::LastMonth: + nDays = 30; + break; + case ctkDICOMPatientItemWidget::DateType::LastYear: + nDays = 365; + break; + } + + return nDays; +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::addStudyItemWidget(const QString& studyItem) +{ + Q_D(ctkDICOMPatientItemWidget); + + if (!d->DicomDatabase) + { + logger.error("addStudyItemWidget failed, no DICOM Database has been set. \n"); + return; + } + + QString studyInstanceUID = d->DicomDatabase->fieldForStudy("StudyInstanceUID", studyItem); + QString studyID = d->DicomDatabase->fieldForStudy("StudyID", studyItem); + QString studyDate = d->DicomDatabase->fieldForStudy("StudyDate", studyItem); + QString formattedStudyDate = d->formatDate(studyDate); + QString studyDescription = d->DicomDatabase->fieldForStudy("StudyDescription", studyItem); + + ctkDICOMStudyItemWidget* studyItemWidget = new ctkDICOMStudyItemWidget(d->VisualDICOMBrowser.data()); + studyItemWidget->setStudyItem(studyItem); + studyItemWidget->setPatientID(d->PatientID); + studyItemWidget->setStudyInstanceUID(studyInstanceUID); + if (formattedStudyDate.isEmpty() && studyID.isEmpty()) + { + studyItemWidget->setTitle(tr("Study")); + } + else if (formattedStudyDate.isEmpty()) + { + studyItemWidget->setTitle(tr("Study ID %1").arg(studyID)); + } + else if (studyID.isEmpty()) + { + studyItemWidget->setTitle(tr("Study --- %1").arg(formattedStudyDate)); + } + else + { + studyItemWidget->setTitle(tr("Study ID %1 --- %2").arg(studyID).arg(formattedStudyDate)); + } + + studyItemWidget->setDescription(studyDescription); + studyItemWidget->setThumbnailSize(d->ThumbnailSize); + studyItemWidget->setFilteringSeriesDescription(d->FilteringSeriesDescription); + studyItemWidget->setFilteringModalities(d->FilteringModalities); + studyItemWidget->setDicomDatabase(d->DicomDatabase); + studyItemWidget->setScheduler(d->Scheduler); + // Show in default (and start query/retrieve) only for the first 2 studies + // NOTE: in the layout for each studyItemWidget there is a QSpacerItem + if (d->StudiesListWidget->layout()->count() < d->NumberOfStudiesPerPatient * 2) + { + studyItemWidget->generateSeries(); + } + else + { + studyItemWidget->setCollapsed(true); + this->connect(studyItemWidget->collapsibleGroupBox(), SIGNAL(toggled(bool)), + studyItemWidget, SLOT(generateSeries(bool))); + } + studyItemWidget->setContextMenuPolicy(Qt::CustomContextMenu); + + this->connect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemDoubleClicked(QTableWidgetItem *)), + d->VisualDICOMBrowser.data(), SLOT(onLoad())); + this->connect(studyItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + d->VisualDICOMBrowser.data(), SLOT(showStudyContextMenu(const QPoint&))); + this->connect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemClicked(QTableWidgetItem *)), + this, SLOT(onSeriesItemClicked())); + this->connect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemSelectionChanged()), + this, SLOT(raiseSelectedSeriesJobsPriority())); + + d->StudiesListWidget->layout()->addWidget(studyItemWidget); + d->StudyItemWidgetsList.append(studyItemWidget); +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::removeStudyItemWidget(const QString& studyItem) +{ + Q_D(ctkDICOMPatientItemWidget); + + for (int studyIndex = 0; studyIndex < d->StudyItemWidgetsList.size(); ++studyIndex) + { + ctkDICOMStudyItemWidget* studyItemWidget = + qobject_cast(d->StudyItemWidgetsList[studyIndex]); + if (!studyItemWidget || studyItemWidget->studyItem() != studyItem) + { + continue; + } + + this->disconnect(studyItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + d->VisualDICOMBrowser.data(), SLOT(showStudyContextMenu(const QPoint&))); + this->disconnect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemClicked(QTableWidgetItem *)), + this, SLOT(onSeriesItemClicked())); + this->disconnect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemSelectionChanged()), + this, SLOT(raiseSelectedSeriesJobsPriority())); + d->StudyItemWidgetsList.removeOne(studyItemWidget); + delete studyItemWidget; + break; + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::setSelection(bool selected) +{ + Q_D(ctkDICOMPatientItemWidget); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, d->StudyItemWidgetsList) + { + studyItemWidget->setSelection(selected); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::generateStudies() +{ + Q_D(ctkDICOMPatientItemWidget); + + d->createStudies(); + if (d->Scheduler && d->Scheduler->getNumberOfQueryRetrieveServers() > 0) + { + d->Scheduler->queryStudies(d->PatientID, QThread::NormalPriority); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::updateGUIFromScheduler(const QVariant& data) +{ + Q_D(ctkDICOMPatientItemWidget); + + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + d->createStudies(); + } + + if (td.JobUID.isEmpty() || + td.JobType != ctkDICOMJobResponseSet::JobType::QueryStudies || + td.PatientID != d->PatientID) + { + return; + } + + d->createStudies(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::raiseSelectedSeriesJobsPriority() +{ + Q_D(ctkDICOMPatientItemWidget); + + if (!d->Scheduler || d->Scheduler->getNumberOfQueryRetrieveServers() == 0) + { + logger.error("raiseSelectedSeriesJobsPriority failed, no task pool has been set. \n"); + return; + } + + QList seriesWidgets; + QList selectedSeriesWidgets; + foreach (ctkDICOMStudyItemWidget* studyItemWidget, d->StudyItemWidgetsList) + { + if (!studyItemWidget) + { + continue; + } + + QTableWidget* seriesListTableWidget = studyItemWidget->seriesListTableWidget(); + for (int row = 0; row < seriesListTableWidget->rowCount(); row++) + { + for (int column = 0; column < seriesListTableWidget->columnCount(); column++) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(seriesListTableWidget->cellWidget(row, column)); + seriesWidgets.append(seriesItemWidget); + } + } + + QList selectedItems = seriesListTableWidget->selectedItems(); + foreach (QTableWidgetItem* selectedItem, selectedItems) + { + if (!selectedItem) + { + continue; + } + + int row = selectedItem->row(); + int column = selectedItem->column(); + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(seriesListTableWidget->cellWidget(row, column)); + + selectedSeriesWidgets.append(seriesItemWidget); + } + } + + QStringList selectedSeriesInstanceUIDs; + foreach (ctkDICOMSeriesItemWidget* seriesWidget, seriesWidgets) + { + if (!seriesWidget) + { + continue; + } + + bool widgetIsSelected = selectedSeriesWidgets.contains(seriesWidget); + if (widgetIsSelected) + { + selectedSeriesInstanceUIDs.append(seriesWidget->seriesInstanceUID()); + } + + seriesWidget->setRaiseJobsPriority(widgetIsSelected); + } + + d->Scheduler->raiseJobsPriorityForSeries(selectedSeriesInstanceUIDs); +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::onSeriesItemClicked() +{ + Q_D(ctkDICOMPatientItemWidget); + + QTableWidget* seriesTable = qobject_cast(sender()); + if (!seriesTable) + { + return; + } + + if (QApplication::keyboardModifiers() & (Qt::ControlModifier | Qt::ShiftModifier)) + { + return; + } + + if (seriesTable->selectedItems().count() != 1) + { + return; + } + + foreach (ctkDICOMStudyItemWidget* studyItemWidget, d->StudyItemWidgetsList) + { + if (!studyItemWidget) + { + continue; + } + + QTableWidget* studySeriesTable = studyItemWidget->seriesListTableWidget(); + if (studySeriesTable == seriesTable) + { + continue; + } + + studySeriesTable->clearSelection(); + } +} diff --git a/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h new file mode 100644 index 0000000000..ee587401d0 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h @@ -0,0 +1,177 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMPatientItemWidget_h +#define __ctkDICOMPatientItemWidget_h + +#include "ctkDICOMWidgetsExport.h" + +// Qt includes +#include +#include + +// ctkDICOMWidgets includes +class ctkDICOMDatabase; +class ctkDICOMScheduler; + +// ctkDICOMWidgets includes +#include "ctkDICOMStudyItemWidget.h" +class ctkDICOMPatientItemWidgetPrivate; +class ctkDICOMStudyItemWidget; + +/// \ingroup DICOM_Widgets +class CTK_DICOM_WIDGETS_EXPORT ctkDICOMPatientItemWidget : public QWidget +{ + Q_OBJECT; + Q_ENUMS(DateType) + Q_PROPERTY(QString patientItem READ patientItem WRITE setPatientItem); + Q_PROPERTY(QString patientID READ patientID WRITE setPatientID); + Q_PROPERTY(int numberOfStudiesPerPatient READ numberOfStudiesPerPatient WRITE setNumberOfStudiesPerPatient); + Q_PROPERTY(ctkDICOMStudyItemWidget::ThumbnailSizeOption thumbnailSize READ thumbnailSize WRITE setThumbnailSize); + +public: + typedef QWidget Superclass; + explicit ctkDICOMPatientItemWidget(QWidget* parent = nullptr); + virtual ~ctkDICOMPatientItemWidget(); + + ///@{ + /// Patient item + void setPatientItem(const QString& patientItem); + QString patientItem() const; + ///@} + + ///@{ + /// Patient ID + void setPatientID(const QString& patientID); + QString patientID() const; + ///@} + + ///@{ + /// Query Filters + /// Empty by default + void setFilteringStudyDescription(const QString& filteringStudyDescription); + QString filteringStudyDescription() const; + ///@} + + /// Date filtering enum + enum DateType + { + Any = 0, + Today, + Yesterday, + LastWeek, + LastMonth, + LastYear + }; + + ///@{ + /// Available values: + /// Any, + /// Today, + /// Yesterday, + /// LastWeek, + /// LastMonth, + /// LastYear. + /// Any by default. + void setFilteringDate(const DateType& filteringDate); + DateType filteringDate() const; + ///@} + + ///@{ + /// Empty by default + void setFilteringSeriesDescription(const QString& filteringSeriesDescription); + QString filteringSeriesDescription() const; + ///@} + + ///@{ + /// ["Any", "CR", "CT", "MR", "NM", "US", "PT", "XA"] by default + void setFilteringModalities(const QStringList& filteringModalities); + QStringList filteringModalities() const; + ///@} + + ///@{ + /// Number of non collapsed studies per patient + /// 2 by default + void setNumberOfStudiesPerPatient(int numberOfStudiesPerPatient); + int numberOfStudiesPerPatient() const; + ///@} + + ///@{ + /// Set the thumbnail size: small, medium, large + /// medium by default + void setThumbnailSize(const ctkDICOMStudyItemWidget::ThumbnailSizeOption& thumbnailSize); + ctkDICOMStudyItemWidget::ThumbnailSizeOption thumbnailSize() const; + ///@} + + /// Return the scheduler. + Q_INVOKABLE ctkDICOMScheduler* scheduler() const; + /// Return the scheduler as a shared pointer + /// (not Python-wrappable). + QSharedPointer schedulerShared() const; + /// Set the scheduler. + Q_INVOKABLE void setScheduler(ctkDICOMScheduler& scheduler); + /// Set the scheduler as a shared pointer + /// (not Python-wrappable). + void setScheduler(QSharedPointer scheduler); + + /// Return the Dicom Database. + Q_INVOKABLE ctkDICOMDatabase* dicomDatabase() const; + /// Return Dicom Database as a shared pointer + /// (not Python-wrappable). + QSharedPointer dicomDatabaseShared() const; + /// Set the Dicom Database. + Q_INVOKABLE void setDicomDatabase(ctkDICOMDatabase& dicomDatabase); + /// Set the Dicom Database as a shared pointer + /// (not Python-wrappable). + void setDicomDatabase(QSharedPointer dicomDatabase); + + /// Return all the study item widgets for the patient + Q_INVOKABLE QList studyItemWidgetsList() const; + + /// Return number of days from filtering date attribute + Q_INVOKABLE static int getNDaysFromFilteringDate(ctkDICOMPatientItemWidget::DateType filteringDate); + + ///@{ + /// Add/Remove study item widgets + Q_INVOKABLE void addStudyItemWidget(const QString& studyItem); + Q_INVOKABLE void removeStudyItemWidget(const QString& studyItem); + ///@} + + /// Set selection for all studies/series + Q_INVOKABLE void setSelection(bool selected); + +public Q_SLOTS: + void generateStudies(); + void updateGUIFromScheduler(const QVariant& data); + void onSeriesItemClicked(); + void raiseSelectedSeriesJobsPriority(); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMPatientItemWidget); + Q_DISABLE_COPY(ctkDICOMPatientItemWidget); +}; + +#endif diff --git a/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp index 3d9fc778ec..e1ff86c54f 100644 --- a/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp @@ -28,7 +28,7 @@ #include #include -/// CTK includes +// ctkCore includes #include #include #include @@ -42,7 +42,6 @@ // ctkDICOMWidgets includes #include "ctkDICOMQueryRetrieveWidget.h" -#include "ctkDICOMQueryResultsTabWidget.h" #include "ui_ctkDICOMQueryRetrieveWidget.h" static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMQueryRetrieveWidget"); @@ -228,11 +227,11 @@ void ctkDICOMQueryRetrieveWidget::query() // create a query for the current server ctkDICOMQuery* query = new ctkDICOMQuery; d->CurrentQuery = query; + query->setConnectionName(parameters["Name"].toString()); query->setCallingAETitle(d->ServerNodeWidget->callingAETitle()); query->setCalledAETitle(parameters["AETitle"].toString()); query->setHost(parameters["Address"].toString()); query->setPort(parameters["Port"].toInt()); - query->setPreferCGET(parameters["CGET"].toBool()); // populate the query with the current search options query->setFilters( d->QueryWidget->parameters() ); @@ -351,21 +350,21 @@ void ctkDICOMQueryRetrieveWidget::retrieve() // Get information which server we want to get the study from and prepare request accordingly QMap::iterator queryIt = d->QueriesByStudyUID.find(studyUID); - ctkDICOMQuery* query = (queryIt == d->QueriesByStudyUID.end() ? nullptr : *queryIt); - if (!query) + ctkDICOMQuery* currentQuery = (queryIt == d->QueriesByStudyUID.end() ? nullptr : *queryIt); + if (!currentQuery) { logger.warn("Retrieve of series " + seriesUID + " failed. No query found for study " + studyUID + "."); continue; } retrieve->setDatabase( d->RetrieveDatabase ); - retrieve->setCallingAETitle( query->callingAETitle() ); - retrieve->setCalledAETitle( query->calledAETitle() ); - retrieve->setPort( query->port() ); - retrieve->setHost( query->host() ); + retrieve->setCallingAETitle( currentQuery->callingAETitle() ); + retrieve->setCalledAETitle( currentQuery->calledAETitle() ); + retrieve->setPort( currentQuery->port() ); + retrieve->setHost( currentQuery->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 - logger.debug("About to retrieve " + seriesUID + " from " + query->host()); + logger.debug("About to retrieve " + seriesUID + " from " + currentQuery->host()); logger.info ( "Starting to retrieve" ); if(d->UseProgressDialog) @@ -379,7 +378,18 @@ void ctkDICOMQueryRetrieveWidget::retrieve() try { // perform the retrieve - if ( query->preferCGET() ) + QMap parameters; + foreach(QString server, d->QueriesByServer.keys()) + { + ctkDICOMQuery* query = d->QueriesByServer[server]; + if (query == currentQuery) + { + parameters = d->ServerNodeWidget->serverNodeParameters(server); + break; + } + } + + if ( parameters["CGET"].toBool() ) { retrieve->getSeries ( studyUID, seriesUID ); } @@ -418,9 +428,9 @@ void ctkDICOMQueryRetrieveWidget::retrieve() logger.info ( "Retrieve success" ); } - if (retrieve->database()) + if (retrieve->dicomDatabase()) { - retrieve->database()->updateDisplayedFields(); + retrieve->dicomDatabase()->updateDisplayedFields(); } if(d->UseProgressDialog) diff --git a/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.h b/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.h index 5265c99c8b..6fa65374f9 100644 --- a/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.h +++ b/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.h @@ -21,8 +21,6 @@ #ifndef __ctkDICOMQueryRetrieveWidget_h #define __ctkDICOMQueryRetrieveWidget_h -#include "ctkDICOMWidgetsExport.h" - // Qt includes #include #include @@ -30,10 +28,12 @@ #include #include -class ctkDICOMTableManager; -// CTK includes +// ctkDICOMCore includes #include +// ctkDICOMWidgets includes +#include "ctkDICOMWidgetsExport.h" +class ctkDICOMTableManager; class ctkDICOMQueryRetrieveWidgetPrivate; /// \ingroup DICOM_Widgets diff --git a/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp new file mode 100644 index 0000000000..81e7bc5c4f --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp @@ -0,0 +1,903 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include +#include +#include +#include +#include + +// CTK includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMDatabase.h" +#include "ctkDICOMScheduler.h" +#include "ctkDICOMJobResponseSet.h" +#include "ctkDICOMThumbnailGenerator.h" + +// ctkDICOMWidgets includes +#include "ctkDICOMSeriesItemWidget.h" +#include "ui_ctkDICOMSeriesItemWidget.h" + +static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMSeriesItemWidget"); + +//---------------------------------------------------------------------------- +class ctkDICOMSeriesItemWidgetPrivate : public Ui_ctkDICOMSeriesItemWidget +{ + Q_DECLARE_PUBLIC(ctkDICOMSeriesItemWidget); + +protected: + ctkDICOMSeriesItemWidget* const q_ptr; + +public: + ctkDICOMSeriesItemWidgetPrivate(ctkDICOMSeriesItemWidget& obj); + ~ctkDICOMSeriesItemWidgetPrivate(); + + void init(); + QString getDICOMCenterFrameFromInstances(QStringList instancesList); + void createThumbnail(ctkDICOMJobDetail td); + void drawModalityThumbnail(); + void drawThumbnail(const QString& file, int numberOfFrames); + void drawTextWithShadow(QPainter *painter, + const QFont &font, + int x, + int y, + Qt::Alignment alignment, + const QString &text); + void updateThumbnailProgressBar(); + + QSharedPointer DicomDatabase; + QSharedPointer Scheduler; + + QString PatientID; + QString SeriesItem; + QString StudyInstanceUID; + QString SeriesInstanceUID; + QString CentralFrameSOPInstanceUID; + QString SeriesNumber; + QString Modality; + bool StopJobs; + bool RaiseJobsPriority; + bool IsCloud; + bool RetrieveFailed; + bool IsLoaded; + bool IsVisible; + int ThumbnailSizePixel; + int NumberOfDownloads; + QImage ThumbnailImage; + bool isThumbnailDocument; +}; + +//---------------------------------------------------------------------------- +// ctkDICOMSeriesItemWidgetPrivate methods + +//---------------------------------------------------------------------------- +ctkDICOMSeriesItemWidgetPrivate::ctkDICOMSeriesItemWidgetPrivate(ctkDICOMSeriesItemWidget& obj) + : q_ptr(&obj) +{ + this->PatientID = ""; + this->SeriesItem = ""; + this->StudyInstanceUID = ""; + this->SeriesInstanceUID = ""; + this->CentralFrameSOPInstanceUID = ""; + this->SeriesNumber = ""; + this->Modality = ""; + + this->IsCloud = false; + this->RetrieveFailed = false; + this->IsLoaded = false; + this->IsVisible = false; + this->StopJobs = false; + this->RaiseJobsPriority = false; + this->isThumbnailDocument = false; + this->ThumbnailSizePixel = 200; + this->NumberOfDownloads = 0; + + this->DicomDatabase = nullptr; + this->Scheduler = nullptr; +} + +//---------------------------------------------------------------------------- +ctkDICOMSeriesItemWidgetPrivate::~ctkDICOMSeriesItemWidgetPrivate() +{ +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::init() +{ + Q_Q(ctkDICOMSeriesItemWidget); + this->setupUi(q); + + this->SeriesThumbnail->setTransformationMode(Qt::TransformationMode::SmoothTransformation); + this->SeriesThumbnail->textPushButton()->setElideMode(Qt::ElideRight); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMSeriesItemWidgetPrivate::getDICOMCenterFrameFromInstances(QStringList instancesList) +{ + if (!this->DicomDatabase) + { + logger.error("getDICOMCenterFrameFromInstances failed, no DICOM Database has been set. \n"); + return ""; + } + + if (instancesList.count() == 0) + { + return ""; + } + + // NOTE: we sort by the instance number. + // We could sort for 3D spatial values (ImagePatientPosition and ImagePatientOrientation), + // plus time information (for 4D datasets). However, this would require additional metadata fetching and logic, which can slow down. + QMap DICOMInstances; + foreach (QString instanceItem, instancesList) + { + int instanceNumber = 0; + QString instanceNumberString = this->DicomDatabase->instanceValue(instanceItem, "0020,0013"); + + if (instanceNumberString != "") + { + instanceNumber = instanceNumberString.toInt(); + } + + DICOMInstances[instanceNumber] = instanceItem; + } + + if (DICOMInstances.count() == 1) + { + return instancesList[0]; + } + + QList keys = DICOMInstances.keys(); + std::sort(keys.begin(), keys.end()); + + int centerFrameIndex = floor(keys.count() / 2); + if (keys.count() <= centerFrameIndex) + { + return instancesList[0]; + } + + int centerInstanceNumber = keys[centerFrameIndex]; + + return DICOMInstances[centerInstanceNumber]; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::createThumbnail(ctkDICOMJobDetail td) +{ + if (!this->DicomDatabase) + { + logger.error("importFiles failed, no DICOM Database has been set. \n"); + return; + } + + ctkDICOMJobResponseSet::JobType jobType = ctkDICOMJobResponseSet::JobType::None; + QString jobSopInstanceUID; + if (!td.JobUID.isEmpty()) + { + jobSopInstanceUID = td.SOPInstanceUID; + jobType = td.JobType; + } + + QStringList instancesList = this->DicomDatabase->instancesForSeries(this->SeriesInstanceUID); + int numberOfFrames = instancesList.count(); + if (numberOfFrames == 0) + { + this->drawModalityThumbnail(); + return; + } + + QStringList filesList = this->DicomDatabase->filesForSeries(this->SeriesInstanceUID); + filesList.removeAll(QString("")); + int numberOfFiles = filesList.count(); + QStringList urlsList = this->DicomDatabase->urlsForSeries(this->SeriesInstanceUID); + filesList.removeAll(QString("")); + int numberOfUrls = urlsList.count(); + if (!this->IsCloud && numberOfFrames > 0 && numberOfUrls > 0 && numberOfFiles < numberOfFrames) + { + this->IsCloud = true; + this->drawModalityThumbnail(); + } + else if (this->IsCloud && numberOfFrames > 0 && numberOfFiles == numberOfFrames) + { + this->IsCloud = false; + this->SeriesThumbnail->operationProgressBar()->hide(); + } + + if (!this->IsCloud) + { + if (this->DicomDatabase->visibleSeries().contains(this->SeriesInstanceUID)) + { + this->IsVisible = true; + } + else if (this->DicomDatabase->loadedSeries().contains(this->SeriesInstanceUID)) + { + this->IsLoaded = true; + } + else + { + this->IsVisible = false; + this->IsLoaded = false; + } + } + + QString file; + if (this->CentralFrameSOPInstanceUID.isEmpty()) + { + this->CentralFrameSOPInstanceUID = this->getDICOMCenterFrameFromInstances(instancesList); + file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); + + // Since getDICOMCenterFrameFromInstances is based on the sorting of the instance number, + // which is not always reliable, it could fail to get the right central frame. + // In these cases, we check if a frame has been already fetched and we use the first found one. + if (file.isEmpty() && numberOfFiles < numberOfFrames) + { + foreach (QString newFile, filesList) + { + if (file.isEmpty()) + { + continue; + } + + file = newFile; + this->CentralFrameSOPInstanceUID = this->DicomDatabase->instanceForFile(file); + break; + } + } + } + else + { + file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); + } + + if (!this->StopJobs && + this->Scheduler && + this->Scheduler->getNumberOfQueryRetrieveServers() > 0) + { + // Get file for thumbnail + if (file.isEmpty() && + this->IsCloud && + (jobType == ctkDICOMJobResponseSet::JobType::None || + jobType == ctkDICOMJobResponseSet::JobType::QueryInstances)) + { + this->Scheduler->retrieveSOPInstance(this->PatientID, + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->CentralFrameSOPInstanceUID, + this->RaiseJobsPriority ? QThread::HighestPriority : QThread::HighPriority); + + return; + } + + // Get series + if (numberOfFrames > 1 && + this->IsCloud && + ((jobSopInstanceUID == this->CentralFrameSOPInstanceUID && + (jobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance || + jobType == ctkDICOMJobResponseSet::JobType::StoreSOPInstance)) || + (jobType == ctkDICOMJobResponseSet::JobType::None || + jobType == ctkDICOMJobResponseSet::JobType::QueryInstances))) + { + this->Scheduler->retrieveSeries(this->PatientID, + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->RaiseJobsPriority ? QThread::HighestPriority : QThread::LowPriority); + } + } + + file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); + if ((jobSopInstanceUID.isEmpty() || + jobSopInstanceUID == this->CentralFrameSOPInstanceUID || + jobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance || + jobType == ctkDICOMJobResponseSet::JobType::StoreSOPInstance) && + !file.isEmpty()) + { + this->drawThumbnail(file, numberOfFrames); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::drawModalityThumbnail() +{ + if (!this->DicomDatabase) + { + logger.error("drawThumbnail failed, no DICOM Database has been set. \n"); + return; + } + + int textSize = floor(this->ThumbnailSizePixel / 7.); + QFont font = this->SeriesThumbnail->font(); + font.setBold(true); + font.setPixelSize(textSize); + + QPixmap resultPixmap(this->ThumbnailSizePixel, this->ThumbnailSizePixel); + resultPixmap.fill(Qt::transparent); + ctkDICOMThumbnailGenerator thumbnailGenerator; + thumbnailGenerator.setWidth(this->ThumbnailSizePixel); + thumbnailGenerator.setHeight(this->ThumbnailSizePixel); + + QImage thumbnailImage; + QPainter painter; + + QColor backgroundColor = this->SeriesThumbnail->palette().color(QPalette::Normal, QPalette::Window); + thumbnailGenerator.generateBlankThumbnail(thumbnailImage, backgroundColor); + resultPixmap = QPixmap::fromImage(thumbnailImage); + if (painter.begin(&resultPixmap)) + { + painter.setRenderHint(QPainter::Antialiasing); + QRect rect = resultPixmap.rect(); + int x = int(rect.width() * 0.5); + int y = int(rect.height() * 0.5); + this->drawTextWithShadow(&painter, font, x, y, Qt::AlignCenter, this->Modality); + painter.end(); + } + + this->SeriesThumbnail->setPixmap(resultPixmap); +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::drawThumbnail(const QString& file, int numberOfFrames) +{ + if (!this->DicomDatabase) + { + logger.error("drawThumbnail failed, no DICOM Database has been set. \n"); + return; + } + + int margin = floor(this->ThumbnailSizePixel / 60.); + int iconSize = floor(this->ThumbnailSizePixel / 6.); + int textSize = floor(this->ThumbnailSizePixel / 12.); + QFont font = this->SeriesThumbnail->font(); + font.setBold(true); + font.setPixelSize(textSize); + + QPixmap resultPixmap(this->ThumbnailSizePixel, this->ThumbnailSizePixel); + resultPixmap.fill(Qt::transparent); + ctkDICOMThumbnailGenerator thumbnailGenerator; + thumbnailGenerator.setWidth(this->ThumbnailSizePixel); + thumbnailGenerator.setHeight(this->ThumbnailSizePixel); + bool thumbnailGenerated = true; + bool emptyThumbnailGenerated = false; + QPainter painter; + if (this->ThumbnailImage.width() != this->ThumbnailSizePixel) + { + if (!thumbnailGenerator.generateThumbnail(file, this->ThumbnailImage)) + { + thumbnailGenerated = false; + emptyThumbnailGenerated = true; + this->isThumbnailDocument = true; + QColor backgroundColor = this->SeriesThumbnail->palette().color(QPalette::Normal, QPalette::Window); + thumbnailGenerator.generateBlankThumbnail(this->ThumbnailImage, backgroundColor); + resultPixmap = QPixmap::fromImage(this->ThumbnailImage); + if (painter.begin(&resultPixmap)) + { + painter.setRenderHint(QPainter::Antialiasing); + QSvgRenderer renderer(QString(":Icons/text_document.svg")); + renderer.render(&painter); + painter.end(); + } + } + } + + if (thumbnailGenerated && !this->isThumbnailDocument) + { + if (painter.begin(&resultPixmap)) + { + painter.setRenderHint(QPainter::Antialiasing); + QRect rect = resultPixmap.rect(); + painter.setFont(font); + int x = int((rect.width() * 0.5) - (this->ThumbnailImage.rect().width() * 0.5)); + int y = int((rect.height() * 0.5) - (this->ThumbnailImage.rect().height() * 0.5)); + painter.drawPixmap(x, y, QPixmap::fromImage(this->ThumbnailImage)); + + QString topLeftString = ctkDICOMSeriesItemWidget::tr("Series: %1\n%2").arg(this->SeriesNumber).arg(this->Modality); + this->drawTextWithShadow(&painter, font, margin, margin, Qt::AlignTop | Qt::AlignLeft, topLeftString); + QString rows = this->DicomDatabase->instanceValue(this->CentralFrameSOPInstanceUID, "0028,0010"); + QString columns = this->DicomDatabase->instanceValue(this->CentralFrameSOPInstanceUID, "0028,0011"); + QString bottomLeftString = rows + "x" + columns + "x" + QString::number(numberOfFrames); + this->drawTextWithShadow(&painter, font, margin, rect.height() - margin, + Qt::AlignBottom | Qt::AlignLeft, bottomLeftString); + QSvgRenderer renderer; + + if (this->IsCloud) + { + if (this->NumberOfDownloads > 0) + { + renderer.load(QString(":Icons/downloading.svg")); + } + else + { + renderer.load(QString(":Icons/cloud.svg")); + } + } + else if (this->IsVisible) + { + renderer.load(QString(":Icons/visible.svg")); + } + else if (this->IsLoaded) + { + renderer.load(QString(":Icons/loaded.svg")); + } + + QPoint topRight = rect.topRight(); + QRectF bounds(topRight.x() - iconSize - margin, topRight.y() + margin, iconSize, iconSize); + renderer.render(&painter, bounds); + painter.end(); + } + } + + if ((thumbnailGenerated && !this->isThumbnailDocument) || emptyThumbnailGenerated) + { + this->SeriesThumbnail->setPixmap(resultPixmap); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::drawTextWithShadow(QPainter *painter, + const QFont &font, + int x, + int y, + Qt::Alignment alignment, + const QString &text) +{ + QColor textColor(60, 164, 255, 225); + QGraphicsDropShadowEffect* dropShadowEffect = new QGraphicsDropShadowEffect; + dropShadowEffect->setXOffset(1); + dropShadowEffect->setYOffset(1); + dropShadowEffect->setBlurRadius(1); + dropShadowEffect->setColor(Qt::gray); + QLabel textLabel; + textLabel.setObjectName("ctkDrawTextWithShadowQLabel"); + QPalette palette = textLabel.palette(); + palette.setColor(QPalette::WindowText, textColor); + textLabel.setPalette(palette); + textLabel.setFont(font); + textLabel.setGraphicsEffect(dropShadowEffect); + textLabel.setText(text); + QPixmap textPixMap = textLabel.grab(); + QRect rect = textPixMap.rect(); + int textWidth = rect.width(); + int textHeight = rect.height(); + + if (alignment == Qt::AlignCenter) + { + painter->drawPixmap(x - textWidth * 0.5, y - textHeight * 0.5, textPixMap); + } + else if (alignment == (Qt::AlignTop | Qt::AlignLeft)) + { + painter->drawPixmap(x, y, textPixMap); + } + else if (alignment == Qt::AlignTop) + { + painter->drawPixmap(x - textWidth * 0.5, y, textPixMap); + } + else if (alignment == (Qt::AlignTop | Qt::AlignRight)) + { + painter->drawPixmap(x - textWidth, y, textPixMap); + } + else if (alignment == (Qt::AlignHCenter | Qt::AlignLeft)) + { + painter->drawPixmap(x, y - textHeight * 0.5, textPixMap); + } + else if (alignment == (Qt::AlignHCenter | Qt::AlignRight)) + { + painter->drawPixmap(x - textWidth, y - textHeight * 0.5, textPixMap); + } + else if (alignment == (Qt::AlignBottom | Qt::AlignLeft)) + { + painter->drawPixmap(x, y - textHeight, textPixMap); + } + else if (alignment == Qt::AlignBottom) + { + painter->drawPixmap(x - textWidth * 0.5, y - textHeight, textPixMap); + } + else if (alignment == (Qt::AlignBottom | Qt::AlignRight)) + { + painter->drawPixmap(x - textWidth, y - textHeight, textPixMap); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::updateThumbnailProgressBar() +{ + if (!this->IsCloud) + { + return; + } + + this->NumberOfDownloads++; + QStringList instancesList = this->DicomDatabase->instancesForSeries(this->SeriesInstanceUID); + int numberOfFrames = instancesList.count(); + float percentageOfInstancesOnLocal = float(this->NumberOfDownloads) / numberOfFrames; + int progress = ceil(percentageOfInstancesOnLocal * 100); + progress = progress > 100 ? 100 : progress; + this->SeriesThumbnail->setOperationProgress(progress); + if (this->NumberOfDownloads == 1) + { + this->SeriesThumbnail->operationProgressBar()->show(); + // change icons + QString file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); + if (!file.isEmpty()) + { + this->drawThumbnail(file, numberOfFrames); + } + } +} + +//---------------------------------------------------------------------------- +// ctkDICOMSeriesItemWidget methods + +//---------------------------------------------------------------------------- +ctkDICOMSeriesItemWidget::ctkDICOMSeriesItemWidget(QWidget* parentWidget) + : Superclass(parentWidget) + , d_ptr(new ctkDICOMSeriesItemWidgetPrivate(*this)) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->init(); +} + +//---------------------------------------------------------------------------- +ctkDICOMSeriesItemWidget::~ctkDICOMSeriesItemWidget() +{ +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setSeriesItem(const QString& seriesItem) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->SeriesItem = seriesItem; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMSeriesItemWidget::seriesItem() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->SeriesItem; +} + +//------------------------------------------------------------------------------ +void ctkDICOMSeriesItemWidget::setPatientID(const QString& patientID) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->PatientID = patientID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMSeriesItemWidget::patientID() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->PatientID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setStudyInstanceUID(const QString& studyInstanceUID) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->StudyInstanceUID = studyInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMSeriesItemWidget::studyInstanceUID() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->StudyInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setSeriesInstanceUID(const QString& seriesInstanceUID) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->SeriesInstanceUID = seriesInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMSeriesItemWidget::seriesInstanceUID() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->SeriesInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setSeriesNumber(const QString& seriesNumber) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->SeriesNumber = seriesNumber; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMSeriesItemWidget::seriesNumber() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->SeriesNumber; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setModality(const QString& modality) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->Modality = modality; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMSeriesItemWidget::modality() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->Modality; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setSeriesDescription(const QString& seriesDescription) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->SeriesThumbnail->setText(seriesDescription); + d->SeriesThumbnail->textPushButton()->setToolTip(seriesDescription); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMSeriesItemWidget::seriesDescription() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->SeriesThumbnail->text(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setStopJobs(bool stopJobs) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->StopJobs = stopJobs; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::stopJobs() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->StopJobs; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setRaiseJobsPriority(bool raiseJobsPriority) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->RaiseJobsPriority = raiseJobsPriority; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::raiseJobsPriority() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->RaiseJobsPriority; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::isCloud() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->IsCloud; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setRetrieveFailed(bool retrieveFailed) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->RetrieveFailed = retrieveFailed; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::retrieveFailed() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->RetrieveFailed; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::IsLoaded() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->IsLoaded; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::IsVisible() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->IsVisible; +} + +//------------------------------------------------------------------------------ +void ctkDICOMSeriesItemWidget::setThumbnailSizePixel(int thumbnailSizePixel) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->ThumbnailSizePixel = thumbnailSizePixel; +} + +//------------------------------------------------------------------------------ +int ctkDICOMSeriesItemWidget::thumbnailSizePixel() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->ThumbnailSizePixel; +} + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +ctkDICOMScheduler* ctkDICOMSeriesItemWidget::scheduler() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->Scheduler.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMSeriesItemWidget::schedulerShared() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->Scheduler; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setScheduler(ctkDICOMScheduler& scheduler) +{ + Q_D(ctkDICOMSeriesItemWidget); + if (d->Scheduler) + { + QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateSeriesProgressBar(QVariant))); + } + + d->Scheduler = QSharedPointer(&scheduler, skipDelete); + + if (d->Scheduler) + { + QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateSeriesProgressBar(QVariant))); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setScheduler(QSharedPointer scheduler) +{ + Q_D(ctkDICOMSeriesItemWidget); + if (d->Scheduler) + { + QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateSeriesProgressBar(QVariant))); + } + + d->Scheduler = scheduler; + + if (d->Scheduler) + { + QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateSeriesProgressBar(QVariant))); + } +} + +//---------------------------------------------------------------------------- +ctkDICOMDatabase* ctkDICOMSeriesItemWidget::dicomDatabase() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->DicomDatabase.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMSeriesItemWidget::dicomDatabaseShared() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->DicomDatabase; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setDicomDatabase(ctkDICOMDatabase& dicomDatabase) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->DicomDatabase = QSharedPointer(&dicomDatabase, skipDelete); +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setDicomDatabase(QSharedPointer dicomDatabase) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->DicomDatabase = dicomDatabase; +} + +//------------------------------------------------------------------------------ +void ctkDICOMSeriesItemWidget::generateInstances() +{ + Q_D(ctkDICOMSeriesItemWidget); + if (!d->DicomDatabase) + { + logger.error("generateInstances failed, no DICOM Database has been set. \n"); + return; + } + + ctkDICOMJobDetail td; + d->createThumbnail(td); + QStringList instancesList = d->DicomDatabase->instancesForSeries(d->SeriesInstanceUID); + if (!d->StopJobs && + instancesList.count() == 0 && + d->Scheduler && + d->Scheduler->getNumberOfQueryRetrieveServers() > 0) + { + d->Scheduler->queryInstances(d->PatientID, + d->StudyInstanceUID, + d->SeriesInstanceUID, + d->RaiseJobsPriority ? QThread::HighestPriority : QThread::NormalPriority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::updateGUIFromScheduler(const QVariant& data) +{ + Q_D(ctkDICOMSeriesItemWidget); + + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty() || + (td.JobType != ctkDICOMJobResponseSet::JobType::QueryInstances && + td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSeries && + td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance&& + td.JobType != ctkDICOMJobResponseSet::JobType::StoreSOPInstance) || + td.StudyInstanceUID != d->StudyInstanceUID || + td.SeriesInstanceUID != d->SeriesInstanceUID) + { + return; + } + + d->createThumbnail(td); +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::updateSeriesProgressBar(const QVariant& data) +{ + Q_D(ctkDICOMSeriesItemWidget); + + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty() || + (td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSeries && + td.JobType != ctkDICOMJobResponseSet::JobType::StoreSOPInstance) || + td.StudyInstanceUID != d->StudyInstanceUID || + td.SeriesInstanceUID != d->SeriesInstanceUID) + { + return; + } + + d->updateThumbnailProgressBar(); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h new file mode 100644 index 0000000000..fadd154770 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h @@ -0,0 +1,172 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMSeriesItemWidget_h +#define __ctkDICOMSeriesItemWidget_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +class ctkDICOMDatabase; +class ctkDICOMScheduler; + +// ctkDICOMWidgets includes +#include "ctkDICOMWidgetsExport.h" +class ctkDICOMSeriesItemWidgetPrivate; + +/// \ingroup DICOM_Widgets +class CTK_DICOM_WIDGETS_EXPORT ctkDICOMSeriesItemWidget : public QWidget +{ + Q_OBJECT; + Q_PROPERTY(QString seriesItem READ seriesItem WRITE setSeriesItem); + Q_PROPERTY(QString patientID READ patientID WRITE setPatientID); + Q_PROPERTY(QString studyInstanceUID READ studyInstanceUID WRITE setStudyInstanceUID); + Q_PROPERTY(QString seriesInstanceUID READ seriesInstanceUID WRITE setSeriesInstanceUID); + Q_PROPERTY(QString seriesNumber READ seriesNumber WRITE setSeriesNumber); + Q_PROPERTY(QString modality READ modality WRITE setModality); + Q_PROPERTY(QString seriesDescription READ seriesDescription WRITE setSeriesDescription); + Q_PROPERTY(bool isCloud READ isCloud); + Q_PROPERTY(bool retrieveFailed READ retrieveFailed WRITE setRetrieveFailed); + Q_PROPERTY(int thumbnailSizePixel READ thumbnailSizePixel WRITE setThumbnailSizePixel); + Q_PROPERTY(bool stopJobs READ stopJobs WRITE setStopJobs); + Q_PROPERTY(bool raiseJobsPriority READ raiseJobsPriority WRITE setRaiseJobsPriority); + +public: + typedef QWidget Superclass; + explicit ctkDICOMSeriesItemWidget(QWidget* parent = nullptr); + virtual ~ctkDICOMSeriesItemWidget(); + + ///@{ + /// Series Item + void setSeriesItem(const QString& seriesItem); + QString seriesItem() const; + ///@} + + ///@{ + /// Patient ID + void setPatientID(const QString& patientID); + QString patientID() const; + ///@} + + ///@{ + /// Study instance UID + void setStudyInstanceUID(const QString& studyInstanceUID); + QString studyInstanceUID() const; + ///@} + + ///@{ + /// Series instance UID + void setSeriesInstanceUID(const QString& seriesInstanceUID); + QString seriesInstanceUID() const; + ///@} + + ///@{ + /// Series Number + void setSeriesNumber(const QString& seriesNumber); + QString seriesNumber() const; + ///@} + + ///@{ + /// Modality + void setModality(const QString& modality); + QString modality() const; + ///@} + + ///@{ + /// Series Description + void setSeriesDescription(const QString& seriesDescription); + QString seriesDescription() const; + ///@} + + ///@{ + /// Stop Series widget to run new jobs + void setStopJobs(bool stopJobs); + bool stopJobs() const; + ///@} + + ///@{ + /// Set high priority to all jobs run from the Series widget + void setRaiseJobsPriority(bool raiseJobsPriority); + bool raiseJobsPriority() const; + ///@} + + /// Series lives in the server + bool isCloud() const; + + ///@{ + /// in case the retrieve job failed + void setRetrieveFailed(bool retrieveFailed); + bool retrieveFailed() const; + ///@} + + /// Series has been loaded by the parent widget + bool IsLoaded() const; + + /// Series is visible in the parent widget + bool IsVisible() const; + + ///@{ + /// Set the thumbnail size in pixel + /// 200 by default + void setThumbnailSizePixel(int thumbnailSizePixel); + int thumbnailSizePixel() const; + ///@} + + /// Return the scheduler. + Q_INVOKABLE ctkDICOMScheduler* scheduler() const; + /// Return the scheduler as a shared pointer + /// (not Python-wrappable). + QSharedPointer schedulerShared() const; + /// Set the scheduler. + Q_INVOKABLE void setScheduler(ctkDICOMScheduler& scheduler); + /// Set the scheduler as a shared pointer + /// (not Python-wrappable). + void setScheduler(QSharedPointer scheduler); + + /// Return the Dicom Database. + Q_INVOKABLE ctkDICOMDatabase* dicomDatabase() const; + /// Return Dicom Database as a shared pointer + /// (not Python-wrappable). + QSharedPointer dicomDatabaseShared() const; + /// Set the Dicom Database. + Q_INVOKABLE void setDicomDatabase(ctkDICOMDatabase& dicomDatabase); + /// Set the Dicom Database as a shared pointer + /// (not Python-wrappable). + void setDicomDatabase(QSharedPointer dicomDatabase); + +public Q_SLOTS: + void generateInstances(); + void updateGUIFromScheduler(const QVariant& data); + void updateSeriesProgressBar(const QVariant& data); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMSeriesItemWidget); + Q_DISABLE_COPY(ctkDICOMSeriesItemWidget); +}; + +#endif diff --git a/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp new file mode 100644 index 0000000000..696c2e6d8b --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp @@ -0,0 +1,1308 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ctkCore includes +#include +#include +#include +#include + +// ctkDICOMCore includes +#include +#include +#include + +// ctkDICOMWidgets includes +#include "ctkDICOMServerNodeWidget2.h" +#include "ui_ctkDICOMServerNodeWidget2.h" + +static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMServerNodeWidget2"); + +class QCenteredStyledItemDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + void paint(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + QStyleOptionViewItem opt = option; + const QWidget* widget = option.widget; + initStyleOption(&opt, index); + QStyle* style = opt.widget ? opt.widget->style() : QApplication::style(); + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, widget); + if (opt.features & QStyleOptionViewItem::HasCheckIndicator) + { + switch (opt.checkState) + { + case Qt::Unchecked: + opt.state |= QStyle::State_Off; + break; + case Qt::PartiallyChecked: + opt.state |= QStyle::State_NoChange; + break; + case Qt::Checked: + opt.state |= QStyle::State_On; + break; + } + auto rect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt, widget); + opt.rect = QStyle::alignedRect(opt.direction, Qt::AlignCenter, rect.size(), opt.rect); + opt.state = opt.state & ~QStyle::State_HasFocus; + style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &opt, painter, widget); + } + else if (!opt.icon.isNull()) + { + // draw the icon + QRect iconRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, widget); + iconRect = QStyle::alignedRect(opt.direction, Qt::AlignCenter, iconRect.size(), opt.rect); + QIcon::Mode mode = QIcon::Normal; + if (!(opt.state & QStyle::State_Enabled)) + { + mode = QIcon::Disabled; + } + else if (opt.state & QStyle::State_Selected) + { + mode = QIcon::Selected; + } + QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off; + opt.icon.paint(painter, iconRect, opt.decorationAlignment, mode, state); + } + else + { + QStyledItemDelegate::paint(painter, option, index); + } + } +protected: + bool editorEvent(QEvent* event, + QAbstractItemModel* model, + const QStyleOptionViewItem& option, + const QModelIndex& index) override + { + Q_ASSERT(event); + Q_ASSERT(model); + // make sure that the item is checkable + Qt::ItemFlags flags = model->flags(index); + if (!(flags & Qt::ItemIsUserCheckable) || !(option.state & QStyle::State_Enabled) || + !(flags & Qt::ItemIsEnabled)) + { + return false; + } + // make sure that we have a check state + QVariant value = index.data(Qt::CheckStateRole); + if (!value.isValid()) + { + return false; + } + const QWidget* widget = option.widget; + QStyle* style = option.widget ? widget->style() : QApplication::style(); + // make sure that we have the right event type + if ((event->type() == QEvent::MouseButtonRelease) || (event->type() == QEvent::MouseButtonDblClick) || + (event->type() == QEvent::MouseButtonPress)) + { + QStyleOptionViewItem viewOpt(option); + initStyleOption(&viewOpt, index); + QRect checkRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &viewOpt, widget); + checkRect = QStyle::alignedRect(viewOpt.direction, Qt::AlignCenter, checkRect.size(), viewOpt.rect); + QMouseEvent* me = static_cast(event); + if (me->button() != Qt::LeftButton || !checkRect.contains(me->pos())) + { + return false; + } + if ((event->type() == QEvent::MouseButtonPress) || (event->type() == QEvent::MouseButtonDblClick)) + { + return true; + } + } + else if (event->type() == QEvent::KeyPress) + { + if (static_cast(event)->key() != Qt::Key_Space && + static_cast(event)->key() != Qt::Key_Select) + { + return false; + } + } + else + { + return false; + } + Qt::CheckState state = static_cast(value.toInt()); + if (flags & Qt::ItemIsUserTristate) + { + state = (static_cast((state + 1) % 3)); + } + else + { + state = (state == Qt::Checked) ? Qt::Unchecked : Qt::Checked; + } + return model->setData(index, static_cast(state), Qt::CheckStateRole); + } +}; + +//---------------------------------------------------------------------------- +class ctkDICOMServerNodeWidget2Private : public Ui_ctkDICOMServerNodeWidget2 +{ + Q_DECLARE_PUBLIC(ctkDICOMServerNodeWidget2); + +protected: + ctkDICOMServerNodeWidget2* const q_ptr; + +public: + ctkDICOMServerNodeWidget2Private(ctkDICOMServerNodeWidget2& obj); + ~ctkDICOMServerNodeWidget2Private(); + + void init(); + void disconnectScheduler(); + void connectScheduler(); + /// Utility function that returns the storageAETitle and + /// storagePort in a map + QMap parameters() const; + + /// Return the list of server names + QStringList serverNodes() const; + /// Return all the information associated to a server defined by its name + QMap serverNodeParameters(const QString& connectionName) const; + QMap serverNodeParameters(int row) const; + QStringList getAllNodesName() const; + int getServerNodeRowFromConnectionName(const QString& connectionName) const; + QString getServerNodeConnectionNameFromRow(int row) const; + + /// Add a server node with the given parameters + /// Return the row index added into the table + int addServerNode(const QMap& parameters); + int addServerNode(ctkDICOMServer* server); + QSharedPointer createServerFromServerNode(const QMap& node); + void updateProxyComboBoxes(const QString& connectionName, int rowCount) const; + + bool SettingsModified; + QSharedPointer Scheduler; + QPushButton* SaveButton; + QPushButton* RestoreButton; +}; + +//---------------------------------------------------------------------------- +ctkDICOMServerNodeWidget2Private::ctkDICOMServerNodeWidget2Private(ctkDICOMServerNodeWidget2& obj) + : q_ptr(&obj) +{ + this->SettingsModified = false; + this->Scheduler = nullptr; + this->RestoreButton = nullptr; + this->SaveButton = nullptr; +} + +//---------------------------------------------------------------------------- +ctkDICOMServerNodeWidget2Private::~ctkDICOMServerNodeWidget2Private() +{ +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2Private::init() +{ + Q_Q(ctkDICOMServerNodeWidget2); + + this->setupUi(q); + + // checkable headers. + QHeaderView* previousHeaderView = this->NodeTable->horizontalHeader(); + ctkCheckableHeaderView* headerView = new ctkCheckableHeaderView(Qt::Horizontal, this->NodeTable); + headerView->setSectionsClickable(false); + headerView->setSectionsMovable(false); + headerView->setHighlightSections(false); + headerView->checkableModelHelper()->setPropagateDepth(-1); + headerView->setStretchLastSection(previousHeaderView->stretchLastSection()); + headerView->setMinimumSectionSize(previousHeaderView->minimumSectionSize()); + headerView->setDefaultSectionSize(previousHeaderView->defaultSectionSize()); + this->NodeTable->setHorizontalHeader(headerView); + + this->TestButton->setEnabled(false); + this->RemoveButton->setEnabled(false); + + this->NodeTable->setItemDelegateForColumn(ctkDICOMServerNodeWidget2::QueryRetrieveColumn, + new QCenteredStyledItemDelegate()); + this->NodeTable->setItemDelegateForColumn(ctkDICOMServerNodeWidget2::StorageColumn, + new QCenteredStyledItemDelegate()); + + QIntValidator* validator = new QIntValidator(0, INT_MAX); + this->StoragePort->setValidator(validator); + + q->readSettings(); + + QObject::connect(this->StorageEnabledCheckBox, SIGNAL(stateChanged(int)), + q, SLOT(onSettingsModified())); + QObject::connect(this->StorageAETitle, SIGNAL(textChanged(QString)), + q, SLOT(onSettingsModified())); + QObject::connect(this->StoragePort, SIGNAL(textChanged(QString)), + q, SLOT(onSettingsModified())); + + QObject::connect(this->NodeTable, SIGNAL(cellChanged(int,int)), + q, SLOT(onSettingsModified())); + QObject::connect(this->NodeTable, SIGNAL(itemSelectionChanged()), + q, SLOT(updateGUIState())); + + QObject::connect(this->AddButton, SIGNAL(clicked()), + q, SLOT(onAddServerNode())); + QObject::connect(this->TestButton, SIGNAL(clicked()), + q, SLOT(onTestCurrentServerNode())); + QObject::connect(this->RemoveButton, SIGNAL(clicked()), + q, SLOT(onRemoveCurrentServerNode())); + this->SaveButton = this->ActionsButtonBox->button(QDialogButtonBox::StandardButton::Save); + this->SaveButton->setText(QObject::tr("Apply changes")); + this->SaveButton->setIcon(QIcon(":/Icons/save.svg")); + this->RestoreButton = this->ActionsButtonBox->button(QDialogButtonBox::StandardButton::Discard); + this->RestoreButton->setText(QObject::tr("Discard changes")); + this->RestoreButton->setIcon(QIcon(":/Icons/cancel.svg")); + QObject::connect(this->RestoreButton, SIGNAL(clicked()), + q, SLOT(readSettings())); + QObject::connect(this->SaveButton, SIGNAL(clicked()), + q, SLOT(saveSettings())); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2Private::disconnectScheduler() +{ + Q_Q(ctkDICOMServerNodeWidget2); + if (!this->Scheduler) + { + return; + } + + ctkDICOMServerNodeWidget2::disconnect(this->Scheduler.data(), SIGNAL(jobStarted(QVariant)), + q, SLOT(updateGUIState())); + ctkDICOMServerNodeWidget2::disconnect(this->Scheduler.data(), SIGNAL(jobFinished(QVariant)), + q, SLOT(updateGUIState())); + ctkDICOMServerNodeWidget2::disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), + q, SLOT(updateGUIState())); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2Private::connectScheduler() +{ + Q_Q(ctkDICOMServerNodeWidget2); + if (!this->Scheduler) + { + return; + } + + ctkDICOMServerNodeWidget2::connect(this->Scheduler.data(), SIGNAL(jobStarted(QVariant)), + q, SLOT(updateGUIState())); + ctkDICOMServerNodeWidget2::connect(this->Scheduler.data(), SIGNAL(jobFinished(QVariant)), + q, SLOT(updateGUIState())); + ctkDICOMServerNodeWidget2::connect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), + q, SLOT(updateGUIState())); +} + +//---------------------------------------------------------------------------- +QMap ctkDICOMServerNodeWidget2Private::parameters() const +{ + Q_Q(const ctkDICOMServerNodeWidget2); + QMap parameters; + + parameters["StorageAETitle"] = this->StorageAETitle->text(); + parameters["StoragePort"] = q->storagePort(); + + return parameters; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMServerNodeWidget2Private::serverNodes() const +{ + QStringList nodes; + int count = this->NodeTable->rowCount(); + for (int row = 0; row < count; ++row) + { + QTableWidgetItem* item = this->NodeTable->item(row, ctkDICOMServerNodeWidget2::NameColumn); + nodes << (item ? item->text() : QString("")); + } + // If there are duplicates, serverNodeParameters(QString) will behave + // strangely + Q_ASSERT(nodes.removeDuplicates() == 0); + return nodes; +} + +//---------------------------------------------------------------------------- +QMap ctkDICOMServerNodeWidget2Private::serverNodeParameters(const QString& connectionName) const +{ + QMap parameters; + int count = this->NodeTable->rowCount(); + for (int row = 0; row < count; ++row) + { + if (this->NodeTable->item(row, 0)->text() == connectionName) + { + return this->serverNodeParameters(row); + } + } + + return parameters; +} + +//---------------------------------------------------------------------------- +QMap ctkDICOMServerNodeWidget2Private::serverNodeParameters(int row) const +{ + QMap node; + if (row < 0 || row >= this->NodeTable->rowCount()) + { + return node; + } + int columnCount = this->NodeTable->columnCount(); + for (int column = 0; column < columnCount; ++column) + { + if (!this->NodeTable->item(row, column)) + { + continue; + } + QString label = this->NodeTable->horizontalHeaderItem(column)->text(); + node[label] = this->NodeTable->item(row, column)->data(Qt::DisplayRole); + } + node["QueryRetrieveCheckState"] = this->NodeTable->item(row, ctkDICOMServerNodeWidget2::QueryRetrieveColumn) ? + this->NodeTable->item(row, ctkDICOMServerNodeWidget2::QueryRetrieveColumn)->checkState() : + static_cast(Qt::Unchecked); + node["StorageCheckState"] = this->NodeTable->item(row, ctkDICOMServerNodeWidget2::StorageColumn) ? + this->NodeTable->item(row, ctkDICOMServerNodeWidget2::StorageColumn)->checkState() : + static_cast(Qt::Unchecked); + + QLineEdit* portLineEdit = qobject_cast(this->NodeTable->cellWidget(row, ctkDICOMServerNodeWidget2::PortColumn)); + if (portLineEdit) + { + node["Port"] = portLineEdit->text(); + } + QSpinBox* timeoutSpinBox = qobject_cast(this->NodeTable->cellWidget(row, ctkDICOMServerNodeWidget2::TimeoutColumn)); + if (timeoutSpinBox) + { + node["Timeout"] = timeoutSpinBox->value(); + } + QComboBox* protocolComboBox = qobject_cast(this->NodeTable->cellWidget(row, ctkDICOMServerNodeWidget2::ProtocolColumn)); + if (protocolComboBox) + { + node["Protocol"] = protocolComboBox->currentText(); + } + QComboBox* proxyComboBox = qobject_cast(this->NodeTable->cellWidget(row, ctkDICOMServerNodeWidget2::ProxyColumn)); + if (proxyComboBox) + { + node["Proxy"] = proxyComboBox->currentText(); + } + + return node; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMServerNodeWidget2Private::getAllNodesName() const +{ + QStringList nodesNames; + int count = this->NodeTable->rowCount(); + for (int row = 0; row < count; ++row) + { + nodesNames.append(this->NodeTable->item(row, ctkDICOMServerNodeWidget2::NameColumn)->data(Qt::DisplayRole).toString()); + } + + return nodesNames; +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2Private::getServerNodeRowFromConnectionName(const QString& connectionName) const +{ + QMap parameters; + int count = this->NodeTable->rowCount(); + for (int row = 0; row < count; ++row) + { + if (this->NodeTable->item(row, 0)->text() == connectionName) + { + return row; + } + } + + return -1; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMServerNodeWidget2Private::getServerNodeConnectionNameFromRow(int row) const +{ + if (row < 0 || row >= this->NodeTable->rowCount()) + { + return ""; + } + + return this->NodeTable->item(row, 0)->text(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2Private::addServerNode(const QMap& node) +{ + Q_Q(ctkDICOMServerNodeWidget2); + + if (this->getServerNodeRowFromConnectionName(node["Name"].toString()) != -1) + { + logger.debug("addServerNode failed: the server has a duplicate. The connection name has to be unique \n"); + return -1; + } + + int rowCount = this->NodeTable->rowCount(); + this->NodeTable->setRowCount(rowCount + 1); + + QTableWidgetItem* newItem; + QString serverName = node["Name"].toString(); + newItem = new QTableWidgetItem(serverName); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::NameColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + newItem->setCheckState(Qt::CheckState(node["QueryRetrieveCheckState"].toInt())); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::QueryRetrieveColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + newItem->setCheckState(Qt::CheckState(node["StorageCheckState"].toInt())); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::StorageColumn, newItem); + + newItem = new QTableWidgetItem(node["Calling AETitle"].toString()); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::CallingAETitleColumn, newItem); + + newItem = new QTableWidgetItem(node["Called AETitle"].toString()); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::CalledAETitleColumn, newItem); + + newItem = new QTableWidgetItem(node["Address"].toString()); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::AddressColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + QLineEdit* portLineEdit = new QLineEdit(); + QIntValidator* validator = new QIntValidator(0, INT_MAX); + portLineEdit->setValidator(validator); + + portLineEdit->setObjectName("portLineEdit"); + portLineEdit->setText(node["Port"].toString()); + portLineEdit->setAlignment(Qt::AlignHCenter); + QObject::connect(portLineEdit, SIGNAL(textChanged(QString)), + q, SLOT(onSettingsModified())); + this->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::PortColumn, portLineEdit); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::PortColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + QComboBox* protocolComboBox = new QComboBox(); + protocolComboBox->setObjectName("protocolComboBox"); + protocolComboBox->addItem("CGET"); + protocolComboBox->addItem("CMOVE"); + // To Do: protocolComboBox->addItem("WADO"); + protocolComboBox->setCurrentIndex(protocolComboBox->findText(node["Protocol"].toString())); + QObject::connect(protocolComboBox, SIGNAL(currentIndexChanged(int)), + q, SLOT(onSettingsModified())); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::ProtocolColumn, newItem); + this->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::ProtocolColumn, protocolComboBox); + + newItem = new QTableWidgetItem(QString("")); + QSpinBox* timeoutSpinBox = new QSpinBox(); + timeoutSpinBox->setObjectName("timeoutSpinBox"); + timeoutSpinBox->setValue(node["Timeout"].toInt()); + timeoutSpinBox->setMinimum(1); + timeoutSpinBox->setMaximum(INT_MAX); + timeoutSpinBox->setSingleStep(1); + timeoutSpinBox->setSuffix(" s"); + timeoutSpinBox->setAlignment(Qt::AlignHCenter); + QObject::connect(timeoutSpinBox, SIGNAL(valueChanged(int)), + q, SLOT(onSettingsModified())); + this->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::TimeoutColumn, timeoutSpinBox); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::TimeoutColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + QComboBox* proxyComboBox = new QComboBox(); + proxyComboBox->setObjectName("proxyComboBox"); + QStringListModel* cbModel = new QStringListModel(); + proxyComboBox->setModel(cbModel); + + proxyComboBox->addItem(""); + QStringList nodesNames = this->getAllNodesName(); + nodesNames.removeOne(serverName); + QString proxyName = node["Proxy"].toString(); + if (!nodesNames.contains(proxyName) && !proxyName.isEmpty()) + { + nodesNames.append(proxyName); + } + proxyComboBox->addItems(nodesNames); + proxyComboBox->setCurrentIndex(proxyComboBox->findText(node["Proxy"].toString())); + QObject::connect(proxyComboBox, SIGNAL(currentIndexChanged(int)), + q, SLOT(onSettingsModified())); + this->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::ProxyColumn, proxyComboBox); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::ProxyColumn, newItem); + + q->onSettingsModified(); + + this->updateProxyComboBoxes(serverName, rowCount); + + return rowCount; +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2Private::addServerNode(ctkDICOMServer* server) +{ + Q_Q(ctkDICOMServerNodeWidget2); + + if (!server) + { + return -1; + } + + if (this->getServerNodeRowFromConnectionName(server->connectionName()) != -1) + { + logger.debug("addServerNode failed: the server has a duplicate. The connection name has to be unique \n"); + return -1; + } + + int rowCount = this->NodeTable->rowCount(); + this->NodeTable->setRowCount(rowCount + 1); + + QTableWidgetItem* newItem; + newItem = new QTableWidgetItem(server->connectionName()); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::NameColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + newItem->setCheckState(server->queryRetrieveEnabled() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::QueryRetrieveColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + newItem->setCheckState(server->storageEnabled() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::StorageColumn, newItem); + + newItem = new QTableWidgetItem(server->callingAETitle()); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::CallingAETitleColumn, newItem); + + newItem = new QTableWidgetItem(server->calledAETitle()); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::CalledAETitleColumn, newItem); + + newItem = new QTableWidgetItem(server->host()); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::AddressColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + QLineEdit* portLineEdit = new QLineEdit(); + QIntValidator* validator = new QIntValidator(0, INT_MAX); + portLineEdit->setValidator(validator); + + portLineEdit->setObjectName("portLineEdit"); + portLineEdit->setText(QString::number(server->port())); + portLineEdit->setAlignment(Qt::AlignHCenter); + QObject::connect(portLineEdit, SIGNAL(textChanged(QString)), + q, SLOT(onSettingsModified())); + this->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::PortColumn, portLineEdit); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::PortColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + QComboBox* protocolComboBox = new QComboBox(); + protocolComboBox->addItem("CGET"); + protocolComboBox->addItem("CMOVE"); + protocolComboBox->setCurrentIndex(protocolComboBox->findText(server->retrieveProtocolAsString())); + // To Do: protocolComboBox->addItem("WADO"); + QObject::connect(protocolComboBox, SIGNAL(currentIndexChanged(int)), + q, SLOT(onSettingsModified())); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::ProtocolColumn, newItem); + this->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::ProtocolColumn, protocolComboBox); + + newItem = new QTableWidgetItem(QString("")); + QSpinBox* timeoutSpinBox = new QSpinBox(); + timeoutSpinBox->setObjectName("timeoutSpinBox"); + timeoutSpinBox->setValue(server->connectionTimeout()); + timeoutSpinBox->setMinimum(1); + timeoutSpinBox->setMaximum(INT_MAX); + timeoutSpinBox->setSingleStep(1); + timeoutSpinBox->setSuffix(" s"); + timeoutSpinBox->setAlignment(Qt::AlignHCenter); + QObject::connect(timeoutSpinBox, SIGNAL(valueChanged(int)), + q, SLOT(onSettingsModified())); + this->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::TimeoutColumn, timeoutSpinBox); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::TimeoutColumn, newItem); + + + newItem = new QTableWidgetItem(QString("")); + QComboBox* proxyComboBox = new QComboBox(); + QStringListModel* cbModel = new QStringListModel(); + proxyComboBox->setModel(cbModel); + + proxyComboBox->addItem(""); + QStringList nodesNames = this->getAllNodesName(); + nodesNames.removeOne(server->connectionName()); + + if (server->proxyServer()) + { + QString proxyName = server->proxyServer()->connectionName(); + if (!nodesNames.contains(proxyName)) + { + nodesNames.append(proxyName); + } + } + proxyComboBox->addItems(nodesNames); + if (server->proxyServer()) + { + QString proxyName = server->proxyServer()->connectionName(); + proxyComboBox->setCurrentIndex(proxyComboBox->findText(proxyName)); + } + + QObject::connect(proxyComboBox, SIGNAL(currentIndexChanged(int)), + q, SLOT(onSettingsModified())); + this->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::ProxyColumn, proxyComboBox); + this->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::ProxyColumn, newItem); + + this->updateProxyComboBoxes(server->connectionName(), rowCount); + + if (server->proxyServer()) + { + this->addServerNode(server->proxyServer()); + rowCount++; + } + + q->onSettingsModified(); + + return rowCount; +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMServerNodeWidget2Private::createServerFromServerNode(const QMap& node) +{ + QSharedPointer server = + QSharedPointer(new ctkDICOMServer); + server->setConnectionName(node["Name"].toString()); + server->setQueryRetrieveEnabled(node["QueryRetrieveCheckState"].toInt() == 0 ? false : true); + server->setStorageEnabled(node["StorageCheckState"].toInt() == 0 ? false : true); + server->setCallingAETitle(node["Calling AETitle"].toString()); + server->setCalledAETitle(node["Called AETitle"].toString()); + server->setHost(node["Address"].toString()); + server->setPort(node["Port"].toInt()); + server->setRetrieveProtocolAsString(node["Protocol"].toString()); + server->setConnectionTimeout(node["Timeout"].toInt()); + server->setMoveDestinationAETitle(this->StorageAETitle->text()); + + return server; +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2Private::updateProxyComboBoxes(const QString& connectionName, int rowCount) const +{ + for (int row = 0; row < rowCount; ++row) + { + QComboBox* proxyComboBox = qobject_cast(this->NodeTable->cellWidget(row, ctkDICOMServerNodeWidget2::ProxyColumn)); + if (proxyComboBox) + { + QStringListModel* cbModel = qobject_cast(proxyComboBox->model()); + if (cbModel) + { + QStringList nodesNames = cbModel->stringList(); + if (nodesNames.contains(connectionName)) + { + continue; + } + } + proxyComboBox->addItem(connectionName); + } + } +} + +//---------------------------------------------------------------------------- +ctkDICOMServerNodeWidget2::ctkDICOMServerNodeWidget2(QWidget* parentWidget) + : Superclass(parentWidget) + , d_ptr(new ctkDICOMServerNodeWidget2Private(*this)) +{ + Q_D(ctkDICOMServerNodeWidget2); + + d->init(); +} + + +//---------------------------------------------------------------------------- +ctkDICOMServerNodeWidget2::~ctkDICOMServerNodeWidget2() +{ +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2::onAddServerNode() +{ + Q_D(ctkDICOMServerNodeWidget2); + int rowCount = d->NodeTable->rowCount(); + d->NodeTable->setRowCount(rowCount + 1); + + QString serverName = "server"; + QTableWidgetItem* newItem = new QTableWidgetItem(serverName); + d->NodeTable->setItem(rowCount, NameColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + newItem->setCheckState(Qt::Unchecked); + d->NodeTable->setItem(rowCount, QueryRetrieveColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + newItem->setCheckState(Qt::Unchecked); + d->NodeTable->setItem(rowCount, StorageColumn, newItem); + + newItem = new QTableWidgetItem(); + d->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::CallingAETitleColumn, newItem); + + newItem = new QTableWidgetItem(); + d->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::CalledAETitleColumn, newItem); + + newItem = new QTableWidgetItem(); + d->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::AddressColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + QLineEdit* portLineEdit = new QLineEdit(); + QIntValidator* validator = new QIntValidator(0, INT_MAX); + portLineEdit->setValidator(validator); + + portLineEdit->setObjectName("portLineEdit"); + portLineEdit->setText("80"); + portLineEdit->setAlignment(Qt::AlignHCenter); + QObject::connect(portLineEdit, SIGNAL(textChanged(QString)), + this, SLOT(onSettingsModified())); + d->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::PortColumn, portLineEdit); + d->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::PortColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + QComboBox* protocolComboBox = new QComboBox(); + protocolComboBox->setObjectName("protocolComboBox"); + protocolComboBox->addItem("CGET"); + protocolComboBox->addItem("CMOVE"); + // To Do: protocolComboBox->addItem("WADO"); + protocolComboBox->setCurrentIndex(protocolComboBox->findText("CGET")); + QObject::connect(protocolComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(onSettingsModified())); + d->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::ProtocolColumn, newItem); + d->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::ProtocolColumn, protocolComboBox); + + newItem = new QTableWidgetItem(QString("")); + QSpinBox* timeoutSpinBox = new QSpinBox(); + timeoutSpinBox->setObjectName("timeoutSpinBox"); + timeoutSpinBox->setValue(10); + timeoutSpinBox->setMinimum(1); + timeoutSpinBox->setMaximum(INT_MAX); + timeoutSpinBox->setSingleStep(1); + timeoutSpinBox->setSuffix(" s"); + timeoutSpinBox->setAlignment(Qt::AlignHCenter); + QObject::connect(timeoutSpinBox, SIGNAL(valueChanged(int)), + this, SLOT(onSettingsModified())); + d->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::TimeoutColumn, timeoutSpinBox); + d->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::TimeoutColumn, newItem); + + newItem = new QTableWidgetItem(QString("")); + QComboBox* proxyComboBox = new QComboBox(); + proxyComboBox->setObjectName("proxyComboBox"); + QStringListModel* cbModel = new QStringListModel(); + proxyComboBox->setModel(cbModel); + + proxyComboBox->addItem(""); + QStringList nodesNames = d->getAllNodesName(); + nodesNames.removeOne(serverName); + proxyComboBox->addItems(nodesNames); + proxyComboBox->setCurrentIndex(-1); + QObject::connect(proxyComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(onSettingsModified())); + d->NodeTable->setCellWidget(rowCount, ctkDICOMServerNodeWidget2::ProxyColumn, proxyComboBox); + d->NodeTable->setItem(rowCount, ctkDICOMServerNodeWidget2::ProxyColumn, newItem); + + d->NodeTable->setCurrentCell(rowCount, NameColumn); + + this->onSettingsModified(); + + return rowCount; +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::onRemoveCurrentServerNode() +{ + Q_D(ctkDICOMServerNodeWidget2); + + QModelIndexList selection = d->NodeTable->selectionModel()->selectedRows(); + if (selection.count() == 0) + { + return; + } + + QModelIndex index = selection.at(0); + int row = index.row(); + d->NodeTable->removeRow(row); + this->onSettingsModified(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::onTestCurrentServerNode() +{ + Q_D(ctkDICOMServerNodeWidget2); + + QModelIndexList selection = d->NodeTable->selectionModel()->selectedRows(); + if (selection.count() == 0) + { + return; + } + + QModelIndex index = selection.at(0); + QString serverName = d->getServerNodeConnectionNameFromRow(index.row()); + ctkDICOMServer* server = this->getServer(serverName.toStdString().c_str()); + if (!server) + { + return; + } + + ctkDICOMEcho echo; + echo.setConnectionName(server->connectionName()); + echo.setCalledAETitle(server->calledAETitle()); + echo.setCallingAETitle(server->callingAETitle()); + echo.setHost(server->host()); + echo.setPort(server->port()); + echo.setConnectionTimeout(server->connectionTimeout()); + + ctkMessageBox echoMessageBox(this); + QString messageString; + if (echo.echo()) + { + messageString = tr("Node response was positive."); + echoMessageBox.setIcon(QMessageBox::Information); + } + else + { + messageString = tr("Node response was negative."); + echoMessageBox.setIcon(QMessageBox::Warning); + } + + echoMessageBox.setText(messageString); + echoMessageBox.exec(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::updateGUIState() +{ + Q_D(ctkDICOMServerNodeWidget2); + QList selectedItems = d->NodeTable->selectedItems(); + d->RemoveButton->setEnabled(selectedItems.count() > 0); + d->TestButton->setEnabled(selectedItems.count() > 0); + + if (d->RestoreButton && d->SaveButton) + { + d->RestoreButton->setEnabled(d->SettingsModified); + d->SaveButton->setEnabled(d->SettingsModified); + } + + if (d->Scheduler && d->Scheduler->isStorageListenerActive()) + { + d->StorageStatusValueLabel->setText(QObject::tr("Active")); + } + else + { + d->StorageStatusValueLabel->setText(QObject::tr("Inactive")); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::onSettingsModified() +{ + Q_D(ctkDICOMServerNodeWidget2); + d->SettingsModified = true; + this->updateGUIState(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::saveSettings() +{ + Q_D(ctkDICOMServerNodeWidget2); + + if (!d->Scheduler) + { + return; + } + + QSettings settings; + int rowCount = d->NodeTable->rowCount(); + + settings.remove("DICOM/ServerNodes"); + this->removeAllServers(); + this->stopAllJobs(); + + settings.setValue("DICOM/ServerNodeCount", rowCount); + + QStringList proxyServers; + for (int row = 0; row < rowCount; ++row) + { + QMap node = d->serverNodeParameters(row); + QString proxyName = node["Proxy"].toString(); + if (!proxyName.isEmpty() && node["QueryRetrieveCheckState"].toInt() > 0) + { + proxyServers.append(proxyName); + } + + settings.setValue(QString("DICOM/ServerNodes/%1").arg(row), QVariant(node)); + } + + for (int row = 0; row < rowCount; ++row) + { + QMap node = d->serverNodeParameters(row); + QString serverName = node["Name"].toString(); + if (proxyServers.contains(serverName)) + { + continue; + } + + QSharedPointer server = d->createServerFromServerNode(node); + d->Scheduler->addServer(server); + } + + for (int ii = 0; ii < rowCount; ++ii) + { + QMap node = d->serverNodeParameters(ii); + QString serverName = node["Name"].toString(); + if (!proxyServers.contains(serverName)) + { + continue; + } + + QSharedPointer proxyServer = d->createServerFromServerNode(node); + for (int jj = 0; jj < rowCount; ++jj) + { + QMap tmpNode = d->serverNodeParameters(jj); + QString tmpServerName = tmpNode["Name"].toString(); + if (serverName == tmpServerName) + { + continue; + } + QString tmpProxyName = tmpNode["Proxy"].toString(); + if (serverName == tmpProxyName) + { + ctkDICOMServer* server = this->getServer(tmpServerName.toStdString().c_str()); + if (server) + { + server->setProxyServer(proxyServer); + server->setMoveDestinationAETitle(proxyServer->calledAETitle()); + break; + } + } + } + } + + settings.setValue("DICOM/StorageEnabled", this->storageListenerEnabled()); + settings.setValue("DICOM/StorageAETitle", this->storageAETitle()); + settings.setValue("DICOM/StoragePort", this->storagePort()); + settings.sync(); + + d->SettingsModified = false; + + if (d->StorageEnabledCheckBox->isChecked() && !d->Scheduler->isStorageListenerActive()) + { + d->Scheduler->startListener(this->storagePort(), + this->storageAETitle(), + QThread::Priority::NormalPriority); + } + + this->updateGUIState(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::readSettings() +{ + Q_D(ctkDICOMServerNodeWidget2); + + d->NodeTable->setRowCount(0); + + QSettings settings; + + QMap node; + if (settings.status() == QSettings::AccessError || + settings.value("DICOM/ServerNodeCount").toInt() == 0) + { + d->StorageAETitle->setText("CTKSTORE"); + d->StoragePort->setText("11112"); + d->StorageEnabledCheckBox->setChecked(false); + + // a dummy example + QMap defaultServerNode; + defaultServerNode["Name"] = QString("ExampleHost"); + defaultServerNode["QueryRetrieveCheckState"] = static_cast(Qt::Unchecked); + defaultServerNode["StorageCheckState"] = static_cast(Qt::Unchecked); + defaultServerNode["Calling AETitle"] = QString("CTK"); + defaultServerNode["Called AETitle"] = QString("AETITLE"); + defaultServerNode["Address"] = QString("dicom.example.com"); + defaultServerNode["Port"] = QString("11112"); + defaultServerNode["Protocol"] = QString("CGET"); + defaultServerNode["Timeout"] = QString("30"); + defaultServerNode["Proxy"] = QString(""); + d->addServerNode(defaultServerNode); + + // the uk example - see http://www.dicomserver.co.uk/ + // and http://www.medicalconnections.co.uk/ + defaultServerNode["Name"] = QString("MedicalConnections"); + defaultServerNode["QueryRetrieveCheckState"] = static_cast(Qt::Unchecked); + defaultServerNode["StorageCheckState"] = static_cast(Qt::Unchecked); + defaultServerNode["Calling AETitle"] = QString("CTK"); + defaultServerNode["Called AETitle"] = QString("ANYAE"); + defaultServerNode["Address"] = QString("dicomserver.co.uk"); + defaultServerNode["Port"] = QString("104"); + defaultServerNode["Protocol"] = QString("CGET"); + defaultServerNode["Timeout"] = QString("30"); + defaultServerNode["Proxy"] = QString(""); + d->addServerNode(defaultServerNode); + + d->SettingsModified = false; + d->NodeTable->clearSelection(); + this->updateGUIState(); + return; + } + + d->StorageEnabledCheckBox->setChecked(settings.value("DICOM/StorageEnabled").toBool()); + d->StorageAETitle->setText(settings.value("DICOM/StorageAETitle").toString()); + d->StoragePort->setText(settings.value("DICOM/StoragePort").toString()); + + int count = settings.value("DICOM/ServerNodeCount").toInt(); + for (int row = 0; row < count; ++row) + { + node = settings.value(QString("DICOM/ServerNodes/%1").arg(row)).toMap(); + d->addServerNode(node); + } + + d->SettingsModified = false; + d->NodeTable->clearSelection(); + this->updateGUIState(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::setStorageListenerEnabled(const bool enabled) +{ + Q_D(const ctkDICOMServerNodeWidget2); + d->StorageEnabledCheckBox->setChecked(enabled); + this->onSettingsModified(); +} + +//---------------------------------------------------------------------------- +bool ctkDICOMServerNodeWidget2::storageListenerEnabled() const +{ + Q_D(const ctkDICOMServerNodeWidget2); + return d->StorageEnabledCheckBox->isChecked(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::setStorageAETitle(const QString& storageAETitle) +{ + Q_D(const ctkDICOMServerNodeWidget2); + d->StorageAETitle->setText(storageAETitle); + this->onSettingsModified(); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMServerNodeWidget2::storageAETitle() const +{ + Q_D(const ctkDICOMServerNodeWidget2); + return d->StorageAETitle->text(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::setStoragePort(int storagePort) +{ + Q_D(const ctkDICOMServerNodeWidget2); + d->StoragePort->setText(QString::number(storagePort)); + this->onSettingsModified(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2::storagePort() const +{ + Q_D(const ctkDICOMServerNodeWidget2); + bool ok = false; + int port = d->StoragePort->text().toInt(&ok); + Q_ASSERT(ok); + return port; +} + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +ctkDICOMScheduler* ctkDICOMServerNodeWidget2::scheduler() const +{ + Q_D(const ctkDICOMServerNodeWidget2); + return d->Scheduler.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMServerNodeWidget2::schedulerShared() const +{ + Q_D(const ctkDICOMServerNodeWidget2); + return d->Scheduler; +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::setScheduler(ctkDICOMScheduler& scheduler) +{ + Q_D(ctkDICOMServerNodeWidget2); + d->disconnectScheduler(); + d->Scheduler = QSharedPointer(&scheduler, skipDelete); + d->connectScheduler(); + this->saveSettings(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::setScheduler(QSharedPointer scheduler) +{ + Q_D(ctkDICOMServerNodeWidget2); + d->disconnectScheduler(); + d->Scheduler = scheduler; + d->connectScheduler(); + this->saveSettings(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2::getNumberOfServers() +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("getNumberOfServers failed, no task pool has been set. \n"); + return -1; + } + + return d->Scheduler->getNumberOfServers(); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMServerNodeWidget2::getNthServer(int id) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("getNthServer failed, no task pool has been set. \n"); + return nullptr; + } + + return d->Scheduler->getNthServer(id); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMServerNodeWidget2::getServer(const QString& connectionName) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("getServer failed, no task pool has been set. \n"); + return nullptr; + } + + return d->Scheduler->getServer(connectionName); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::addServer(ctkDICOMServer* server) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("addServer failed, no task pool has been set. \n"); + return; + } + + d->addServerNode(server); + this->saveSettings(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::removeServer(const QString& connectionName) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("removeServer failed, no task pool has been set. \n"); + return; + } + + this->removeNthServer(this->getServerIndexFromName(connectionName)); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::removeNthServer(int id) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("removeNthServer failed, no task pool has been set. \n"); + return; + } + + QString connectionName = this->getServerNameFromIndex(id); + int row = d->getServerNodeRowFromConnectionName(connectionName); + d->NodeTable->removeRow(row); + this->saveSettings(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::removeAllServers() +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("removeAllServers failed, no task pool has been set. \n"); + return; + } + + d->Scheduler->removeAllServers(); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMServerNodeWidget2::getServerNameFromIndex(int id) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("getServerNameFromIndex failed, no task pool has been set. \n"); + return ""; + } + + return d->Scheduler->getServerNameFromIndex(id); +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2::getServerIndexFromName(const QString& connectionName) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + logger.error("getServerIndexFromName failed, no task pool has been set. \n"); + return -1; + } + + return d->Scheduler->getServerIndexFromName(connectionName); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::stopAllJobs() +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->Scheduler) + { + return; + } + + QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); + d->Scheduler->stopAllJobs(true); + QApplication::restoreOverrideCursor(); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h new file mode 100644 index 0000000000..9f18490533 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h @@ -0,0 +1,137 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMServerNodeWidget2_h +#define __ctkDICOMServerNodeWidget2_h + +// Qt includes +#include +#include +#include +#include +class QTableWidgetItem; + +// ctkCore includes +class ctkAbstractTask; + +// ctkDICOMCore includes +class ctkDICOMScheduler; +class ctkDICOMServer; + +// ctkDICOMWidgets includes +#include "ctkDICOMWidgetsExport.h" +class ctkDICOMServerNodeWidget2Private; + +/// \ingroup DICOM_Widgets +class CTK_DICOM_WIDGETS_EXPORT ctkDICOMServerNodeWidget2 : public QWidget +{ + Q_OBJECT; + Q_PROPERTY(QString storageAETitle READ storageAETitle WRITE setStorageAETitle); + Q_PROPERTY(int storagePort READ storagePort WRITE setStoragePort); + +public: + typedef QWidget Superclass; + explicit ctkDICOMServerNodeWidget2(QWidget* parent = 0); + virtual ~ctkDICOMServerNodeWidget2(); + + ///@{ + /// Storage listener is enabled + /// false by default + void setStorageListenerEnabled(const bool enabled); + bool storageListenerEnabled() const; + ///@} + + ///@{ + /// Storage AE title + /// "CTKSTORE" by default + void setStorageAETitle(const QString& storageAETitle); + QString storageAETitle() const; + ///@} + + ///@{ + /// Storage port + /// 11112 by default + void setStoragePort(int storagePort); + int storagePort() const; + ///@} + + /// Return the scheduler. + Q_INVOKABLE ctkDICOMScheduler* scheduler() const; + /// Return the scheduler as a shared pointer + /// (not Python-wrappable). + QSharedPointer schedulerShared() const; + /// Set the scheduler. + Q_INVOKABLE void setScheduler(ctkDICOMScheduler& scheduler); + /// Set the scheduler as a shared pointer + /// (not Python-wrappable). + void setScheduler(QSharedPointer scheduler); + + /// Servers + ///@{ + Q_INVOKABLE int getNumberOfServers(); + Q_INVOKABLE ctkDICOMServer* getNthServer(int id); + Q_INVOKABLE ctkDICOMServer* getServer(const QString& connectionName); + Q_INVOKABLE void addServer(ctkDICOMServer* server); + Q_INVOKABLE void removeServer(const QString& connectionName); + Q_INVOKABLE void removeNthServer(int id); + Q_INVOKABLE void removeAllServers(); + Q_INVOKABLE QString getServerNameFromIndex(int id); + Q_INVOKABLE int getServerIndexFromName(const QString& connectionName); + Q_INVOKABLE void stopAllJobs(); + ///@} + +public Q_SLOTS: + /// Add an empty server node and make it current + /// Return the row index added into the table + int onAddServerNode(); + /// Remove the current row (different from the checked rows) + void onRemoveCurrentServerNode(); + /// Test the current row (different from the checked rows) + void onTestCurrentServerNode(); + + void readSettings(); + void saveSettings(); + void updateGUIState(); + void onSettingsModified(); + +protected: + QScopedPointer d_ptr; + enum ServerColumns + { + NameColumn = 0, + QueryRetrieveColumn, + StorageColumn, + CallingAETitleColumn, + CalledAETitleColumn, + AddressColumn, + PortColumn, + TimeoutColumn, + ProtocolColumn, + ProxyColumn + }; +private: + Q_DECLARE_PRIVATE(ctkDICOMServerNodeWidget2); + Q_DISABLE_COPY(ctkDICOMServerNodeWidget2); +}; + +#endif diff --git a/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp new file mode 100644 index 0000000000..0ca136ba95 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp @@ -0,0 +1,787 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include + +// CTK includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMDatabase.h" +#include "ctkDICOMScheduler.h" +#include "ctkDICOMJobResponseSet.h" + +// ctkDICOMWidgets includes +#include "ctkDICOMStudyItemWidget.h" +#include "ui_ctkDICOMStudyItemWidget.h" + +#include + +static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMStudyItemWidget"); + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +class ctkDICOMStudyItemWidgetPrivate : public Ui_ctkDICOMStudyItemWidget +{ + Q_DECLARE_PUBLIC(ctkDICOMStudyItemWidget); + +protected: + ctkDICOMStudyItemWidget* const q_ptr; + +public: + ctkDICOMStudyItemWidgetPrivate(ctkDICOMStudyItemWidget& obj); + ~ctkDICOMStudyItemWidgetPrivate(); + + void init(QWidget* parentWidget); + void updateColumnsWidths(); + void createSeries(); + int getScreenWidth(); + int getScreenHeight(); + int calculateNumerOfSeriesPerRow(); + int calculateThumbnailSizeInPixel(const ctkDICOMStudyItemWidget::ThumbnailSizeOption& thumbnailSize); + void addEmptySeriesItemWidget(int rowIndex, int columnIndex); + bool isSeriesItemAlreadyAdded(const QString& seriesItem); + + QString FilteringSeriesDescription; + QStringList FilteringModalities; + + QSharedPointer DicomDatabase; + QSharedPointer Scheduler; + QSharedPointer VisualDICOMBrowser; + + ctkDICOMStudyItemWidget::ThumbnailSizeOption ThumbnailSize; + int ThumbnailSizePixel; + QString PatientID; + QString StudyInstanceUID; + QString StudyItem; +}; + +//---------------------------------------------------------------------------- +// ctkDICOMStudyItemWidgetPrivate methods + +//---------------------------------------------------------------------------- +ctkDICOMStudyItemWidgetPrivate::ctkDICOMStudyItemWidgetPrivate(ctkDICOMStudyItemWidget& obj) + : q_ptr(&obj) +{ + this->ThumbnailSize = ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium; + this->ThumbnailSizePixel = 200; + this->FilteringSeriesDescription = ""; + this->PatientID = ""; + this->StudyInstanceUID = ""; + this->StudyItem = ""; + + this->DicomDatabase = nullptr; + this->Scheduler = nullptr; + this->VisualDICOMBrowser = nullptr; +} + +//---------------------------------------------------------------------------- +ctkDICOMStudyItemWidgetPrivate::~ctkDICOMStudyItemWidgetPrivate() +{ + Q_Q(ctkDICOMStudyItemWidget); + + for (int row = 0; row < this->SeriesListTableWidget->rowCount(); row++) + { + for (int column = 0; column < this->SeriesListTableWidget->columnCount(); column++) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(this->SeriesListTableWidget->cellWidget(row, column)); + if (!seriesItemWidget) + { + continue; + } + + q->disconnect(seriesItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + this->VisualDICOMBrowser.data(), SLOT(showSeriesContextMenu(const QPoint&))); + } + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidgetPrivate::init(QWidget* parentWidget) +{ + Q_Q(ctkDICOMStudyItemWidget); + this->setupUi(q); + + this->VisualDICOMBrowser = QSharedPointer(parentWidget, skipDelete); + + this->StudyDescriptionTextBrowser->hide(); + this->StudyDescriptionTextBrowser->setReadOnly(true); + this->StudyItemCollapsibleGroupBox->setCollapsed(false); + + q->connect(this->StudySelectionCheckBox, SIGNAL(clicked(bool)), + q, SLOT(onStudySelectionClicked(bool))); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidgetPrivate::updateColumnsWidths() +{ + for (int columnIndex = 0; columnIndex < this->SeriesListTableWidget->columnCount(); ++columnIndex) + { + this->SeriesListTableWidget->setColumnWidth(columnIndex, this->ThumbnailSizePixel); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidgetPrivate::createSeries() +{ + Q_Q(ctkDICOMStudyItemWidget); + + if (!this->DicomDatabase) + { + logger.error("createSeries failed, no DICOM Database has been set. \n"); + return; + } + + QStringList seriesList = this->DicomDatabase->seriesForStudy(this->StudyInstanceUID); + if (seriesList.count() == 0) + { + return; + } + + // Sort by SeriesNumber + QMap seriesMap; + foreach (QString seriesItem, seriesList) + { + if (this->isSeriesItemAlreadyAdded(seriesItem)) + { + continue; + } + + QString modality = this->DicomDatabase->fieldForSeries("Modality", seriesItem); + QString seriesDescription = this->DicomDatabase->fieldForSeries("SeriesDescription", seriesItem); + // Filter with modality and seriesDescription + if ((this->FilteringSeriesDescription.isEmpty() || + seriesDescription.contains(this->FilteringSeriesDescription, Qt::CaseInsensitive)) && + (this->FilteringModalities.contains("Any") || this->FilteringModalities.contains(modality))) + { + int seriesNumber = this->DicomDatabase->fieldForSeries("SeriesNumber", seriesItem).toInt(); + while (seriesMap.contains(seriesNumber)) + { + seriesNumber++; + } + // QMap automatically sort in ascending with the key + seriesMap[seriesNumber] = seriesItem; + } + } + + int tableIndex = 0; + int seriesIndex = 0; + int numberOfSeries = seriesMap.count(); + foreach (QString seriesItem, seriesMap) + { + QString seriesInstanceUID = this->DicomDatabase->fieldForSeries("SeriesInstanceUID", seriesItem); + if (seriesInstanceUID.isEmpty()) + { + numberOfSeries--; + continue; + } + seriesIndex++; + + QString modality = this->DicomDatabase->fieldForSeries("Modality", seriesItem); + QString seriesDescription = this->DicomDatabase->fieldForSeries("SeriesDescription", seriesItem); + + q->addSeriesItemWidget(tableIndex, seriesItem, seriesInstanceUID, modality, seriesDescription); + tableIndex++; + + if (seriesIndex == numberOfSeries) + { + int emptyIndex = tableIndex; + int columnIndex = emptyIndex % this->SeriesListTableWidget->columnCount(); + while (columnIndex != 0) + { + int rowIndex = floor(emptyIndex / this->SeriesListTableWidget->columnCount()); + columnIndex = emptyIndex % this->SeriesListTableWidget->columnCount(); + this->addEmptySeriesItemWidget(rowIndex, columnIndex); + emptyIndex++; + } + } + + int iHeight = 0; + for (int rowIndex = 0; rowIndex < this->SeriesListTableWidget->rowCount(); ++rowIndex) + { + iHeight += this->SeriesListTableWidget->verticalHeader()->sectionSize(rowIndex); + } + if (iHeight < this->ThumbnailSizePixel) + { + iHeight = this->ThumbnailSizePixel; + } + iHeight += 25; + this->SeriesListTableWidget->setMinimumHeight(iHeight); + } +} + +//------------------------------------------------------------------------------ +int ctkDICOMStudyItemWidgetPrivate::getScreenWidth() +{ + QList screens = QApplication::screens(); + int width = 1920; + foreach (QScreen* screen, screens) + { + QRect rec = screen->geometry(); + if (rec.width() > width) + { + width = rec.width(); + } + } + + return width; +} + +//------------------------------------------------------------------------------ +int ctkDICOMStudyItemWidgetPrivate::getScreenHeight() +{ + QList screens = QApplication::screens(); + int height = 1080; + foreach (QScreen* screen, screens) + { + QRect rec = screen->geometry(); + if (rec.height() > height) + { + height = rec.height(); + } + } + + return height; +} + +//------------------------------------------------------------------------------ +int ctkDICOMStudyItemWidgetPrivate::calculateNumerOfSeriesPerRow() +{ + int width = this->getScreenWidth(); + int numberOfSeriesPerRow = 1; + numberOfSeriesPerRow = floor(width / this->ThumbnailSizePixel) - 1; + + return numberOfSeriesPerRow; +} + +//------------------------------------------------------------------------------ +int ctkDICOMStudyItemWidgetPrivate::calculateThumbnailSizeInPixel(const ctkDICOMStudyItemWidget::ThumbnailSizeOption& thumbnailSize) +{ + int height = this->getScreenHeight(); + int thumbnailSizeInPixel = 1; + switch (thumbnailSize) + { + case ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small: + { + thumbnailSizeInPixel = floor(height / 7.); + } + break; + case ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium: + { + thumbnailSizeInPixel = floor(height / 5.5); + } + break; + case ctkDICOMStudyItemWidget::ThumbnailSizeOption::Large: + { + thumbnailSizeInPixel = floor(height / 4.); + } + break; + } + + return thumbnailSizeInPixel; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidgetPrivate::addEmptySeriesItemWidget(int rowIndex, int columnIndex) +{ + QTableWidgetItem* tableItem = new QTableWidgetItem; + tableItem->setFlags(Qt::NoItemFlags); + tableItem->setSizeHint(QSize(this->ThumbnailSizePixel, this->ThumbnailSizePixel)); + + this->SeriesListTableWidget->setItem(rowIndex, columnIndex, tableItem); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMStudyItemWidgetPrivate::isSeriesItemAlreadyAdded(const QString& seriesItem) +{ + bool alreadyAdded = false; + for (int i = 0; i < this->SeriesListTableWidget->rowCount(); i++) + { + for (int j = 0; j < this->SeriesListTableWidget->columnCount(); j++) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(this->SeriesListTableWidget->cellWidget(i, j)); + if (!seriesItemWidget) + { + continue; + } + + if (seriesItemWidget->seriesItem() == seriesItem) + { + alreadyAdded = true; + break; + } + } + + if (alreadyAdded) + { + break; + } + } + + return alreadyAdded; +} + +//---------------------------------------------------------------------------- +// ctkDICOMStudyItemWidget methods + +//---------------------------------------------------------------------------- +ctkDICOMStudyItemWidget::ctkDICOMStudyItemWidget(QWidget* parentWidget) + : Superclass(parentWidget) + , d_ptr(new ctkDICOMStudyItemWidgetPrivate(*this)) +{ + Q_D(ctkDICOMStudyItemWidget); + d->init(parentWidget); +} + +//---------------------------------------------------------------------------- +ctkDICOMStudyItemWidget::~ctkDICOMStudyItemWidget() +{ +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setStudyItem(const QString& studyItem) +{ + Q_D(ctkDICOMStudyItemWidget); + d->StudyItem = studyItem; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMStudyItemWidget::studyItem() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->StudyItem; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::setPatientID(const QString& patientID) +{ + Q_D(ctkDICOMStudyItemWidget); + d->PatientID = patientID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStudyItemWidget::patientID() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->PatientID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setStudyInstanceUID(const QString& studyInstanceUID) +{ + Q_D(ctkDICOMStudyItemWidget); + d->StudyInstanceUID = studyInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStudyItemWidget::studyInstanceUID() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->StudyInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setTitle(const QString& title) +{ + Q_D(ctkDICOMStudyItemWidget); + d->StudyItemCollapsibleGroupBox->setTitle(title); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStudyItemWidget::title() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->StudyItemCollapsibleGroupBox->title(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setDescription(const QString& description) +{ + Q_D(ctkDICOMStudyItemWidget); + if (description.isEmpty()) + { + d->StudyDescriptionTextBrowser->hide(); + } + else + { + d->StudyDescriptionTextBrowser->setText(description); + d->StudyDescriptionTextBrowser->show(); + } +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStudyItemWidget::description() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->StudyDescriptionTextBrowser->toPlainText(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setCollapsed(bool collapsed) +{ + Q_D(ctkDICOMStudyItemWidget); + d->StudyItemCollapsibleGroupBox->setCollapsed(collapsed); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMStudyItemWidget::collapsed() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->StudyItemCollapsibleGroupBox->collapsed(); +} + +//------------------------------------------------------------------------------ +int ctkDICOMStudyItemWidget::numberOfSeriesPerRow() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->SeriesListTableWidget->columnCount(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::setThumbnailSize(const ctkDICOMStudyItemWidget::ThumbnailSizeOption& thumbnailSize) +{ + Q_D(ctkDICOMStudyItemWidget); + d->ThumbnailSize = thumbnailSize; + d->ThumbnailSizePixel = d->calculateThumbnailSizeInPixel(d->ThumbnailSize); + d->SeriesListTableWidget->setColumnCount(d->calculateNumerOfSeriesPerRow()); + d->updateColumnsWidths(); +} + +//------------------------------------------------------------------------------ +ctkDICOMStudyItemWidget::ThumbnailSizeOption ctkDICOMStudyItemWidget::thumbnailSize() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->ThumbnailSize; +} + +//------------------------------------------------------------------------------ +int ctkDICOMStudyItemWidget::thumbnailSizePixel() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->ThumbnailSizePixel; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::setSelection(bool selected) +{ + Q_D(const ctkDICOMStudyItemWidget); + if (selected) + { + d->SeriesListTableWidget->selectAll(); + } + else + { + d->SeriesListTableWidget->clearSelection(); + } + + d->StudySelectionCheckBox->setChecked(selected); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMStudyItemWidget::selection() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->StudySelectionCheckBox->isChecked(); +} + +//---------------------------------------------------------------------------- +ctkDICOMScheduler* ctkDICOMStudyItemWidget::scheduler() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->Scheduler.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMStudyItemWidget::schedulerShared() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->Scheduler; +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setScheduler(ctkDICOMScheduler& scheduler) +{ + Q_D(ctkDICOMStudyItemWidget); + if (d->Scheduler) + { + QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + } + + d->Scheduler = QSharedPointer(&scheduler, skipDelete); + + if (d->Scheduler) + { + QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setScheduler(QSharedPointer scheduler) +{ + Q_D(ctkDICOMStudyItemWidget); + if (d->Scheduler) + { + QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + } + + d->Scheduler = scheduler; + + if (d->Scheduler) + { + QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + this, SLOT(updateGUIFromScheduler(QVariant))); + } +} + +//---------------------------------------------------------------------------- +ctkDICOMDatabase* ctkDICOMStudyItemWidget::dicomDatabase() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->DicomDatabase.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMStudyItemWidget::dicomDatabaseShared() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->DicomDatabase; +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setDicomDatabase(ctkDICOMDatabase& dicomDatabase) +{ + Q_D(ctkDICOMStudyItemWidget); + d->DicomDatabase = QSharedPointer(&dicomDatabase, skipDelete); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setDicomDatabase(QSharedPointer dicomDatabase) +{ + Q_D(ctkDICOMStudyItemWidget); + d->DicomDatabase = dicomDatabase; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::setFilteringSeriesDescription(const QString& filteringSeriesDescription) +{ + Q_D(ctkDICOMStudyItemWidget); + d->FilteringSeriesDescription = filteringSeriesDescription; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStudyItemWidget::filteringSeriesDescription() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->FilteringSeriesDescription; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::setFilteringModalities(const QStringList& filteringModalities) +{ + Q_D(ctkDICOMStudyItemWidget); + d->FilteringModalities = filteringModalities; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMStudyItemWidget::filteringModalities() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->FilteringModalities; +} + +//------------------------------------------------------------------------------ +QTableWidget* ctkDICOMStudyItemWidget::seriesListTableWidget() +{ + Q_D(ctkDICOMStudyItemWidget); + return d->SeriesListTableWidget; +} + +//------------------------------------------------------------------------------ +QList ctkDICOMStudyItemWidget::seriesItemWidgetsList() const +{ + Q_D(const ctkDICOMStudyItemWidget); + QList seriesItemWidgetsList; + + for (int row = 0; row < d->SeriesListTableWidget->rowCount(); row++) + { + for (int column = 0; column < d->SeriesListTableWidget->columnCount(); column++) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(d->SeriesListTableWidget->cellWidget(row, column)); + if (!seriesItemWidget) + { + continue; + } + + seriesItemWidgetsList.append(seriesItemWidget); + } + } + + return seriesItemWidgetsList; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::addSeriesItemWidget(int tableIndex, + const QString& seriesItem, + const QString& seriesInstanceUID, + const QString& modality, + const QString& seriesDescription) +{ + Q_D(ctkDICOMStudyItemWidget); + if (!d->DicomDatabase) + { + logger.error("addSeriesItemWidget failed, no DICOM Database has been set. \n"); + return; + } + + QString seriesNumber = d->DicomDatabase->fieldForSeries("SeriesNumber", seriesItem); + ctkDICOMSeriesItemWidget* seriesItemWidget = new ctkDICOMSeriesItemWidget; + seriesItemWidget->setSeriesItem(seriesItem); + seriesItemWidget->setPatientID(d->PatientID); + seriesItemWidget->setStudyInstanceUID(d->StudyInstanceUID); + seriesItemWidget->setSeriesInstanceUID(seriesInstanceUID); + seriesItemWidget->setSeriesNumber(seriesNumber); + seriesItemWidget->setModality(modality); + seriesItemWidget->setSeriesDescription(seriesDescription); + seriesItemWidget->setThumbnailSizePixel(d->ThumbnailSizePixel); + seriesItemWidget->setDicomDatabase(d->DicomDatabase); + seriesItemWidget->setScheduler(d->Scheduler); + seriesItemWidget->generateInstances(); + seriesItemWidget->setContextMenuPolicy(Qt::CustomContextMenu); + + this->connect(seriesItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + d->VisualDICOMBrowser.data(), SLOT(showSeriesContextMenu(const QPoint&))); + + QTableWidgetItem* tableItem = new QTableWidgetItem; + tableItem->setSizeHint(QSize(d->ThumbnailSizePixel, d->ThumbnailSizePixel)); + + int rowIndex = floor(tableIndex / d->SeriesListTableWidget->columnCount()); + int columnIndex = tableIndex % d->SeriesListTableWidget->columnCount(); + if (columnIndex == 0) + { + d->SeriesListTableWidget->insertRow(rowIndex); + d->SeriesListTableWidget->setRowHeight(rowIndex, d->ThumbnailSizePixel + 30); + } + + d->SeriesListTableWidget->setItem(rowIndex, columnIndex, tableItem); + d->SeriesListTableWidget->setCellWidget(rowIndex, columnIndex, seriesItemWidget); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::removeSeriesItemWidget(const QString& seriesItem) +{ + Q_D(ctkDICOMStudyItemWidget); + + for (int row = 0; row < d->SeriesListTableWidget->rowCount(); row++) + { + for (int column = 0; column < d->SeriesListTableWidget->columnCount(); column++) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(d->SeriesListTableWidget->cellWidget(row, column)); + if (!seriesItemWidget || seriesItemWidget->seriesItem() != seriesItem) + { + continue; + } + + d->SeriesListTableWidget->removeCellWidget(row, column); + this->disconnect(seriesItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + d->VisualDICOMBrowser.data(), SLOT(showSeriesContextMenu(const QPoint&))); + delete seriesItemWidget; + QTableWidgetItem* tableItem = d->SeriesListTableWidget->item(row, column); + delete tableItem; + + d->addEmptySeriesItemWidget(row, column); + break; + } + } +} + +//------------------------------------------------------------------------------ +ctkCollapsibleGroupBox* ctkDICOMStudyItemWidget::collapsibleGroupBox() +{ + Q_D(ctkDICOMStudyItemWidget); + return d->StudyItemCollapsibleGroupBox; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::generateSeries(bool toggled) +{ + Q_D(ctkDICOMStudyItemWidget); + if (!toggled) + { + return; + } + + d->createSeries(); + + if (d->Scheduler && d->Scheduler->getNumberOfQueryRetrieveServers() > 0) + { + d->Scheduler->querySeries(d->PatientID, + d->StudyInstanceUID, + QThread::NormalPriority); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::updateGUIFromScheduler(const QVariant& data) +{ + Q_D(ctkDICOMStudyItemWidget); + + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + d->createSeries(); + } + + if (td.JobUID.isEmpty() || + td.JobType != ctkDICOMJobResponseSet::JobType::QuerySeries || + td.PatientID != d->PatientID || + td.StudyInstanceUID != d->StudyInstanceUID) + { + return; + } + + d->createSeries(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::onStudySelectionClicked(bool toggled) +{ + this->setSelection(toggled); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h new file mode 100644 index 0000000000..135913021b --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h @@ -0,0 +1,194 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMStudyItemWidget_h +#define __ctkDICOMStudyItemWidget_h + +#include "ctkDICOMWidgetsExport.h" + +// Qt includes +#include +#include +class QTableWidget; + +// ctkWidgets includes +class ctkCollapsibleGroupBox; + +// ctkDICOMCore includes +class ctkDICOMDatabase; +class ctkDICOMScheduler; + +// ctkDICOMWidgets includes +#include "ctkDICOMSeriesItemWidget.h" +class ctkDICOMSeriesItemWidget; +class ctkDICOMStudyItemWidgetPrivate; + +/// \ingroup DICOM_Widgets +class CTK_DICOM_WIDGETS_EXPORT ctkDICOMStudyItemWidget : public QWidget +{ + Q_OBJECT; + Q_ENUMS(ThumbnailSizeOption) + Q_PROPERTY(QString studyItem READ studyItem WRITE setStudyItem); + Q_PROPERTY(QString patientID READ patientID WRITE setPatientID); + Q_PROPERTY(QString studyInstanceUID READ studyInstanceUID WRITE setStudyInstanceUID); + Q_PROPERTY(QString title READ title WRITE setTitle); + Q_PROPERTY(QString description READ description WRITE setDescription); + Q_PROPERTY(bool collapsed READ collapsed WRITE setCollapsed); + Q_PROPERTY(int numberOfSeriesPerRow READ numberOfSeriesPerRow); + Q_PROPERTY(ThumbnailSizeOption thumbnailSize READ thumbnailSize WRITE setThumbnailSize); + Q_PROPERTY(int thumbnailSizePixel READ thumbnailSizePixel); + +public: + typedef QWidget Superclass; + explicit ctkDICOMStudyItemWidget(QWidget* parent = nullptr); + virtual ~ctkDICOMStudyItemWidget(); + + ///@{ + /// Study item + void setStudyItem(const QString& studyItem); + QString studyItem() const; + ///@} + + ///@{ + /// Patient ID + void setPatientID(const QString& patientID); + QString patientID() const; + ///@} + + ///@{ + /// Study instance UID + void setStudyInstanceUID(const QString& studyInstanceUID); + QString studyInstanceUID() const; + ///@} + + ///@{ + /// Study title + void setTitle(const QString& title); + QString title() const; + ///@} + + ///@{ + /// Study Description + void setDescription(const QString& description); + QString description() const; + ///@} + + ///@{ + /// Study GroupBox collapsed + /// False by default + void setCollapsed(bool collapsed); + bool collapsed() const; + ///@} + + /// Number of series displayed per row + int numberOfSeriesPerRow() const; + + enum ThumbnailSizeOption + { + Small = 0, + Medium, + Large, + }; + + ///@{ + /// Set the thumbnail size: small, medium, large + /// medium by default + void setThumbnailSize(const ThumbnailSizeOption& thumbnailSize); + ThumbnailSizeOption thumbnailSize() const; + ///@} + + /// Thumbnail size in pixel + int thumbnailSizePixel() const; + + ///@{ + /// Study is selected + void setSelection(bool selected); + bool selection() const; + ///@} + + ///@{ + /// Query Filters + /// Empty by default + void setFilteringSeriesDescription(const QString& filteringSeriesDescription); + QString filteringSeriesDescription() const; + ///@} + + ///@{ + /// ["Any", "CR", "CT", "MR", "NM", "US", "PT", "XA"] by default + void setFilteringModalities(const QStringList& filteringModalities); + QStringList filteringModalities() const; + ///@} + + /// Return the scheduler. + Q_INVOKABLE ctkDICOMScheduler* scheduler() const; + /// Return the scheduler as a shared pointer + /// (not Python-wrappable). + QSharedPointer schedulerShared() const; + /// Set the scheduler. + Q_INVOKABLE void setScheduler(ctkDICOMScheduler& scheduler); + /// Set the scheduler as a shared pointer + /// (not Python-wrappable). + void setScheduler(QSharedPointer scheduler); + + /// Return the Dicom Database. + Q_INVOKABLE ctkDICOMDatabase* dicomDatabase() const; + /// Return Dicom Database as a shared pointer + /// (not Python-wrappable). + QSharedPointer dicomDatabaseShared() const; + /// Set the Dicom Database. + Q_INVOKABLE void setDicomDatabase(ctkDICOMDatabase& dicomDatabase); + /// Set the Dicom Database as a shared pointer + /// (not Python-wrappable). + void setDicomDatabase(QSharedPointer dicomDatabase); + + /// Series list table. + Q_INVOKABLE QTableWidget* seriesListTableWidget(); + + /// Return all the series item widgets for the study + Q_INVOKABLE QList seriesItemWidgetsList() const; + + /// Add/Remove Series item widget + Q_INVOKABLE void addSeriesItemWidget(int tableIndex, + const QString& seriesItem, + const QString& seriesInstanceUID, + const QString& modality, + const QString& seriesDescription); + Q_INVOKABLE void removeSeriesItemWidget(const QString& seriesItem); + + /// Collapsible group box. + Q_INVOKABLE ctkCollapsibleGroupBox* collapsibleGroupBox(); + +public Q_SLOTS: + void generateSeries(bool toggled = true); + void updateGUIFromScheduler(const QVariant& data); + void onStudySelectionClicked(bool); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMStudyItemWidget); + Q_DISABLE_COPY(ctkDICOMStudyItemWidget); +}; + +#endif diff --git a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp index 861c995915..bc4e7ad52a 100644 --- a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp @@ -131,7 +131,7 @@ bool ctkDICOMThumbnailGenerator::generateThumbnail(DicomImage *dcmImage, QImage& EI_Status result = dcmImage->getStatus(); if (result != EIS_Normal) { - qCritical() << Q_FUNC_INFO << QString("Rendering of DICOM image failed for thumbnail failed: ") + DicomImage::getString(result); + logger.warn(QString("Rendering of DICOM image failed for thumbnail failed: ") + DicomImage::getString(result)); return false; } // Select first window defined in image. If none, compute min/max window as best guess. @@ -214,12 +214,12 @@ bool ctkDICOMThumbnailGenerator::generateThumbnail(const QString dcmImagePath, c } //------------------------------------------------------------------------------ -void ctkDICOMThumbnailGenerator::generateBlankThumbnail(QImage& image) +void ctkDICOMThumbnailGenerator::generateBlankThumbnail(QImage& image, QColor color) { Q_D(ctkDICOMThumbnailGenerator); if (image.width() != d->Width || image.height() != d->Height) { image = QImage(d->Width, d->Height, QImage::Format_RGB32); } - image.fill(Qt::darkGray); + image.fill(color); } diff --git a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h index 292cdfddd1..eb8db9a9e8 100644 --- a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h +++ b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h @@ -23,19 +23,20 @@ #define __ctkDICOMThumbnailGenerator_h // Qt includes +#include class QImage; -// CTK includes -#include "ctkDICOMWidgetsExport.h" +// ctkDICOMWidgets includes #include "ctkDICOMAbstractThumbnailGenerator.h" - +#include "ctkDICOMWidgetsExport.h" class ctkDICOMThumbnailGeneratorPrivate; + +// DCMTK includes class DicomImage; /// \ingroup DICOM_Widgets /// -/// \brief thumbnail generator class -/// +/// \brief Thumbnail generator class class CTK_DICOM_WIDGETS_EXPORT ctkDICOMThumbnailGenerator : public ctkDICOMAbstractThumbnailGenerator { Q_OBJECT @@ -56,7 +57,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMThumbnailGenerator : public ctkDICOMAbstr /// Generate a blank thumbnail image (currently a solid gray box of the requested thumbnail size). /// It can be used as a placeholder for invalid images or duringan image is loaded. - Q_INVOKABLE void generateBlankThumbnail(QImage& image); + Q_INVOKABLE void generateBlankThumbnail(QImage& image, QColor color = Qt::darkGray); /// Set thumbnail width void setWidth(int width); diff --git a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp new file mode 100644 index 0000000000..c2533c97ac --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp @@ -0,0 +1,3310 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +// Qt includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// CTK includes +#include +#include +#include +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMDatabase.h" +#include "ctkDICOMIndexer.h" +#include "ctkDICOMScheduler.h" +#include "ctkDICOMServer.h" +#include "ctkDICOMJobResponseSet.h" +#include "ctkUtils.h" + +// ctkDICOMWidgets includes +#include "ctkDICOMObjectListWidget.h" +#include "ctkDICOMVisualBrowserWidget.h" +#include "ctkDICOMSeriesItemWidget.h" +#include "ctkDICOMServerNodeWidget2.h" +#include "ctkDICOMVisualBrowserWidget.h" +#include "ui_ctkDICOMVisualBrowserWidget.h" + +static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMVisualBrowserWidget"); + +class ctkDICOMMetadataDialog : public QDialog +{ +public: + ctkDICOMMetadataDialog(QWidget* parent = 0) + : QDialog(parent) + { + this->setWindowFlags(Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint | Qt::Window); + this->setModal(true); + this->setSizeGripEnabled(true); + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setMargin(0); + this->tagListWidget = new ctkDICOMObjectListWidget(); + layout->addWidget(this->tagListWidget); + } + + virtual ~ctkDICOMMetadataDialog() + { + } + + void setFileList(const QStringList& fileList) + { + this->tagListWidget->setFileList(fileList); + } + + void closeEvent(QCloseEvent* evt) + { + // just hide the window when close button is clicked + evt->ignore(); + this->hide(); + } + + void showEvent(QShowEvent* event) + { + QDialog::showEvent(event); + // QDialog would reset window position and size when shown. + // Restore its previous size instead (user may look at metadata + // of different series one after the other and would be inconvenient to + // set the desired size manually each time). + if (!this->savedGeometry.isEmpty()) + { + this->restoreGeometry(this->savedGeometry); + if (this->isMaximized()) + { + this->setGeometry(QApplication::desktop()->availableGeometry(this)); + } + } + } + + void hideEvent(QHideEvent* event) + { + this->savedGeometry = this->saveGeometry(); + QDialog::hideEvent(event); + } + +protected: + ctkDICOMObjectListWidget* tagListWidget; + QByteArray savedGeometry; +}; + +//---------------------------------------------------------------------------- +class ctkDICOMVisualBrowserWidgetPrivate : public Ui_ctkDICOMVisualBrowserWidget +{ + Q_DECLARE_PUBLIC(ctkDICOMVisualBrowserWidget); + +protected: + ctkDICOMVisualBrowserWidget* const q_ptr; + QToolButton* patientsTabMenuToolButton; + +public: + ctkDICOMVisualBrowserWidgetPrivate(ctkDICOMVisualBrowserWidget& obj); + ~ctkDICOMVisualBrowserWidgetPrivate(); + + void init(); + void disconnectScheduler(); + void connectScheduler(); + void importDirectory(QString directory, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); + void importFiles(const QStringList& files, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); + void importOldSettings(); + void showUpdateSchemaDialog(); + void updateModalityCheckableComboBox(); + void createPatients(); + void updateFiltersWarnings(); + void setBackgroundColorToFilterWidgets(bool warning = false); + void setBackgroundColorToWidget(QColor color, QWidget* widget); + void retrieveSeries(); + bool updateServer(ctkDICOMServer* server); + void removeAllPatientItemWidgets(); + bool isPatientTabAlreadyAdded(const QString& patientItem); + void updateSeriesTablesSelection(ctkDICOMSeriesItemWidget* selectedSeriesItemWidget); + QStringList getPatientUIDsFromWidgets(ctkDICOMModel::IndexType level, + QList selectedWidgets); + QStringList getStudyUIDsFromWidgets(ctkDICOMModel::IndexType level, + QList selectedWidgets); + QStringList getSeriesUIDsFromWidgets(ctkDICOMModel::IndexType level, + QList selectedWidgets); + QStringList filterPatientList(const QStringList& patientList, + const QMap& filters); + QStringList filterStudyList(const QStringList& patientList, + const QMap& filters); + QStringList filterSeriesList(const QStringList& patientList, + const QMap& filters); + ctkDICOMStudyItemWidget* getCurrentPatientStudyWidgetByUIDs(const QString& studyInstanceUID); + ctkDICOMSeriesItemWidget* getCurrentPatientSeriesWidgetByUIDs(const QString& studyInstanceUID, + const QString& seriesInstanceUID); + + // Return a sanitized version of the string that is safe to be used + // as a filename component. + // All non-ASCII characters are replaced, because they may be used on an internal hard disk, + // but it may not be possible to use them on file systems of an external drive or network storage. + QString filenameSafeString(const QString& str) + { + QString safeStr; + const QString illegalChars("/\\<>:\"|?*"); + foreach (const QChar& c, str) + { + int asciiCode = c.toLatin1(); + if (asciiCode >= 32 && asciiCode <= 127 && !illegalChars.contains(c)) + { + safeStr.append(c); + } + else + { + safeStr.append("_"); + } + } + // remove leading/trailing whitespaces + return safeStr.trimmed(); + } + + // local count variables to keep track of the number of items + // added to the database during an import operation + int PatientsAddedDuringImport; + int StudiesAddedDuringImport; + int SeriesAddedDuringImport; + int InstancesAddedDuringImport; + ctkFileDialog* ImportDialog; + + QSharedPointer MetadataDialog; + + // Settings key that stores database directory + QString DatabaseDirectorySettingsKey; + + // If database directory is specified with relative path then this directory will be used as a base + QString DatabaseDirectoryBase; + + // Default database path to use if there is nothing in settings + QString DefaultDatabaseDirectory; + QString DatabaseDirectory; + + QSharedPointer DicomDatabase; + QSharedPointer Scheduler; + QSharedPointer Indexer; + + QString FilteringPatientID; + QString FilteringPatientName; + + QString FilteringStudyDescription; + ctkDICOMPatientItemWidget::DateType FilteringDate; + + QString FilteringSeriesDescription; + QStringList PreviousFilteringModalities; + QStringList FilteringModalities; + + int NumberOfStudiesPerPatient; + ctkDICOMStudyItemWidget::ThumbnailSizeOption ThumbnailSize; + bool SendActionVisible; + bool DeleteActionVisible; + bool IsGUIUpdating; + bool IsLoading; + + ctkDICOMServerNodeWidget2* ServerNodeWidget; + QProgressDialog* UpdateSchemaProgress; + QProgressDialog* ExportProgress; +}; + +CTK_GET_CPP(ctkDICOMVisualBrowserWidget, QString, databaseDirectoryBase, DatabaseDirectoryBase); +CTK_SET_CPP(ctkDICOMVisualBrowserWidget, const QString&, setDatabaseDirectoryBase, DatabaseDirectoryBase); + +//---------------------------------------------------------------------------- +// ctkDICOMVisualBrowserWidgetPrivate methods + +//---------------------------------------------------------------------------- +ctkDICOMVisualBrowserWidgetPrivate::ctkDICOMVisualBrowserWidgetPrivate(ctkDICOMVisualBrowserWidget& obj) + : q_ptr(&obj) +{ + this->DicomDatabase = QSharedPointer(new ctkDICOMDatabase); + + this->Scheduler = QSharedPointer(new ctkDICOMScheduler); + this->Scheduler->setDicomDatabase(this->DicomDatabase); + + this->Indexer = QSharedPointer(new ctkDICOMIndexer); + this->Indexer->setDatabase(this->DicomDatabase.data()); + + this->MetadataDialog = QSharedPointer(new ctkDICOMMetadataDialog()); + this->MetadataDialog->setObjectName("DICOMMetadata"); + this->MetadataDialog->setWindowTitle(ctkDICOMVisualBrowserWidget::tr("DICOM File Metadata")); + + this->DatabaseDirectorySettingsKey = ""; + this->DatabaseDirectoryBase = ""; + this->DefaultDatabaseDirectory = ""; + this->DatabaseDirectory = ""; + + this->NumberOfStudiesPerPatient = 2; + this->ThumbnailSize = ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium; + this->SendActionVisible = false; + this->DeleteActionVisible = true; + + this->FilteringPatientID = ""; + this->FilteringPatientName = ""; + this->FilteringStudyDescription = ""; + this->FilteringSeriesDescription = ""; + this->FilteringDate = ctkDICOMPatientItemWidget::DateType::Any; + + this->FilteringModalities.append("Any"); + this->FilteringModalities.append("CR"); + this->FilteringModalities.append("CT"); + this->FilteringModalities.append("MR"); + this->FilteringModalities.append("NM"); + this->FilteringModalities.append("US"); + this->FilteringModalities.append("PT"); + this->FilteringModalities.append("XA"); + + this->PatientsAddedDuringImport = 0; + this->StudiesAddedDuringImport = 0; + this->SeriesAddedDuringImport = 0; + this->InstancesAddedDuringImport = 0; + this->ImportDialog = nullptr; + + this->IsGUIUpdating = false; + this->IsLoading = false; + + this->ServerNodeWidget = new ctkDICOMServerNodeWidget2(); + this->ServerNodeWidget->setScheduler(this->Scheduler); + this->connectScheduler(); + + this->ExportProgress = nullptr; + this->UpdateSchemaProgress = nullptr; +} + +//---------------------------------------------------------------------------- +ctkDICOMVisualBrowserWidgetPrivate::~ctkDICOMVisualBrowserWidgetPrivate() +{ + this->removeAllPatientItemWidgets(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::init() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + this->setupUi(q); + + this->DatabaseDirectoryProblemFrame->hide(); + QObject::connect(this->SelectDatabaseDirectoryButton, SIGNAL(clicked()), + q, SLOT(selectDatabaseDirectory())); + QObject::connect(this->CreateNewDatabaseButton, SIGNAL(clicked()), + q, SLOT(createNewDatabaseDirectory())); + QObject::connect(this->UpdateDatabaseButton, SIGNAL(clicked()), + q, SLOT(updateDatabase())); + + this->WarningPushButton->hide(); + QObject::connect(this->FilteringPatientIDSearchBox, SIGNAL(textChanged(QString)), + q, SLOT(onFilteringPatientIDChanged())); + + QObject::connect(this->FilteringPatientNameSearchBox, SIGNAL(textChanged(QString)), + q, SLOT(onFilteringPatientNameChanged())); + + QObject::connect(this->FilteringStudyDescriptionSearchBox, SIGNAL(textChanged(QString)), + q, SLOT(onFilteringStudyDescriptionChanged())); + + QObject::connect(this->FilteringSeriesDescriptionSearchBox, SIGNAL(textChanged(QString)), + q, SLOT(onFilteringSeriesDescriptionChanged())); + + QObject::connect(this->FilteringModalityCheckableComboBox, SIGNAL(checkedIndexesChanged()), + q, SLOT(onFilteringModalityCheckableComboBoxChanged())); + this->updateModalityCheckableComboBox(); + + QObject::connect(this->FilteringDateComboBox, SIGNAL(currentIndexChanged(int)), + q, SLOT(onFilteringDateComboBoxChanged(int))); + + QObject::connect(this->QueryPatientPushButton, SIGNAL(clicked()), + q, SLOT(onQueryPatients())); + + this->ServersSettingsCollapsibleGroupBox->layout()->addWidget(this->ServerNodeWidget); + this->PatientsTabWidget->clear(); + + // setup patients menu + this->patientsTabMenuToolButton = new QToolButton(q); + this->patientsTabMenuToolButton->setObjectName("patientsTabMenuToolButton"); + this->patientsTabMenuToolButton->setCheckable(false); + this->patientsTabMenuToolButton->setChecked(false); + this->patientsTabMenuToolButton->setIcon(QIcon(":/Icons/more_vert.svg")); + this->patientsTabMenuToolButton->hide(); + + QObject::connect(this->patientsTabMenuToolButton, SIGNAL(clicked()), + q, SLOT(onPatientsTabMenuToolButtonClicked())); + + this->PatientsTabWidget->setCornerWidget(this->patientsTabMenuToolButton, Qt::TopLeftCorner); + + QObject::connect(this->PatientsTabWidget, SIGNAL(currentChanged(int)), + q, SLOT(onPatientItemChanged(int))); + + QObject::connect(this->ClosePushButton, SIGNAL(clicked()), + q, SLOT(onClose())); + + QObject::connect(this->ImportPushButton, SIGNAL(clicked()), + q, SLOT(onImport())); + + // Initialize directoryMode widget + QFormLayout* layout = new QFormLayout; + QComboBox* importDirectoryModeComboBox = new QComboBox(); + importDirectoryModeComboBox->addItem(ctkDICOMVisualBrowserWidget::tr("Add Link"), static_cast(ctkDICOMVisualBrowserWidget::ImportDirectoryAddLink)); + importDirectoryModeComboBox->addItem(ctkDICOMVisualBrowserWidget::tr("Copy"), static_cast(ctkDICOMVisualBrowserWidget::ImportDirectoryCopy)); + importDirectoryModeComboBox->setToolTip( + ctkDICOMVisualBrowserWidget::tr("Indicate if the files should be copied to the local database" + " directory or if only links should be created ?")); + layout->addRow(new QLabel(ctkDICOMVisualBrowserWidget::tr("Import Directory Mode:")), importDirectoryModeComboBox); + layout->setContentsMargins(0, 0, 0, 0); + QWidget* importDirectoryBottomWidget = new QWidget(); + importDirectoryBottomWidget->setLayout(layout); + + // Default values + importDirectoryModeComboBox->setCurrentIndex( + importDirectoryModeComboBox->findData(static_cast(q->importDirectoryMode()))); + + // Initialize import widget + this->ImportDialog = new ctkFileDialog(); + this->ImportDialog->setBottomWidget(importDirectoryBottomWidget); + this->ImportDialog->setFileMode(QFileDialog::Directory); + // XXX Method setSelectionMode must be called after setFileMode + this->ImportDialog->setSelectionMode(QAbstractItemView::ExtendedSelection); + this->ImportDialog->setLabelText(QFileDialog::Accept, ctkDICOMVisualBrowserWidget::tr("Import")); + this->ImportDialog->setWindowTitle(ctkDICOMVisualBrowserWidget::tr("Import DICOM files from directory ...")); + this->ImportDialog->setWindowModality(Qt::ApplicationModal); + + q->connect(this->ImportDialog, SIGNAL(filesSelected(QStringList)), + q, SLOT(onImportDirectoriesSelected(QStringList))); + + q->connect(importDirectoryModeComboBox, SIGNAL(currentIndexChanged(int)), + q, SLOT(onImportDirectoryComboBoxCurrentIndexChanged(int))); + + this->ProgressFrame->hide(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::disconnectScheduler() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (!this->Scheduler) + { + return; + } + + ctkDICOMVisualBrowserWidget::disconnect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + q, SLOT(updateGUIFromScheduler(QVariant))); + ctkDICOMVisualBrowserWidget::disconnect(this->Scheduler.data(), SIGNAL(taskFailed(QString, QString)), + q, SLOT(onTaskFailed(QString, QString))); + ctkDICOMVisualBrowserWidget::disconnect(this->Indexer.data(), SIGNAL(progress(int)), q, SLOT(onIndexingProgress(int))); + ctkDICOMVisualBrowserWidget::disconnect(this->Indexer.data(), SIGNAL(progressStep(QString)), q, SLOT(onIndexingProgressStep(QString))); + ctkDICOMVisualBrowserWidget::disconnect(this->Indexer.data(), SIGNAL(progressDetail(QString)), q, SLOT(onIndexingProgressDetail(QString))); + ctkDICOMVisualBrowserWidget::disconnect(this->Indexer.data(), SIGNAL(indexingComplete(int, int, int, int)), q, SLOT(onIndexingComplete(int, int, int, int))); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::connectScheduler() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (!this->Scheduler) + { + return; + } + + ctkDICOMVisualBrowserWidget::connect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), + q, SLOT(updateGUIFromScheduler(QVariant))); + ctkDICOMVisualBrowserWidget::connect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), + q, SLOT(onTaskFailed(QVariant))); + ctkDICOMVisualBrowserWidget::connect(this->Indexer.data(), SIGNAL(progress(int)), q, SLOT(onIndexingProgress(int))); + ctkDICOMVisualBrowserWidget::connect(this->Indexer.data(), SIGNAL(progressStep(QString)), q, SLOT(onIndexingProgressStep(QString))); + ctkDICOMVisualBrowserWidget::connect(this->Indexer.data(), SIGNAL(progressDetail(QString)), q, SLOT(onIndexingProgressDetail(QString))); + ctkDICOMVisualBrowserWidget::connect(this->Indexer.data(), SIGNAL(indexingComplete(int, int, int, int)), q, SLOT(onIndexingComplete(int, int, int, int))); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::importDirectory(QString directory, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode) +{ + if (!this->DicomDatabase) + { + logger.error("importDirectory failed, no DICOM Database has been set. \n"); + return; + } + + if (!this->Scheduler || !this->Indexer) + { + logger.error("importDirectory failed, no task pool has been set. \n"); + return; + } + + if (!QDir(directory).exists()) + { + logger.error(QString("importDirectory failed, input directory %1 does not exist. \n").arg(directory)); + return; + } + // Start background indexing + this->Indexer->addDirectory(directory, mode == ctkDICOMVisualBrowserWidget::ImportDirectoryCopy); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::importFiles(const QStringList& files, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode) +{ + if (!this->DicomDatabase) + { + logger.error("importFiles failed, no DICOM Database has been set. \n"); + return; + } + + if (!this->Scheduler || !this->Indexer) + { + logger.error("importFiles failed, no task pool has been set. \n"); + return; + } + + // Start background indexing + this->Indexer->addListOfFiles(files, mode == ctkDICOMVisualBrowserWidget::ImportDirectoryCopy); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::importOldSettings() +{ + // Backward compatibility + QSettings settings; + int dontConfirmCopyOnImport = settings.value("MainWindow/DontConfirmCopyOnImport", static_cast(QMessageBox::InvalidRole)).toInt(); + if (dontConfirmCopyOnImport == QMessageBox::AcceptRole) + { + settings.setValue("DICOM/ImportDirectoryMode", static_cast(ctkDICOMVisualBrowserWidget::ImportDirectoryCopy)); + } + settings.remove("MainWindow/DontConfirmCopyOnImport"); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::showUpdateSchemaDialog() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (this->UpdateSchemaProgress == 0) + { + // + // Set up the Update Schema Progress Dialog + // + this->UpdateSchemaProgress = new QProgressDialog( + ctkDICOMVisualBrowserWidget::tr("DICOM Schema Update"), ctkDICOMVisualBrowserWidget::tr("Cancel"), 0, 100, q, Qt::WindowTitleHint | Qt::WindowSystemMenuHint); + + // We don't want the progress dialog to resize itself, so we bypass the label by creating our own + QLabel* progressLabel = new QLabel(ctkDICOMVisualBrowserWidget::tr("Initialization...")); + this->UpdateSchemaProgress->setLabel(progressLabel); + this->UpdateSchemaProgress->setWindowModality(Qt::ApplicationModal); + this->UpdateSchemaProgress->setMinimumDuration(0); + this->UpdateSchemaProgress->setValue(0); + + q->connect(DicomDatabase.data(), SIGNAL(schemaUpdateStarted(int)), + this->UpdateSchemaProgress, SLOT(setMaximum(int))); + q->connect(DicomDatabase.data(), SIGNAL(schemaUpdateProgress(int)), + this->UpdateSchemaProgress, SLOT(setValue(int))); + q->connect(DicomDatabase.data(), SIGNAL(schemaUpdateProgress(QString)), + progressLabel, SLOT(setText(QString))); + + // close the dialog + q->connect(this->DicomDatabase.data(), SIGNAL(schemaUpdated()), + this->UpdateSchemaProgress, SLOT(close())); + } + this->UpdateSchemaProgress->show(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::updateModalityCheckableComboBox() +{ + QAbstractItemModel* model = this->FilteringModalityCheckableComboBox->checkableModel(); + int wasBlocking = this->FilteringModalityCheckableComboBox->blockSignals(true); + + if ((!this->PreviousFilteringModalities.contains("Any") && + this->FilteringModalities.contains("Any")) || + this->FilteringModalities.count() == 0) + { + this->FilteringModalities.clear(); + this->FilteringModalities.append("Any"); + this->FilteringModalities.append("CR"); + this->FilteringModalities.append("CT"); + this->FilteringModalities.append("MR"); + this->FilteringModalities.append("NM"); + this->FilteringModalities.append("US"); + this->FilteringModalities.append("PT"); + this->FilteringModalities.append("XA"); + + for (int i = 0; i < this->FilteringModalityCheckableComboBox->count(); ++i) + { + QModelIndex modelIndex = this->FilteringModalityCheckableComboBox->checkableModel()->index(i, 0); + this->FilteringModalityCheckableComboBox->setCheckState(modelIndex, Qt::CheckState::Checked); + } + this->FilteringModalityCheckableComboBox->blockSignals(wasBlocking); + return; + } + + for (int i = 0; i < this->FilteringModalityCheckableComboBox->count(); ++i) + { + QModelIndex modelIndex = this->FilteringModalityCheckableComboBox->checkableModel()->index(i, 0); + this->FilteringModalityCheckableComboBox->setCheckState(modelIndex, Qt::CheckState::Unchecked); + } + + foreach (QString modality, this->FilteringModalities) + { + QModelIndexList indexList = model->match(model->index(0, 0), 0, modality); + if (indexList.length() == 0) + { + continue; + } + + QModelIndex index = indexList[0]; + this->FilteringModalityCheckableComboBox->setCheckState(index, Qt::CheckState::Checked); + } + + if (this->FilteringModalityCheckableComboBox->allChecked()) + { + this->FilteringModalityCheckableComboBox->blockSignals(wasBlocking); + return; + } + + int anyCheckState = Qt::CheckState::Unchecked; + QModelIndex anyModelIndex = this->FilteringModalityCheckableComboBox->checkableModel()->index(0, 0); + for (int i = 1; i < this->FilteringModalityCheckableComboBox->count(); ++i) + { + QModelIndex modelIndex = this->FilteringModalityCheckableComboBox->checkableModel()->index(i, 0); + if (this->FilteringModalityCheckableComboBox->checkState(modelIndex) != Qt::CheckState::Checked) + { + anyCheckState = Qt::CheckState::PartiallyChecked; + break; + } + } + + if (anyCheckState == Qt::CheckState::PartiallyChecked) + { + this->FilteringModalityCheckableComboBox->setCheckState(anyModelIndex, Qt::CheckState::PartiallyChecked); + this->FilteringModalities.removeAll("Any"); + } + else + { + this->FilteringModalityCheckableComboBox->setCheckState(anyModelIndex, Qt::CheckState::Checked); + this->FilteringModalities.append("Any"); + } + + this->FilteringModalityCheckableComboBox->blockSignals(wasBlocking); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::createPatients() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (this->IsGUIUpdating) + { + return; + } + + if (!this->DicomDatabase) + { + logger.error("createPatients failed, no DICOM database has been set. \n"); + return; + } + + QStringList patientList = this->DicomDatabase->patients(); + if (patientList.count() == 0) + { + this->patientsTabMenuToolButton->hide(); + return; + } + + this->patientsTabMenuToolButton->show(); + + this->IsGUIUpdating = true; + + int wasBlocking = this->PatientsTabWidget->blockSignals(true); + foreach (QString patientItem, patientList) + { + QString patientID = this->DicomDatabase->fieldForPatient("PatientID", patientItem); + QString patientName = this->DicomDatabase->fieldForPatient("PatientsName", patientItem); + patientName.replace(R"(^)", R"( )"); + if (this->isPatientTabAlreadyAdded(patientItem)) + { + continue; + } + + // Filter with patientID and patientsName + if ((!this->FilteringPatientID.isEmpty() && !patientID.contains(this->FilteringPatientID, Qt::CaseInsensitive)) || + (!this->FilteringPatientName.isEmpty() && !patientName.contains(this->FilteringPatientName, Qt::CaseInsensitive))) + { + continue; + } + + q->addPatientItemWidget(patientItem); + } + + this->PatientsTabWidget->setCurrentIndex(0); + this->PatientsTabWidget->blockSignals(wasBlocking); + q->onPatientItemChanged(0); + + this->IsGUIUpdating = false; +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::updateFiltersWarnings() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (!this->DicomDatabase) + { + logger.error("updateFiltersWarnings failed, no DICOM database has been set. \n"); + return; + } + + // Loop over all the data in the dicom database and apply the filters. + // If there are no series, highlight which are the filters that produce no results + this->setBackgroundColorToFilterWidgets(); + + QColor visualDICOMBrowserColor = q->palette().color(QPalette::Normal, q->backgroundRole()); + QColor color = Qt::yellow; + if (visualDICOMBrowserColor.lightnessF() < 0.5) + { + color.setRgb(60, 164, 255); + } + + QStringList patientList = this->DicomDatabase->patients(); + if (patientList.count() == 0) + { + this->setBackgroundColorToWidget(color, this->FilteringPatientIDSearchBox); + this->setBackgroundColorToWidget(color, this->FilteringPatientNameSearchBox); + return; + } + + QMap filters; + filters.insert("PatientsName", this->FilteringPatientName); + filters.insert("PatientID", this->FilteringPatientID); + QStringList filteredPatientList = this->filterPatientList(patientList, filters); + if (filteredPatientList.count() == 0) + { + this->setBackgroundColorToWidget(color, this->FilteringPatientIDSearchBox); + this->setBackgroundColorToWidget(color, this->FilteringPatientNameSearchBox); + return; + } + + QStringList studiesList; + foreach (QString patientItem, filteredPatientList) + { + studiesList.append(this->DicomDatabase->studiesForPatient(patientItem)); + } + + filters.clear(); + filters.insert("StudyDate", QString::number(ctkDICOMPatientItemWidget::getNDaysFromFilteringDate(this->FilteringDate))); + filters.insert("StudyDescription", this->FilteringStudyDescription); + + QStringList filteredStudyList = this->filterStudyList(studiesList, filters); + if (filteredStudyList.count() == 0) + { + this->setBackgroundColorToWidget(color, this->FilteringDateComboBox); + this->setBackgroundColorToWidget(color, this->FilteringStudyDescriptionSearchBox); + return; + } + + QStringList seriesList; + foreach (QString studyItem, filteredStudyList) + { + QString studyInstanceUID = this->DicomDatabase->fieldForStudy("StudyInstanceUID", studyItem); + seriesList.append(this->DicomDatabase->seriesForStudy(studyInstanceUID)); + } + + filters.clear(); + filters.insert("Modality", this->FilteringModalities); + filters.insert("SeriesDescription", this->FilteringSeriesDescription); + + QStringList filteredSeriesList = this->filterSeriesList(seriesList, filters); + if (filteredSeriesList.count() == 0) + { + this->setBackgroundColorToWidget(color, this->FilteringSeriesDescriptionSearchBox); + this->setBackgroundColorToWidget(color, this->FilteringModalityCheckableComboBox); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::setBackgroundColorToFilterWidgets(bool warning) +{ + Q_Q(ctkDICOMVisualBrowserWidget); + QColor visualDICOMBrowserColor = q->palette().color(QPalette::Normal, q->backgroundRole()); + if (warning) + { + QColor color = Qt::yellow; + if (visualDICOMBrowserColor.lightnessF() < 0.5) + { + color.setRgb(60, 164, 255); + } + this->setBackgroundColorToWidget(color, this->FilteringPatientIDSearchBox); + this->setBackgroundColorToWidget(color, this->FilteringPatientNameSearchBox); + this->setBackgroundColorToWidget(color, this->FilteringDateComboBox); + this->setBackgroundColorToWidget(color, this->FilteringStudyDescriptionSearchBox); + this->setBackgroundColorToWidget(color, this->FilteringSeriesDescriptionSearchBox); + this->setBackgroundColorToWidget(color, this->FilteringModalityCheckableComboBox); + } + else + { + QColor colorSearchBox(255, 255, 255); + QColor colorButton(239, 239, 239); + if (visualDICOMBrowserColor.lightnessF() < 0.5) + { + colorSearchBox.setRgb(30, 30, 30); + colorButton.setRgb(50, 50, 50); + } + else if (visualDICOMBrowserColor.lightnessF() > 0.95) + { + colorSearchBox.setRgb(255, 255, 255); + colorButton.setRgb(255, 255, 255); + } + this->setBackgroundColorToWidget(colorSearchBox, this->FilteringPatientIDSearchBox); + this->setBackgroundColorToWidget(colorSearchBox, this->FilteringPatientNameSearchBox); + this->setBackgroundColorToWidget(colorButton, this->FilteringDateComboBox); + this->setBackgroundColorToWidget(colorSearchBox, this->FilteringStudyDescriptionSearchBox); + this->setBackgroundColorToWidget(colorSearchBox, this->FilteringSeriesDescriptionSearchBox); + this->setBackgroundColorToWidget(colorButton, this->FilteringModalityCheckableComboBox); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::setBackgroundColorToWidget(QColor color, + QWidget* widget) +{ + if (!widget) + { + return; + } + + QPalette pal = widget->palette(); + QComboBox* comboBox = qobject_cast(widget); + if (comboBox) + { + pal.setColor(QPalette::Button, color); + } + else + { + pal.setColor(widget->backgroundRole(), color); + } + widget->setPalette(pal); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::retrieveSeries() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (!this->Scheduler) + { + logger.error("retrieveSeries failed, no task pool has been set. \n"); + return; + } + + if (this->IsLoading) + { + return; + } + + ctkDICOMPatientItemWidget* currentPatientItemWidget = + qobject_cast(this->PatientsTabWidget->currentWidget()); + if (!currentPatientItemWidget) + { + return; + } + + this->IsLoading = true; + + QList seriesWidgetsList; + for (int patientIndex = 0; patientIndex < this->PatientsTabWidget->count(); ++patientIndex) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(this->PatientsTabWidget->widget(patientIndex)); + if (!patientItemWidget) + { + continue; + } + + QList studyItemWidgetsList = patientItemWidget->studyItemWidgetsList(); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) + { + QTableWidget* seriesListTableWidget = studyItemWidget->seriesListTableWidget(); + for (int row = 0; row < seriesListTableWidget->rowCount(); row++) + { + for (int column = 0; column < seriesListTableWidget->columnCount(); column++) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(seriesListTableWidget->cellWidget(row, column)); + if (!seriesItemWidget) + { + continue; + } + + seriesWidgetsList.append(seriesItemWidget); + } + } + } + } + + QList selectedSeriesWidgetsList; + QList studyItemWidgetsList = currentPatientItemWidget->studyItemWidgetsList(); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) + { + QTableWidget* seriesListTableWidget = studyItemWidget->seriesListTableWidget(); + QModelIndexList indexList = seriesListTableWidget->selectionModel()->selectedIndexes(); + foreach (QModelIndex index, indexList) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = qobject_cast + (seriesListTableWidget->cellWidget(index.row(), index.column())); + if (!seriesItemWidget) + { + continue; + } + + selectedSeriesWidgetsList.append(seriesItemWidget); + } + } + + if (selectedSeriesWidgetsList.count() == 0) + { + this->IsLoading = false; + return; + } + + QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); + + bool deleteActionWasVisible = this->DeleteActionVisible; + this->DeleteActionVisible = false; + + bool queryPatientButtonWasEnabled = this->QueryPatientPushButton->isEnabled(); + this->QueryPatientPushButton->setEnabled(false); + + QStringList seriesInstanceUIDsToStop; + QStringList selectedSeriesInstanceUIDs; + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, seriesWidgetsList) + { + if (!seriesItemWidget) + { + continue; + } + + if (!selectedSeriesWidgetsList.contains(seriesItemWidget)) + { + seriesItemWidget->setStopJobs(true); + seriesInstanceUIDsToStop.append(seriesItemWidget->seriesInstanceUID()); + } + else + { + selectedSeriesInstanceUIDs.append(seriesItemWidget->seriesInstanceUID()); + } + } + + this->Scheduler->stopJobsByUIDs({}, + {}, + seriesInstanceUIDsToStop); + + bool wait = true; + while (wait) + { + this->Scheduler->waitForDone(300); + wait = false; + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, selectedSeriesWidgetsList) + { + if (!seriesItemWidget) + { + continue; + } + + if (seriesItemWidget->isCloud() && !seriesItemWidget->retrieveFailed()) + { + wait = true; + break; + } + } + } + + this->updateFiltersWarnings(); + this->ProgressFrame->hide(); + this->QueryPatientPushButton->setIcon(QIcon(":/Icons/query.svg")); + + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, seriesWidgetsList) + { + if (!seriesItemWidget) + { + continue; + } + + seriesItemWidget->setStopJobs(false); + } + + q->emit seriesRetrieved(selectedSeriesInstanceUIDs); + + this->IsLoading = false; + this->DeleteActionVisible = deleteActionWasVisible; + this->QueryPatientPushButton->setEnabled(queryPatientButtonWasEnabled); + QApplication::restoreOverrideCursor(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::removeAllPatientItemWidgets() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + + int wasBlocking = this->PatientsTabWidget->blockSignals(true); + for (int patientIndex = 0; patientIndex < this->PatientsTabWidget->count(); ++patientIndex) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(this->PatientsTabWidget->widget(patientIndex)); + if (!patientItemWidget) + { + continue; + } + + this->PatientsTabWidget->removeTab(patientIndex); + q->disconnect(patientItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + q, SLOT(showPatientContextMenu(const QPoint&))); + + QList studyItemWidgets = patientItemWidget->studyItemWidgetsList(); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgets) + { + q->disconnect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemDoubleClicked(QTableWidgetItem*)), + q, SLOT(onLoad())); + } + + delete patientItemWidget; + patientIndex--; + } + this->PatientsTabWidget->blockSignals(wasBlocking); +} + +//---------------------------------------------------------------------------- +bool ctkDICOMVisualBrowserWidgetPrivate::isPatientTabAlreadyAdded(const QString& patientItem) +{ + bool alreadyAdded = false; + for (int index = 0; index < this->PatientsTabWidget->count(); ++index) + { + if (patientItem == this->PatientsTabWidget->tabWhatsThis(index)) + { + alreadyAdded = true; + break; + } + } + + return alreadyAdded; +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::updateSeriesTablesSelection(ctkDICOMSeriesItemWidget* selectedSeriesItemWidget) +{ + if (!selectedSeriesItemWidget) + { + return; + } + + ctkDICOMPatientItemWidget* currentPatientItemWidget = + qobject_cast(this->PatientsTabWidget->currentWidget()); + if (!currentPatientItemWidget) + { + return; + } + + QList studyItemWidgetsList = currentPatientItemWidget->studyItemWidgetsList(); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) + { + if (!studyItemWidget) + { + continue; + } + QTableWidget* seriesListTableWidget = studyItemWidget->seriesListTableWidget(); + QList selectedItems = seriesListTableWidget->selectedItems(); + foreach (QTableWidgetItem* selectedItem, selectedItems) + { + if (!selectedItem) + { + continue; + } + + int row = selectedItem->row(); + int column = selectedItem->column(); + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(seriesListTableWidget->cellWidget(row, column)); + + if (seriesItemWidget == selectedSeriesItemWidget) + { + seriesListTableWidget->itemClicked(selectedItem); + return; + } + } + } +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::getPatientUIDsFromWidgets(ctkDICOMModel::IndexType level, + QList selectedWidgets) +{ + QStringList selectedPatientUIDs; + + if (!this->DicomDatabase) + { + return selectedPatientUIDs; + } + + foreach (QWidget* selectedWidget, selectedWidgets) + { + if (!selectedWidget) + { + continue; + } + + if (level == ctkDICOMModel::PatientType) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(selectedWidget); + if (patientItemWidget) + { + QString patientID = patientItemWidget->patientID(); + selectedPatientUIDs << patientID; + } + } + } + + return selectedPatientUIDs; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::getStudyUIDsFromWidgets(ctkDICOMModel::IndexType level, + QList selectedWidgets) +{ + QStringList selectedStudyUIDs; + + if (!this->DicomDatabase) + { + return selectedStudyUIDs; + } + + foreach (QWidget* selectedWidget, selectedWidgets) + { + if (!selectedWidget) + { + continue; + } + + if (level == ctkDICOMModel::PatientType) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(selectedWidget); + if (patientItemWidget) + { + selectedStudyUIDs << this->DicomDatabase->studiesForPatient(patientItemWidget->patientItem()); + } + } + else if (level == ctkDICOMModel::StudyType) + { + ctkDICOMStudyItemWidget* studyItemWidget = + qobject_cast(selectedWidget); + if (studyItemWidget) + { + selectedStudyUIDs << studyItemWidget->studyInstanceUID(); + } + } + } + + return selectedStudyUIDs; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::getSeriesUIDsFromWidgets(ctkDICOMModel::IndexType level, + QList selectedWidgets) +{ + QStringList selectedStudyUIDs; + QStringList selectedSeriesUIDs; + + if (!this->DicomDatabase) + { + return selectedSeriesUIDs; + } + + foreach (QWidget* selectedWidget, selectedWidgets) + { + if (!selectedWidget) + { + continue; + } + + if (level == ctkDICOMModel::PatientType) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(selectedWidget); + if (patientItemWidget) + { + selectedStudyUIDs << this->DicomDatabase->studiesForPatient(patientItemWidget->patientItem()); + } + } + else if (level == ctkDICOMModel::StudyType) + { + ctkDICOMStudyItemWidget* studyItemWidget = + qobject_cast(selectedWidget); + if (studyItemWidget) + { + selectedStudyUIDs << studyItemWidget->studyInstanceUID(); + } + } + + if (level == ctkDICOMModel::SeriesType) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(selectedWidget); + if (seriesItemWidget) + { + selectedSeriesUIDs << seriesItemWidget->seriesInstanceUID(); + } + } + else + { + foreach (const QString& uid, selectedStudyUIDs) + { + selectedSeriesUIDs << this->DicomDatabase->seriesForStudy(uid); + } + } + } + + return selectedSeriesUIDs; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::filterPatientList(const QStringList& patientList, + const QMap& filters) +{ + QStringList filteredPatientList; + if (!this->DicomDatabase) + { + logger.error("filterPatientList failed, no DICOM Database has been set. \n"); + return filteredPatientList; + } + + foreach (QString patientItem, patientList) + { + bool filtered = false; + for (QString key : filters.keys()) + { + QString filter = this->DicomDatabase->fieldForPatient(key, patientItem); + QString filterValue = filters.value(key).toString(); + if (!filter.contains(filterValue, Qt::CaseInsensitive)) + { + filtered = true; + break; + } + } + + if (filtered) + { + continue; + } + + filteredPatientList.append(patientItem); + } + + return filteredPatientList; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::filterStudyList(const QStringList& studyList, + const QMap& filters) +{ + QStringList filteredStudyList; + if (!this->DicomDatabase) + { + logger.error("filterStudyList failed, no DICOM Database has been set. \n"); + return filteredStudyList; + } + + foreach (QString studyItem, studyList) + { + bool filtered = false; + for (QString key : filters.keys()) + { + QString filter = this->DicomDatabase->fieldForStudy(key, studyItem); + QString filterValue = filters.value(key).toString(); + if (key == "StudyDate") + { + int nDays = filterValue.toInt(); + if (nDays != -1) + { + QDate endDate = QDate::currentDate(); + QDate startDate = endDate.addDays(-nDays); + filter.replace(QString("-"), QString("")); + QDate studyDate = QDate::fromString(filter, "yyyyMMdd"); + if (studyDate < startDate || studyDate > endDate) + { + filtered = true; + break; + } + } + } + else if (!filter.contains(filterValue, Qt::CaseInsensitive)) + { + filtered = true; + break; + } + } + + if (filtered) + { + continue; + } + + filteredStudyList.append(studyItem); + } + + return filteredStudyList; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::filterSeriesList(const QStringList& seriesList, + const QMap& filters) +{ + QStringList filteredSeriesList; + if (!this->DicomDatabase) + { + logger.error("filterSeriesList failed, no DICOM Database has been set. \n"); + return filteredSeriesList; + } + + foreach (QString seriesItem, seriesList) + { + bool filtered = false; + for (QString key : filters.keys()) + { + QString filter = this->DicomDatabase->fieldForSeries(key, seriesItem); + if (key == "Modality") + { + QStringList filterValues = filters.value(key).toStringList(); + if (!filterValues.contains("Any") && !filterValues.contains(filter)) + { + filtered = true; + break; + } + } + else + { + QString filterValue = filters.value(key).toString(); + if (!filter.contains(filterValue, Qt::CaseInsensitive)) + { + filtered = true; + break; + } + } + } + + if (filtered) + { + continue; + } + + filteredSeriesList.append(seriesItem); + } + + return filteredSeriesList; +} + +//---------------------------------------------------------------------------- +ctkDICOMStudyItemWidget* ctkDICOMVisualBrowserWidgetPrivate::getCurrentPatientStudyWidgetByUIDs(const QString& studyInstanceUID) +{ + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(this->PatientsTabWidget->currentWidget()); + if (!patientItemWidget) + { + return nullptr; + } + + QList studyItemWidgetsList = patientItemWidget->studyItemWidgetsList(); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) + { + if (!studyItemWidget || studyItemWidget->studyInstanceUID() != studyInstanceUID) + { + continue; + } + + return studyItemWidget; + } + + return nullptr; +} + +//---------------------------------------------------------------------------- +ctkDICOMSeriesItemWidget* ctkDICOMVisualBrowserWidgetPrivate::getCurrentPatientSeriesWidgetByUIDs(const QString& studyInstanceUID, + const QString& seriesInstanceUID) +{ + ctkDICOMStudyItemWidget* studyItemWidget = this->getCurrentPatientStudyWidgetByUIDs(studyInstanceUID); + if (!studyItemWidget) + { + return nullptr; + } + + QList seriesItemWidgetsList = studyItemWidget->seriesItemWidgetsList(); + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, seriesItemWidgetsList) + { + if (!seriesItemWidget || seriesItemWidget->seriesInstanceUID() != seriesInstanceUID) + { + continue; + } + + return seriesItemWidget; + } + + return nullptr; +} + +//---------------------------------------------------------------------------- +// ctkDICOMVisualBrowserWidget methods + +//---------------------------------------------------------------------------- +ctkDICOMVisualBrowserWidget::ctkDICOMVisualBrowserWidget(QWidget* parentWidget) + : Superclass(parentWidget) + , d_ptr(new ctkDICOMVisualBrowserWidgetPrivate(*this)) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->init(); +} + +//---------------------------------------------------------------------------- +ctkDICOMVisualBrowserWidget::~ctkDICOMVisualBrowserWidget() +{ +} + +//---------------------------------------------------------------------------- +QString ctkDICOMVisualBrowserWidget::databaseDirectory() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + + // If override settings is specified then try to get database directory from there first + return d->DatabaseDirectory; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMVisualBrowserWidget::databaseDirectorySettingsKey() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->DatabaseDirectorySettingsKey; +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::setDatabaseDirectorySettingsKey(const QString& key) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->DatabaseDirectorySettingsKey = key; + + QSettings settings; + QString databaseDirectory = ctk::absolutePathFromInternal(settings.value(d->DatabaseDirectorySettingsKey, "").toString(), d->DatabaseDirectoryBase); + this->setDatabaseDirectory(databaseDirectory); +} + +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + +//---------------------------------------------------------------------------- +ctkDICOMScheduler* ctkDICOMVisualBrowserWidget::scheduler() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->Scheduler.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMVisualBrowserWidget::schedulerShared() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->Scheduler; +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::setScheduler(ctkDICOMScheduler& Scheduler) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->disconnectScheduler(); + d->Scheduler = QSharedPointer(&Scheduler, skipDelete); + d->ServerNodeWidget->setScheduler(d->Scheduler); + d->connectScheduler(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::setScheduler(QSharedPointer Scheduler) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->disconnectScheduler(); + d->Scheduler = Scheduler; + d->ServerNodeWidget->setScheduler(d->Scheduler); + d->connectScheduler(); +} + +//---------------------------------------------------------------------------- +ctkDICOMDatabase* ctkDICOMVisualBrowserWidget::dicomDatabase() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->DicomDatabase.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMVisualBrowserWidget::dicomDatabaseShared() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->DicomDatabase; +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::setTagsToPrecache(const QStringList& tags) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("setTagsToPrecache failed, no DICOM Database has been set. \n"); + return; + } + + d->DicomDatabase->setTagsToPrecache(tags); +} + +//---------------------------------------------------------------------------- +const QStringList ctkDICOMVisualBrowserWidget::tagsToPrecache() +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("Get tagsToPrecache failed, no DICOM Database has been set. \n"); + return QStringList(); + } + + return d->DicomDatabase->tagsToPrecache(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::setStorageAETitle(const QString& storageAETitle) +{ + Q_D(const ctkDICOMVisualBrowserWidget); + d->ServerNodeWidget->setStorageAETitle(storageAETitle); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMVisualBrowserWidget::storageAETitle() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->storageAETitle(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::setStoragePort(int storagePort) +{ + Q_D(const ctkDICOMVisualBrowserWidget); + d->ServerNodeWidget->setStoragePort(storagePort); +} + +//---------------------------------------------------------------------------- +int ctkDICOMVisualBrowserWidget::storagePort() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->storagePort(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMVisualBrowserWidget::getNumberOfServers() +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->getNumberOfServers(); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMVisualBrowserWidget::getNthServer(int id) +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->getNthServer(id); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMVisualBrowserWidget::getServer(const QString& connectionName) +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->getServer(connectionName); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::addServer(ctkDICOMServer* server) +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->addServer(server); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::removeServer(const QString& connectionName) +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->removeServer(connectionName); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::removeNthServer(int id) +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->removeNthServer(id); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::removeAllServers() +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->removeAllServers(); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMVisualBrowserWidget::getServerNameFromIndex(int id) +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->getServerNameFromIndex(id); +} + +//---------------------------------------------------------------------------- +int ctkDICOMVisualBrowserWidget::getServerIndexFromName(const QString& connectionName) +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget->getServerIndexFromName(connectionName); +} + +//------------------------------------------------------------------------------ +ctkDICOMServerNodeWidget2* ctkDICOMVisualBrowserWidget::serverSettingsWidget() +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServerNodeWidget; +} + +//------------------------------------------------------------------------------ +ctkCollapsibleGroupBox* ctkDICOMVisualBrowserWidget::serverSettingsGroupBox() +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->ServersSettingsCollapsibleGroupBox; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setFilteringPatientID(const QString& filteringPatientID) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringPatientID = filteringPatientID; + d->FilteringPatientIDSearchBox->setText(d->FilteringPatientID); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMVisualBrowserWidget::filteringPatientID() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->FilteringPatientID; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setFilteringPatientName(const QString& filteringPatientName) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringPatientName = filteringPatientName; + d->FilteringPatientNameSearchBox->setText(d->FilteringPatientName); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMVisualBrowserWidget::filteringPatientName() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->FilteringPatientName; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setFilteringStudyDescription(const QString& filteringStudyDescription) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringStudyDescription = filteringStudyDescription; + d->FilteringStudyDescriptionSearchBox->setText(d->FilteringStudyDescription); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMVisualBrowserWidget::filteringStudyDescription() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->FilteringStudyDescription; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setFilteringDate(const ctkDICOMPatientItemWidget::DateType& filteringDate) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringDate = filteringDate; + d->FilteringDateComboBox->setCurrentIndex(d->FilteringDate); +} + +//------------------------------------------------------------------------------ +ctkDICOMPatientItemWidget::DateType ctkDICOMVisualBrowserWidget::filteringDate() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->FilteringDate; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setFilteringSeriesDescription(const QString& filteringSeriesDescription) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringSeriesDescription = filteringSeriesDescription; + d->FilteringSeriesDescriptionSearchBox->setText(d->FilteringSeriesDescription); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMVisualBrowserWidget::filteringSeriesDescription() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->FilteringSeriesDescription; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setFilteringModalities(const QStringList& filteringModalities) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringModalities = filteringModalities; + d->updateModalityCheckableComboBox(); +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMVisualBrowserWidget::filteringModalities() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->FilteringModalities; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setNumberOfStudiesPerPatient(int numberOfStudiesPerPatient) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->NumberOfStudiesPerPatient = numberOfStudiesPerPatient; +} + +//------------------------------------------------------------------------------ +int ctkDICOMVisualBrowserWidget::numberOfStudiesPerPatient() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->NumberOfStudiesPerPatient; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setThumbnailSize(const ctkDICOMStudyItemWidget::ThumbnailSizeOption& thumbnailSize) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->ThumbnailSize = thumbnailSize; +} + +//------------------------------------------------------------------------------ +ctkDICOMStudyItemWidget::ThumbnailSizeOption ctkDICOMVisualBrowserWidget::thumbnailSize() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->ThumbnailSize; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setSendActionVisible(bool visible) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->SendActionVisible = visible; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMVisualBrowserWidget::isSendActionVisible() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->SendActionVisible; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setDeleteActionVisible(bool visible) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->DeleteActionVisible = visible; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMVisualBrowserWidget::isDeleteActionVisible() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->DeleteActionVisible; +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::addPatientItemWidget(const QString& patientItem) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("addPatientItemWidget failed, no DICOM database has been set. \n"); + return; + } + + QString patientName = d->DicomDatabase->fieldForPatient("PatientsName", patientItem); + QString patientID = d->DicomDatabase->fieldForPatient("PatientID", patientItem); + + ctkDICOMPatientItemWidget* patientItemWidget = new ctkDICOMPatientItemWidget(this); + patientItemWidget->setPatientItem(patientItem); + patientItemWidget->setPatientID(patientID); + patientItemWidget->setFilteringStudyDescription(d->FilteringStudyDescription); + patientItemWidget->setFilteringDate(d->FilteringDate); + patientItemWidget->setFilteringSeriesDescription(d->FilteringSeriesDescription); + patientItemWidget->setFilteringModalities(d->FilteringModalities); + patientItemWidget->setThumbnailSize(d->ThumbnailSize); + patientItemWidget->setNumberOfStudiesPerPatient(d->NumberOfStudiesPerPatient); + patientItemWidget->setDicomDatabase(d->DicomDatabase); + patientItemWidget->setScheduler(d->Scheduler); + patientItemWidget->setContextMenuPolicy(Qt::CustomContextMenu); + this->connect(patientItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + this, SLOT(showPatientContextMenu(const QPoint&))); + + patientName.replace(R"(^)", R"( )"); + int index = d->PatientsTabWidget->addTab(patientItemWidget, QIcon(":/Icons/patient.svg"), patientName); + d->PatientsTabWidget->setTabWhatsThis(index, patientItem); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::removePatientItemWidget(const QString& patientItem) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + for (int patientIndex = 0; patientIndex < d->PatientsTabWidget->count(); ++patientIndex) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(patientIndex)); + + if (!patientItemWidget || patientItemWidget->patientItem() != patientItem) + { + continue; + } + + d->PatientsTabWidget->removeTab(patientIndex); + this->disconnect(patientItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + this, SLOT(showPatientContextMenu(const QPoint&))); + delete patientItemWidget; + break; + } +} + +//------------------------------------------------------------------------------ +ctkDICOMPatientItemWidget* ctkDICOMVisualBrowserWidget::getPatientItemWidgetByPatientName(const QString& patientName) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + return nullptr; + } + + for (int patientIndex = 0; patientIndex < d->PatientsTabWidget->count(); ++patientIndex) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(patientIndex)); + if (!patientItemWidget) + { + continue; + } + + QString tempPatientName = d->DicomDatabase->fieldForPatient("PatientsName", patientItemWidget->patientItem()); + tempPatientName.replace(R"(^)", R"( )"); + if (tempPatientName != patientName) + { + continue; + } + + return patientItemWidget; + } + + return nullptr; +} + +//------------------------------------------------------------------------------ +int ctkDICOMVisualBrowserWidget::patientsAddedDuringImport() +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->PatientsAddedDuringImport; +} + +//------------------------------------------------------------------------------ +int ctkDICOMVisualBrowserWidget::studiesAddedDuringImport() +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->StudiesAddedDuringImport; +} + +//------------------------------------------------------------------------------ +int ctkDICOMVisualBrowserWidget::seriesAddedDuringImport() +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->SeriesAddedDuringImport; +} + +//------------------------------------------------------------------------------ +int ctkDICOMVisualBrowserWidget::instancesAddedDuringImport() +{ + Q_D(ctkDICOMVisualBrowserWidget); + return d->InstancesAddedDuringImport; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::resetItemsAddedDuringImportCounters() +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->PatientsAddedDuringImport = 0; + d->StudiesAddedDuringImport = 0; + d->SeriesAddedDuringImport = 0; + d->InstancesAddedDuringImport = 0; +} + +//------------------------------------------------------------------------------ +ctkDICOMVisualBrowserWidget::ImportDirectoryMode ctkDICOMVisualBrowserWidget::importDirectoryMode() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + ctkDICOMVisualBrowserWidgetPrivate* mutable_d = const_cast(d); + mutable_d->importOldSettings(); + QSettings settings; + return static_cast(settings.value( + "DICOM/ImportDirectoryMode", static_cast(ctkDICOMVisualBrowserWidget::ImportDirectoryAddLink)).toInt() ); +} + +//------------------------------------------------------------------------------ +ctkFileDialog* ctkDICOMVisualBrowserWidget::importDialog() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->ImportDialog; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setImportDirectoryMode(ImportDirectoryMode mode) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + QSettings settings; + settings.setValue("DICOM/ImportDirectoryMode", static_cast(mode)); + if (!d->ImportDialog) + { + return; + } + if (!(d->ImportDialog->options() & QFileDialog::DontUseNativeDialog)) + { + return; // Native dialog does not support modifying or getting widget elements. + } + QComboBox* comboBox = d->ImportDialog->bottomWidget()->findChild(); + comboBox->setCurrentIndex(comboBox->findData(static_cast(mode))); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setDatabaseDirectory(const QString& directory) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("setDatabaseDirectory failed, no DICOM database has been set. \n"); + return; + } + + QString absDirectory = ctk::absolutePathFromInternal(directory, d->DatabaseDirectoryBase); + + // close the active DICOM database + d->DicomDatabase->closeDatabase(); + + // open DICOM database on the directory + QString databaseFileName = QDir(absDirectory).filePath("ctkDICOM.sql"); + + bool success = true; + if (!QDir(absDirectory).exists() + || (!ctk::isDirEmpty(QDir(absDirectory)) && !QFile(databaseFileName).exists())) + { + logger.warn(tr("Database folder does not contain ctkDICOM.sql file: ") + absDirectory + "\n"); + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + //: %1 is the folder path + tr("No valid DICOM database found in folder %1.").arg(absDirectory) + ); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); + success = false; + } + + if (success) + { + bool databaseOpenSuccess = false; + try + { + d->DicomDatabase->openDatabase(databaseFileName); + databaseOpenSuccess = d->DicomDatabase->isOpen(); + } + catch (const std::exception& e) + { + Q_UNUSED(e); + databaseOpenSuccess = false; + } + if (!databaseOpenSuccess || d->DicomDatabase->schemaVersionLoaded().isEmpty()) + { + logger.warn(tr("Database error: %1 \n").arg(d->DicomDatabase->lastError())); + d->DicomDatabase->closeDatabase(); + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + //: %1 is the folder path + tr("No valid DICOM database found in folder %1.").arg(absDirectory) + ); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); + success = false; + } + } + + if (success) + { + if (d->DicomDatabase->schemaVersionLoaded() != d->DicomDatabase->schemaVersion()) + { + logger.warn(tr("Database version mismatch: version of selected database = %1, version required = %2 \n") + .arg(d->DicomDatabase->schemaVersionLoaded()).arg(d->DicomDatabase->schemaVersion())); + d->DicomDatabase->closeDatabase(); + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + //: %1 is the folder path + tr("Incompatible DICOM database version found in folder %1.").arg(absDirectory) + ); + d->UpdateDatabaseButton->show(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); + success = false; + } + } + + if (success) + { + d->DatabaseDirectoryProblemFrame->hide(); + } + + // Save new database directory in this object and in application settings. + d->DatabaseDirectory = absDirectory; + if (!d->DatabaseDirectorySettingsKey.isEmpty()) + { + QSettings settings; + settings.setValue(d->DatabaseDirectorySettingsKey, ctk::internalPathFromAbsolute(absDirectory, d->DatabaseDirectoryBase)); + settings.sync(); + } + + this->onShowPatients(); + + // pass DICOM database instance to Import widget + emit databaseDirectoryChanged(absDirectory); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::openImportDialog() +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->ProgressFrame->show(); + d->ProgressDetailLineEdit->hide(); + int dialogCode = d->ImportDialog->exec(); + if (dialogCode == QDialog::Rejected) + { + d->ProgressFrame->hide(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::importDirectories(const QStringList& directories, ImportDirectoryMode mode) +{ + Q_D(ctkDICOMVisualBrowserWidget); + foreach (const QString& directory, directories) + { + d->importDirectory(directory, mode); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::importDirectory(const QString& directory, ImportDirectoryMode mode) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->importDirectory(directory, mode); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::importFiles(const QStringList& files, ImportDirectoryMode mode) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->importFiles(files, mode); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::waitForImportFinished() +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->Scheduler || !d->Indexer) + { + logger.error("waitForImportFinished failed, no task pool has been set. \n"); + return; + } + d->Indexer->waitForImportFinished(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onIndexingProgress(int percent) +{ + Q_D(const ctkDICOMVisualBrowserWidget); + d->ProgressBar->setValue(percent); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onIndexingProgressStep(const QString& step) +{ + Q_D(const ctkDICOMVisualBrowserWidget); + d->ProgressLabel->setText(step); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onIndexingProgressDetail(const QString& detail) +{ + Q_D(const ctkDICOMVisualBrowserWidget); + if (detail.isEmpty()) + { + d->ProgressDetailLineEdit->hide(); + } + else + { + d->ProgressDetailLineEdit->setText(detail); + d->ProgressDetailLineEdit->show(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onIndexingComplete(int patientsAdded, int studiesAdded, int seriesAdded, int imagesAdded) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + d->PatientsAddedDuringImport += patientsAdded; + d->StudiesAddedDuringImport += studiesAdded; + d->SeriesAddedDuringImport += seriesAdded; + d->InstancesAddedDuringImport += imagesAdded; + + d->ProgressFrame->hide(); + d->QueryPatientPushButton->setIcon(QIcon(":/Icons/query.svg")); + + // allow users of this widget to know that the process has finished + emit directoryImported(); + + d->createPatients(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::selectDatabaseDirectory() +{ + Q_D(const ctkDICOMVisualBrowserWidget); + d->DatabaseDirectoryProblemFrame->hide(); + ctkDirectoryButton directoryButton(this); + directoryButton.setDirectory(d->DatabaseDirectory); + QString dir = directoryButton.browse(); + this->setDatabaseDirectory(dir); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::createNewDatabaseDirectory() +{ + Q_D(ctkDICOMVisualBrowserWidget); + + // Use the current database folder as a basis for the new name + QString baseFolder = this->databaseDirectory(); + if (baseFolder.isEmpty()) + { + baseFolder = d->DefaultDatabaseDirectory; + } + else + { + // only use existing folder name as a basis if it is empty or + // a valid database + if (!ctk::isDirEmpty(QDir(baseFolder))) + { + QString databaseFileName = QDir(baseFolder).filePath("ctkDICOM.sql"); + if (!QFile(databaseFileName).exists()) + { + // current folder is a non-empty and not a DICOM database folder + // create a subfolder for the new DICOM database based on the name + // of default database path + QFileInfo defaultFolderInfo(d->DefaultDatabaseDirectory); + QString defaultSubfolderName = defaultFolderInfo.fileName(); + if (defaultSubfolderName.isEmpty()) + { + defaultSubfolderName = defaultFolderInfo.dir().dirName(); + } + baseFolder += "/" + defaultSubfolderName; + } + } + } + // Remove existing numerical suffix + QString separator = "_"; + bool isSuffixValid = false; + QString suffixStr = baseFolder.split(separator).last(); + int suffixStart = suffixStr.toInt(&isSuffixValid); + if (isSuffixValid) + { + QStringList baseFolderComponents = baseFolder.split(separator); + baseFolderComponents.removeLast(); + baseFolder = baseFolderComponents.join(separator); + } + // Try folder names, starting with the current one, + // incrementing the original numerical suffix. + int attemptsCount = 100; + for (int attempt = 0; attempt < attemptsCount; attempt++) + { + QString newFolder = baseFolder; + int suffix = (suffixStart + attempt) % attemptsCount; + if (suffix) + { + newFolder += separator + QString::number(suffix); + } + if (!QDir(newFolder).exists()) + { + if (!QDir().mkpath(newFolder)) + { + continue; + } + } + if (!ctk::isDirEmpty(QDir(newFolder))) + { + continue; + } + // Folder exists and empty, try to use this + setDatabaseDirectory(newFolder); + return; + } + std::cerr << "Failed to create new database in folder: " << qPrintable(baseFolder) << "\n"; + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + //: %1 is the folder path + tr("Failed to create new database in folder %1.").arg(QDir(baseFolder).absolutePath()) + ); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::updateDatabase() +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->DatabaseDirectoryProblemFrame->hide(); + d->showUpdateSchemaDialog(); + QString dir = this->databaseDirectory(); + // open DICOM database on the directory + QString databaseFileName = QDir(dir).filePath("ctkDICOM.sql"); + try + { + d->DicomDatabase->openDatabase(databaseFileName); + } + catch (const std::exception& e) + { + Q_UNUSED(e); + std::cerr << "Database error: " << qPrintable(d->DicomDatabase->lastError()) << "\n"; + d->DicomDatabase->closeDatabase(); + return; + } + d->DicomDatabase->updateSchema(); + // Update GUI + this->setDatabaseDirectory(dir); +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMVisualBrowserWidget::fileListForCurrentSelection(ctkDICOMModel::IndexType level, + const QList& selectedWidgets) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("fileListForCurrentSelection failed, no DICOM database has been set. \n"); + return QStringList(); + } + + QStringList selectedSeriesUIDs = d->getSeriesUIDsFromWidgets(level, selectedWidgets); + + QStringList fileList; + foreach (const QString& selectedSeriesUID, selectedSeriesUIDs) + { + fileList << d->DicomDatabase->filesForSeries(selectedSeriesUID); + } + return fileList; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::showMetadata(const QStringList& fileList) +{ + Q_D(const ctkDICOMVisualBrowserWidget); + d->MetadataDialog->setFileList(fileList); + d->MetadataDialog->show(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::removeSelectedItems(ctkDICOMModel::IndexType level, + const QList& selectedWidgets) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("removeSelectedItems failed, no DICOM database has been set. \n"); + return; + } + + QStringList selectedPatientUIDs; + QStringList selectedStudyUIDs; + QStringList selectedSeriesUIDs; + + if (level == ctkDICOMModel::RootType) + { + for (int patientIndex = 0; patientIndex < d->PatientsTabWidget->count(); ++patientIndex) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(patientIndex)); + if (!patientItemWidget) + { + continue; + } + QString patientItem = patientItemWidget->patientItem(); + QString patientID = patientItemWidget->patientID(); + selectedStudyUIDs << d->DicomDatabase->studiesForPatient(patientItem); + selectedPatientUIDs << patientID; + } + + if (!this->confirmDeleteSelectedUIDs(selectedPatientUIDs)) + { + return; + } + + d->removeAllPatientItemWidgets(); + } + else if (level == ctkDICOMModel::PatientType) + { + selectedPatientUIDs = d->getPatientUIDsFromWidgets(ctkDICOMModel::PatientType, selectedWidgets); + if (!this->confirmDeleteSelectedUIDs(selectedPatientUIDs)) + { + return; + } + } + else if (level == ctkDICOMModel::StudyType) + { + selectedStudyUIDs = d->getStudyUIDsFromWidgets(ctkDICOMModel::StudyType, selectedWidgets); + if (!this->confirmDeleteSelectedUIDs(selectedStudyUIDs)) + { + return; + } + } + else if (level == ctkDICOMModel::SeriesType) + { + selectedSeriesUIDs = d->getSeriesUIDsFromWidgets(ctkDICOMModel::SeriesType, selectedWidgets); + if (!this->confirmDeleteSelectedUIDs(selectedSeriesUIDs)) + { + return; + } + } + + foreach (QWidget* selectedWidget, selectedWidgets) + { + if (!selectedWidget) + { + continue; + } + else if (level == ctkDICOMModel::PatientType) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(selectedWidget); + if (patientItemWidget) + { + QString patientItem = patientItemWidget->patientItem(); + selectedStudyUIDs << d->DicomDatabase->studiesForPatient(patientItem); + + this->removePatientItemWidget(patientItem); + } + } + else if (level == ctkDICOMModel::StudyType) + { + ctkDICOMStudyItemWidget* studyItemWidget = + qobject_cast(selectedWidget); + if (studyItemWidget) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->currentWidget()); + if (patientItemWidget) + { + patientItemWidget->removeStudyItemWidget(studyItemWidget->studyItem()); + } + } + } + + if (level == ctkDICOMModel::SeriesType) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(selectedWidget); + if (seriesItemWidget) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->currentWidget()); + if (patientItemWidget) + { + QList studyItemWidgetsList = patientItemWidget->studyItemWidgetsList(); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) + { + if (!studyItemWidget || studyItemWidget->studyInstanceUID() != seriesItemWidget->studyInstanceUID()) + { + continue; + } + + studyItemWidget->removeSeriesItemWidget(seriesItemWidget->seriesItem()); + break; + } + } + } + } + else + { + foreach (const QString& uid, selectedStudyUIDs) + { + selectedSeriesUIDs << d->DicomDatabase->seriesForStudy(uid); + } + } + } + + // Stop fetching jobs for selected widgets. + d->Scheduler->stopJobsByUIDs(selectedPatientUIDs, + selectedStudyUIDs, + selectedSeriesUIDs); + + foreach (const QString& uid, selectedSeriesUIDs) + { + d->DicomDatabase->removeSeries(uid, false, level == ctkDICOMModel::RootType); + } + foreach (const QString& uid, selectedStudyUIDs) + { + d->DicomDatabase->removeStudy(uid, level == ctkDICOMModel::RootType); + } + foreach (const QString& uid, selectedPatientUIDs) + { + d->DicomDatabase->removePatient(uid, level == ctkDICOMModel::RootType); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onFilteringPatientIDChanged() +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringPatientID = d->FilteringPatientIDSearchBox->text(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onFilteringPatientNameChanged() +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringPatientName = d->FilteringPatientNameSearchBox->text(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onFilteringStudyDescriptionChanged() +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringStudyDescription = d->FilteringStudyDescriptionSearchBox->text(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onFilteringSeriesDescriptionChanged() +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringSeriesDescription = d->FilteringSeriesDescriptionSearchBox->text(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onFilteringModalityCheckableComboBoxChanged() +{ + Q_D(ctkDICOMVisualBrowserWidget); + + d->PreviousFilteringModalities = d->FilteringModalities; + d->FilteringModalities.clear(); + QModelIndexList indexList = d->FilteringModalityCheckableComboBox->checkedIndexes(); + foreach (QModelIndex index, indexList) + { + QVariant value = d->FilteringModalityCheckableComboBox->checkableModel()->data(index); + d->FilteringModalities.append(value.toString()); + } + d->updateModalityCheckableComboBox(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onFilteringDateComboBoxChanged(int index) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->FilteringDate = static_cast(index); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onShowPatients() +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (d->IsGUIUpdating) + { + return; + } + + if (!d->DicomDatabase) + { + logger.error("onQueryPatient failed, no DICOM database has been set. \n"); + return; + } + + // Stop any fetching task. + this->onStop(); + + // Clear the UI. + d->removeAllPatientItemWidgets(); + + if (d->DicomDatabase->patients().count() == 0) + { + d->setBackgroundColorToFilterWidgets(true); + + d->WarningPushButton->setText(tr("No patients have been found in the local database.")); + d->WarningPushButton->show(); + d->patientsTabMenuToolButton->hide(); + return; + } + else + { + d->WarningPushButton->hide(); + } + + d->createPatients(); + d->updateFiltersWarnings(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onQueryPatients() +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (d->IsGUIUpdating) + { + return; + } + + if (!d->DicomDatabase) + { + logger.error("onQueryPatient failed, no DICOM database has been set. \n"); + return; + } + + // Stop any fetching task. + this->onStop(); + + // Clear the UI. + d->removeAllPatientItemWidgets(); + + bool filtersEmpty = + d->FilteringPatientID.isEmpty() && + d->FilteringPatientName.isEmpty() && + d->FilteringStudyDescription.isEmpty() && + d->FilteringSeriesDescription.isEmpty() && + d->FilteringDate == ctkDICOMPatientItemWidget::DateType::Any && + d->FilteringModalities.contains("Any"); + + if (d->DicomDatabase->patients().count() == 0 && + filtersEmpty) + { + d->setBackgroundColorToFilterWidgets(true); + + d->WarningPushButton->setText(tr("No filters have been set and no patients have been found in the local database." + "\nPlease set at least one filter to query the servers")); + d->WarningPushButton->show(); + d->patientsTabMenuToolButton->hide(); + return; + } + else + { + d->WarningPushButton->hide(); + } + + d->createPatients(); + + if (filtersEmpty || (d->Scheduler && d->Scheduler->getNumberOfQueryRetrieveServers() == 0)) + { + d->updateFiltersWarnings(); + } + else if (d->Scheduler && d->Scheduler->getNumberOfQueryRetrieveServers() > 0) + { + QMap parameters; + parameters["Name"] = d->FilteringPatientName; + parameters["ID"] = d->FilteringPatientID; + parameters["Study"] = d->FilteringStudyDescription; + parameters["Series"] = d->FilteringSeriesDescription; + if (!d->FilteringModalities.contains("Any")) + { + parameters["Modalities"] = d->FilteringModalities; + } + + int nDays = ctkDICOMPatientItemWidget::getNDaysFromFilteringDate(d->FilteringDate); + if (nDays != -1) + { + QDate endDate = QDate::currentDate(); + QString formattedEndDate = endDate.toString("yyyyMMdd"); + + QDate startDate = endDate.addDays(-nDays); + QString formattedStartDate = startDate.toString("yyyyMMdd"); + + parameters["StartDate"] = formattedStartDate; + parameters["EndDate"] = formattedEndDate; + } + + d->Scheduler->setFilters(parameters); + d->Scheduler->queryPatients(QThread::NormalPriority); + + d->QueryPatientPushButton->setIcon(QIcon(":/Icons/wait.svg")); + d->ProgressFrame->show(); + d->ProgressDetailLineEdit->hide(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::updateGUIFromScheduler(const QVariant& data) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->ProgressFrame->hide(); + d->QueryPatientPushButton->setIcon(QIcon(":/Icons/query.svg")); + + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + d->updateFiltersWarnings(); + return; + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryStudies || + td.JobType == ctkDICOMJobResponseSet::JobType::QuerySeries) + { + d->updateFiltersWarnings(); + return; + } + else if (td.JobType != ctkDICOMJobResponseSet::JobType::QueryPatients) + { + return; + } + + d->updateFiltersWarnings(); + if (td.NumberOfDataSets == 0) + { + d->WarningPushButton->setText(tr("The query provided no results. Please refine your filters.")); + d->WarningPushButton->show(); + } + + d->createPatients(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onTaskFailed(const QVariant& data) +{ + Q_D(ctkDICOMVisualBrowserWidget); + ctkDICOMJobDetail td = data.value(); + + if (td.JobClass == "ctkDICOMQueryJob") + { + d->updateFiltersWarnings(); + d->ProgressFrame->hide(); + d->QueryPatientPushButton->setIcon(QIcon(":/Icons/query.svg")); + } + + if (td.JobClass == "ctkDICOMRetrieveJob") + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + d->getCurrentPatientSeriesWidgetByUIDs(td.StudyInstanceUID, td.SeriesInstanceUID); + if (seriesItemWidget) + { + seriesItemWidget->setRetrieveFailed(true); + } + + d->WarningPushButton->setText(tr("%1 job failed to fetch the data." + "\nFor more information please open the error report console. \n").arg(td.JobUID)); + d->WarningPushButton->show(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onPatientItemChanged(int index) +{ + Q_D(ctkDICOMVisualBrowserWidget); + ctkDICOMPatientItemWidget* patientItem = + qobject_cast(d->PatientsTabWidget->widget(index)); + if (!patientItem || patientItem->patientItem().isEmpty()) + { + return; + } + + patientItem->generateStudies(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::showPatientContextMenu(const QPoint& point) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->currentWidget()); + if (!patientItemWidget) + { + return; + } + + QList selectedWidgets; + selectedWidgets.append(patientItemWidget); + + QPoint globalPos = patientItemWidget->mapToGlobal(point); + QMenu* patientMenu = new QMenu(); + + QString loadString = tr("Load patient files"); + QAction* loadAction = new QAction(loadString, patientMenu); + patientMenu->addAction(loadAction); + + QString metadataString = tr("View patient DICOM metadata"); + QAction* metadataAction = new QAction(metadataString, patientMenu); + patientMenu->addAction(metadataAction); + + QString deleteString = tr("Delete patient from local database"); + QAction* deleteAction = new QAction(deleteString, patientMenu); + patientMenu->addAction(deleteAction); + deleteAction->setVisible(this->isDeleteActionVisible()); + + QString exportString = tr("Export patient to file system"); + QAction* exportAction = new QAction(exportString, patientMenu); + patientMenu->addAction(exportAction); + + QString sendString = tr("Send patient to DICOM server"); + QAction* sendAction = new QAction(sendString, patientMenu); + sendAction->setVisible(this->isSendActionVisible()); + patientMenu->addAction(sendAction); + + QAction* selectedAction = patientMenu->exec(globalPos); + if (selectedAction == loadAction) + { + // first select all the series for all studies + ctkDICOMPatientItemWidget* currentPatientItemWidget = + qobject_cast(d->PatientsTabWidget->currentWidget()); + if (currentPatientItemWidget) + { + currentPatientItemWidget->setSelection(true); + } + + this->onLoad(); + } + else if (selectedAction == metadataAction) + { + this->showMetadata(this->fileListForCurrentSelection(ctkDICOMModel::PatientType, selectedWidgets)); + } + else if (selectedAction == deleteAction) + { + this->removeSelectedItems(ctkDICOMModel::PatientType, selectedWidgets); + } + else if (selectedAction == exportAction) + { + this->exportSelectedItems(ctkDICOMModel::PatientType, selectedWidgets); + } + else if (selectedAction == sendAction) + { + emit sendRequested(this->fileListForCurrentSelection(ctkDICOMModel::PatientType, selectedWidgets)); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::showStudyContextMenu(const QPoint& point) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + ctkDICOMStudyItemWidget* studyItemWidget = + qobject_cast(QObject::sender()); + if (!studyItemWidget) + { + return; + } + + studyItemWidget->setSelection(true); + + ctkDICOMPatientItemWidget* currentPatientItemWidget = + qobject_cast(d->PatientsTabWidget->currentWidget()); + if (!currentPatientItemWidget) + { + return; + } + + QList studyItemWidgetsList = currentPatientItemWidget->studyItemWidgetsList(); + QList selectedWidgets; + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) + { + if (!studyItemWidget || !studyItemWidget->selection()) + { + continue; + } + + selectedWidgets.append(studyItemWidget); + } + + int numberOfSelectedStudies = selectedWidgets.count(); + + QPoint globalPos = studyItemWidget->mapToGlobal(point); + QMenu* studyMenu = new QMenu(); + + QString loadString = numberOfSelectedStudies == 1 ? tr("Load study") : + tr("Load %1 studies").arg(numberOfSelectedStudies); + QAction* loadAction = new QAction(loadString, studyMenu); + studyMenu->addAction(loadAction); + + QString metadataString = numberOfSelectedStudies == 1 ? tr("View study DICOM metadata") : + tr("View %1 studies DICOM metadata").arg(numberOfSelectedStudies); + QAction* metadataAction = new QAction(metadataString, studyMenu); + studyMenu->addAction(metadataAction); + + QString deleteString = numberOfSelectedStudies == 1 ? tr("Delete study from local database") : + tr("Delete %1 studies from local database").arg(numberOfSelectedStudies); + QAction* deleteAction = new QAction(deleteString, studyMenu); + studyMenu->addAction(deleteAction); + deleteAction->setVisible(this->isDeleteActionVisible()); + + QString exportString = numberOfSelectedStudies == 1 ? tr("Export study to file system") : + tr("Export %1 studies to file system").arg(numberOfSelectedStudies); + QAction* exportAction = new QAction(exportString, studyMenu); + studyMenu->addAction(exportAction); + + QString sendString = numberOfSelectedStudies == 1 ? tr("Send study to DICOM server") : + tr("Send %1 studies to DICOM server").arg(numberOfSelectedStudies); + QAction* sendAction = new QAction(sendString, studyMenu); + sendAction->setVisible(this->isSendActionVisible()); + studyMenu->addAction(sendAction); + + QAction* selectedAction = studyMenu->exec(globalPos); + if (selectedAction == loadAction) + { + this->onLoad(); + } + else if (selectedAction == metadataAction) + { + this->showMetadata(this->fileListForCurrentSelection(ctkDICOMModel::StudyType, selectedWidgets)); + } + else if (selectedAction == deleteAction) + { + this->removeSelectedItems(ctkDICOMModel::StudyType, selectedWidgets); + } + else if (selectedAction == exportAction) + { + this->exportSelectedItems(ctkDICOMModel::StudyType, selectedWidgets); + } + else if (selectedAction == sendAction) + { + emit sendRequested(this->fileListForCurrentSelection(ctkDICOMModel::StudyType, selectedWidgets)); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::showSeriesContextMenu(const QPoint& point) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + ctkDICOMSeriesItemWidget* selectedSeriesItemWidget = + qobject_cast(QObject::sender()); + if (!selectedSeriesItemWidget) + { + return; + } + + ctkDICOMPatientItemWidget* currentPatientItemWidget = + qobject_cast(d->PatientsTabWidget->currentWidget()); + if (!currentPatientItemWidget) + { + return; + } + + d->updateSeriesTablesSelection(selectedSeriesItemWidget); + QList studyItemWidgetsList = currentPatientItemWidget->studyItemWidgetsList(); + QList selectedWidgets; + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) + { + if (!studyItemWidget) + { + continue; + } + QTableWidget* seriesListTableWidget = studyItemWidget->seriesListTableWidget(); + QList selectedItems = seriesListTableWidget->selectedItems(); + foreach (QTableWidgetItem* selectedItem, selectedItems) + { + if (!selectedItem) + { + continue; + } + + int row = selectedItem->row(); + int column = selectedItem->column(); + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(seriesListTableWidget->cellWidget(row, column)); + + selectedWidgets.append(seriesItemWidget); + } + } + + int numberOfSelectedSeries = selectedWidgets.count(); + + QPoint globalPos = selectedSeriesItemWidget->mapToGlobal(point); + QMenu* seriesMenu = new QMenu(); + + QString loadString = numberOfSelectedSeries == 1 ? tr("Load series") : + tr("Load %1 series").arg(numberOfSelectedSeries); + QAction *loadAction = new QAction(loadString, seriesMenu); + seriesMenu->addAction(loadAction); + + QString metadataString = numberOfSelectedSeries == 1 ? tr("View series DICOM metadata") : + tr("View %1 series DICOM metadata").arg(numberOfSelectedSeries); + QAction *metadataAction = new QAction(metadataString, seriesMenu); + seriesMenu->addAction(metadataAction); + + QString deleteString = numberOfSelectedSeries == 1 ? tr("Delete series from local database") : + tr("Delete %1 series from local database").arg(numberOfSelectedSeries); + QAction *deleteAction = new QAction(deleteString, seriesMenu); + seriesMenu->addAction(deleteAction); + deleteAction->setVisible(this->isDeleteActionVisible()); + + QString exportString = numberOfSelectedSeries == 1 ? tr("Export series to file system") : + tr("Export %1 series to file system").arg(numberOfSelectedSeries); + QAction *exportAction = new QAction(exportString, seriesMenu); + seriesMenu->addAction(exportAction); + + QString sendString = numberOfSelectedSeries == 1 ? tr("Send series to DICOM server") : + tr("Send %1 series to DICOM server").arg(numberOfSelectedSeries); + QAction* sendAction = new QAction(sendString, seriesMenu); + sendAction->setVisible(this->isSendActionVisible()); + seriesMenu->addAction(sendAction); + + QAction* selectedAction = seriesMenu->exec(globalPos); + if (selectedAction == loadAction) + { + this->onLoad(); + } + else if (selectedAction == metadataAction) + { + this->showMetadata(this->fileListForCurrentSelection(ctkDICOMModel::SeriesType, selectedWidgets)); + } + else if (selectedAction == deleteAction) + { + this->removeSelectedItems(ctkDICOMModel::SeriesType, selectedWidgets); + } + else if (selectedAction == exportAction) + { + this->exportSelectedItems(ctkDICOMModel::SeriesType, selectedWidgets); + } + else if (selectedAction == sendAction) + { + emit sendRequested(this->fileListForCurrentSelection(ctkDICOMModel::SeriesType, selectedWidgets)); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onPatientsTabMenuToolButtonClicked() +{ + Q_D(ctkDICOMVisualBrowserWidget); + + QPoint globalPos = this->mapToGlobal(d->patientsTabMenuToolButton->geometry().bottomLeft()); + globalPos.setY(globalPos.y() + d->patientsTabMenuToolButton->height() * 3); + globalPos.setX(globalPos.x()); + + QMenu* patientMenu = new QMenu(); + patientMenu->move(globalPos); + for (int patientIndex = 0; patientIndex < d->PatientsTabWidget->count(); ++patientIndex) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(patientIndex)); + if (!patientItemWidget) + { + continue; + } + + QString patientItem = patientItemWidget->patientItem(); + QString patientName = d->DicomDatabase->fieldForPatient("PatientsName", patientItem); + patientName.replace(R"(^)", R"( )"); + QAction* changePatientAction = new QAction(patientName, patientMenu); + if (patientItemWidget == d->PatientsTabWidget->currentWidget()) + { + changePatientAction->setIcon(QIcon(":Icons/patient.svg")); + QFont font(changePatientAction->font()); + font.setBold(true); + changePatientAction->setFont(font); + } + patientMenu->addAction(changePatientAction); + } + + patientMenu->addSeparator(); + QString deleteString = tr("Delete all Patients from local database"); + QAction* deleteAction = new QAction(deleteString, patientMenu); + deleteAction->setIcon(QIcon(":Icons/delete.svg")); + patientMenu->addAction(deleteAction); + deleteAction->setVisible(this->isDeleteActionVisible()); + + QAction* selectedAction = patientMenu->exec(globalPos); + if (selectedAction == deleteAction) + { + this->removeSelectedItems(ctkDICOMModel::RootType); + d->patientsTabMenuToolButton->hide(); + } + else if (selectedAction) + { + QString patientName = selectedAction->text(); + ctkDICOMPatientItemWidget* patientItemWidget = this->getPatientItemWidgetByPatientName(patientName); + if (patientItemWidget) + { + d->PatientsTabWidget->setCurrentWidget(patientItemWidget); + } + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::exportSelectedItems(ctkDICOMModel::IndexType level, + const QList& selectedWidgets) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("exportSelectedItems failed, no DICOM database has been set. \n"); + return; + } + + ctkFileDialog* directoryDialog = new ctkFileDialog(); + directoryDialog->setOption(QFileDialog::ShowDirsOnly); + directoryDialog->setFileMode(QFileDialog::Directory); + directoryDialog->setOption(QFileDialog::ShowDirsOnly); + bool res = directoryDialog->exec(); + if (!res) + { + delete directoryDialog; + return; + } + QStringList dirs = directoryDialog->selectedFiles(); + delete directoryDialog; + QString dirPath = dirs[0]; + + QStringList selectedSeriesUIDs = d->getSeriesUIDsFromWidgets(level, selectedWidgets); + + this->exportSeries(dirPath, selectedSeriesUIDs); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::exportSeries(const QString& dirPath, const QStringList& uids) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("exportSeries failed, no DICOM database has been set. \n"); + return; + } + + foreach (const QString& uid, uids) + { + QStringList filesForSeries = d->DicomDatabase->filesForSeries(uid); + + // Use the first file to get the overall series information + QString firstFilePath = filesForSeries[0]; + QHash descriptions(d->DicomDatabase->descriptionsForFile(firstFilePath)); + QString patientName = descriptions["PatientsName"]; + QString patientIDTag = QString("0010,0020"); + QString patientID = d->DicomDatabase->fileValue(firstFilePath, patientIDTag); + QString studyDescription = descriptions["StudyDescription"]; + QString seriesDescription = descriptions["SeriesDescription"]; + QString studyDateTag = QString("0008,0020"); + QString studyDate = d->DicomDatabase->fileValue(firstFilePath, studyDateTag); + QString seriesNumberTag = QString("0020,0011"); + QString seriesNumber = d->DicomDatabase->fileValue(firstFilePath, seriesNumberTag); + + QString sep = "/"; + QString nameSep = "-"; + QString destinationDir = dirPath + sep + d->filenameSafeString(patientID); + if (!patientName.isEmpty()) + { + destinationDir += nameSep + d->filenameSafeString(patientName); + } + destinationDir += sep + d->filenameSafeString(studyDate); + if (!studyDescription.isEmpty()) + { + destinationDir += nameSep + d->filenameSafeString(studyDescription); + } + destinationDir += sep + d->filenameSafeString(seriesNumber); + if (!seriesDescription.isEmpty()) + { + destinationDir += nameSep + d->filenameSafeString(seriesDescription); + } + destinationDir += sep; + + + // create the destination directory if necessary + if (!QDir().exists(destinationDir)) + { + if (!QDir().mkpath(destinationDir)) + { + //: %1 is the destination directory + QString errorString = tr("Unable to create export destination directory:\n\n%1" + "\n\nHalting export.") + .arg(destinationDir); + ctkMessageBox createDirectoryErrorMessageBox(this); + createDirectoryErrorMessageBox.setText(errorString); + createDirectoryErrorMessageBox.setIcon(QMessageBox::Warning); + createDirectoryErrorMessageBox.exec(); + return; + } + } + + // show progress + if (d->ExportProgress == 0) + { + d->ExportProgress = new QProgressDialog(tr("DICOM Export"), tr("Close"), 0, 100, this, Qt::WindowTitleHint | Qt::WindowSystemMenuHint); + d->ExportProgress->setWindowModality(Qt::ApplicationModal); + d->ExportProgress->setMinimumDuration(0); + } + QLabel* exportLabel = new QLabel( + //: %1 is the series number + tr("Exporting series %1").arg(seriesNumber) + ); + d->ExportProgress->setLabel(exportLabel); + d->ExportProgress->setValue(0); + + int fileNumber = 0; + int numFiles = filesForSeries.size(); + d->ExportProgress->setMaximum(numFiles); + foreach (const QString& filePath, filesForSeries) + { + // File name example: my/destination/folder/000001.dcm + QString destinationFileName = QStringLiteral("%1%2.dcm").arg(destinationDir).arg(fileNumber, 6, 10, QLatin1Char('0')); + + if (!QFile::exists(filePath)) + { + d->ExportProgress->setValue(numFiles); + //: %1 is the file path + QString errorString = tr("Export source file not found:\n\n%1" + "\n\nHalting export.\n\nError may be fixed via Repair.") + .arg(filePath); + ctkMessageBox copyErrorMessageBox; + copyErrorMessageBox.setText(errorString); + copyErrorMessageBox.setIcon(QMessageBox::Warning); + copyErrorMessageBox.exec(); + return; + } + if (QFile::exists(destinationFileName)) + { + d->ExportProgress->setValue(numFiles); + //: %1 is the destination file name + QString errorString = tr("Export destination file already exists:\n\n%1" + "\n\nHalting export.") + .arg(destinationFileName); + ctkMessageBox copyErrorMessageBox(this); + copyErrorMessageBox.setText(errorString); + copyErrorMessageBox.setIcon(QMessageBox::Warning); + copyErrorMessageBox.exec(); + return; + } + + bool copyResult = QFile::copy(filePath, destinationFileName); + if (!copyResult) + { + d->ExportProgress->setValue(numFiles); + //: %1 and %2 refers to source and destination file paths + QString errorString = tr("Failed to copy\n\n%1\n\nto\n\n%2" + "\n\nHalting export.") + .arg(filePath) + .arg(destinationFileName); + ctkMessageBox copyErrorMessageBox(this); + copyErrorMessageBox.setText(errorString); + copyErrorMessageBox.setIcon(QMessageBox::Warning); + copyErrorMessageBox.exec(); + return; + } + + fileNumber++; + d->ExportProgress->setValue(fileNumber); + } + d->ExportProgress->setValue(numFiles); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onImportDirectoriesSelected(const QStringList& directories) +{ + Q_D(ctkDICOMVisualBrowserWidget); + this->importDirectories(directories, this->importDirectoryMode()); + d->updateFiltersWarnings(); + + // Clear selection + d->ImportDialog->clearSelection(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onImportDirectoryComboBoxCurrentIndexChanged(int index) +{ + Q_D(ctkDICOMVisualBrowserWidget); + Q_UNUSED(index); + if (!(d->ImportDialog->options() & QFileDialog::DontUseNativeDialog)) + { + return; // Native dialog does not support modifying or getting widget elements. + } + QComboBox* comboBox = d->ImportDialog->bottomWidget()->findChild(); + ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode = + static_cast(comboBox->itemData(index).toInt()); + this->setImportDirectoryMode(mode); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onClose() +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (d->IsGUIUpdating) + { + return; + } + + this->onStop(); + this->close(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onLoad() +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (d->IsGUIUpdating) + { + return; + } + + d->retrieveSeries(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onImport() +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (d->IsGUIUpdating) + { + return; + } + + if (d->Scheduler && + d->Scheduler->numberOfJobs() > d->Scheduler->numberOfPersistentJobs()) + { + QString warningString = tr("The browser is already fetching/importing data." + "\n\n The queued tasks will be deleted. The running tasks will be stopped."); + ctkMessageBox warningMessageBox(this); + warningMessageBox.setText(warningString); + warningMessageBox.setIcon(QMessageBox::Warning); + warningMessageBox.exec(); + + this->onStop(); + } + + this->openImportDialog(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onStop(bool stopPersistentTasks) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->Scheduler) + { + return; + } + + QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); + d->Scheduler->stopAllJobs(stopPersistentTasks); + d->updateFiltersWarnings(); + d->ProgressFrame->hide(); + d->QueryPatientPushButton->setIcon(QIcon(":/Icons/query.svg")); + QApplication::restoreOverrideCursor(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setCurrentTabWidget(ctkDICOMPatientItemWidget* patientItemWidget) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->PatientsTabWidget->setCurrentWidget(patientItemWidget); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::closeEvent(QCloseEvent* event) +{ + this->onStop(); + event->accept(); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMVisualBrowserWidget::confirmDeleteSelectedUIDs(const QStringList& uids) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("confirmDeleteSelectedUIDs failed, no DICOM database has been set. \n"); + return false; + } + + if (uids.isEmpty()) + { + return false; + } + + ctkMessageBox confirmDeleteDialog(this); + QString message = tr("Do you want to delete the following selected items from the LOCAL database? \n" + "The data will not be deleted from the PACs server. \n"); + + // add the information about the selected UIDs + int numUIDs = uids.size(); + for (int i = 0; i < numUIDs; ++i) + { + QString uid = uids.at(i); + + // try using the given UID to find a descriptive string + QString patientName = d->DicomDatabase->nameForPatient(uid); + QString studyDescription = d->DicomDatabase->descriptionForStudy(uid); + QString seriesDescription = d->DicomDatabase->descriptionForSeries(uid); + + if (!patientName.isEmpty()) + { + message += QString("\n") + patientName; + } + else if (!studyDescription.isEmpty()) + { + message += QString("\n") + studyDescription; + } + else if (!seriesDescription.isEmpty()) + { + message += QString("\n") + seriesDescription; + } + else + { + // if all other descriptors are empty, use the UID + message += QString("\n") + uid; + } + } + confirmDeleteDialog.setText(message); + confirmDeleteDialog.setIcon(QMessageBox::Question); + + confirmDeleteDialog.addButton(tr("Delete"), QMessageBox::AcceptRole); + confirmDeleteDialog.addButton(tr("Cancel"), QMessageBox::RejectRole); + confirmDeleteDialog.setDontShowAgainSettingsKey("VisualDICOMBrowser/DontConfirmDeleteSelected"); + + int response = confirmDeleteDialog.exec(); + + if (response == QMessageBox::AcceptRole) + { + return true; + } + else + { + return false; + } +} diff --git a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h new file mode 100644 index 0000000000..bcf8aaa70f --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h @@ -0,0 +1,421 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) Kitware Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Davide Punzo, punzodavide@hotmail.it, + and development was supported by the Center for Intelligent Image-guided Interventions (CI3). + +=========================================================================*/ + +#ifndef __ctkDICOMVisualBrowserWidget_h +#define __ctkDICOMVisualBrowserWidget_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include + +// ctkDICOMWidgets includes +#include "ctkDICOMPatientItemWidget.h" +#include "ctkDICOMStudyItemWidget.h" +#include "ctkDICOMWidgetsExport.h" + +// DCMTK includes +#include + +class ctkCollapsibleGroupBox; +class ctkDICOMVisualBrowserWidgetPrivate; +class ctkDICOMDatabase; +class ctkFileDialog; +class ctkDICOMScheduler; +class ctkDICOMServer; +class ctkDICOMServerNodeWidget2; +class ctkDICOMJobResponseSet; + +/// \ingroup DICOM_Widgets +/// +/// \brief The DICOM visual browser widget provides an interface to organize DICOM +/// data stored in a local/server ctkDICOMDatabases. +/// +/// Using a local database avoids redundant calculations and speed up subsequent +/// access. +/// +/// The operations are queued by the scheduler into jobs with a priority and +/// executed by workers in separate threads. +/// +/// Supported operations are: +/// +/// * Filtering and navigation with thumbnails of local database and servers results +/// * Import from file system to local database +/// * Query/Retrieve from servers (DIMSE C-GET/C-MOVE ) +/// * Storage listener +/// * Send (emits only a signal for the moment, requires external implementation) +/// * Remove (only from local database, not from server) +/// * Metadata exploration +/// +class CTK_DICOM_WIDGETS_EXPORT ctkDICOMVisualBrowserWidget : public QWidget +{ + Q_OBJECT; + Q_ENUMS(ImportDirectoryMode) + Q_PROPERTY(QString databaseDirectory READ databaseDirectory WRITE setDatabaseDirectory) + Q_PROPERTY(QString databaseDirectorySettingsKey READ databaseDirectorySettingsKey WRITE setDatabaseDirectorySettingsKey) + Q_PROPERTY(QString databaseDirectoryBase READ databaseDirectoryBase WRITE setDatabaseDirectoryBase) + Q_PROPERTY(QString filteringPatientID READ filteringPatientID WRITE setFilteringPatientID); + Q_PROPERTY(QString filteringPatientName READ filteringPatientName WRITE setFilteringPatientName); + Q_PROPERTY(int numberOfStudiesPerPatient READ numberOfStudiesPerPatient WRITE setNumberOfStudiesPerPatient); + Q_PROPERTY(ctkDICOMStudyItemWidget::ThumbnailSizeOption thumbnailSize READ thumbnailSize WRITE setThumbnailSize); + Q_PROPERTY(bool sendActionVisible READ isSendActionVisible WRITE setSendActionVisible) + Q_PROPERTY(bool deleteActionVisible READ isDeleteActionVisible WRITE setDeleteActionVisible) + Q_PROPERTY(QString storageAETitle READ storageAETitle WRITE setStorageAETitle); + Q_PROPERTY(int storagePort READ storagePort WRITE setStoragePort); + +public: + typedef QWidget Superclass; + explicit ctkDICOMVisualBrowserWidget(QWidget* parent = nullptr); + virtual ~ctkDICOMVisualBrowserWidget(); + + /// Directory being used to store the dicom database + QString databaseDirectory() const; + + /// Get settings key used to store DatabaseDirectory in application settings. + QString databaseDirectorySettingsKey() const; + + /// Set settings key that stores DatabaseDirectory in application settings. + /// Calling this method sets DatabaseDirectory from current value stored in the settings + /// (overwriting current value of DatabaseDirectory). + void setDatabaseDirectorySettingsKey(const QString& settingsKey); + + /// Get the directory that will be used as a basis if databaseDirectory is specified with a relative path. + /// @see setDatabaseDirectoryBase, setDatabaseDirectory + QString databaseDirectoryBase() const; + + /// Set the directory that will be used as a basis if databaseDirectory is specified with a relative path. + /// If DatabaseDirectoryBase is empty (by default it is) then the current working directory is used as a basis. + /// @see databaseDirectoryBase, setDatabaseDirectory + void setDatabaseDirectoryBase(const QString& base); + + /// Return the task pool. + Q_INVOKABLE ctkDICOMScheduler* scheduler() const; + /// Return the task pool as a shared pointer + /// (not Python-wrappable). + QSharedPointer schedulerShared() const; + /// Set the task pool. + Q_INVOKABLE void setScheduler(ctkDICOMScheduler& scheduler); + /// Set the task pool as a shared pointer + /// (not Python-wrappable). + void setScheduler(QSharedPointer scheduler); + + /// Return the Dicom Database. + Q_INVOKABLE ctkDICOMDatabase* dicomDatabase() const; + /// Return Dicom Database as a shared pointer + /// (not Python-wrappable). + QSharedPointer dicomDatabaseShared() const; + + ///@{ + /// See ctkDICOMDatabase for description - these accessors + /// delegate to the corresponding routines of the internal + /// instance of the database. + /// @see ctkDICOMDatabase + Q_INVOKABLE void setTagsToPrecache(const QStringList& tags); + Q_INVOKABLE const QStringList tagsToPrecache(); + ///@} + + ///@{ + /// Storage AE title + /// "CTKSTORE" by default + void setStorageAETitle(const QString& storageAETitle); + QString storageAETitle() const; + ///@} + + ///@{ + /// Storage port + /// 11112 by default + void setStoragePort(int storagePort); + int storagePort() const; + ///@} + + ///@{ + /// Servers + Q_INVOKABLE int getNumberOfServers(); + Q_INVOKABLE ctkDICOMServer* getNthServer(int id); + Q_INVOKABLE ctkDICOMServer* getServer(const QString& connectionName); + Q_INVOKABLE void addServer(ctkDICOMServer* server); + Q_INVOKABLE void removeServer(const QString& connectionName); + Q_INVOKABLE void removeNthServer(int id); + Q_INVOKABLE void removeAllServers(); + Q_INVOKABLE QString getServerNameFromIndex(int id); + Q_INVOKABLE int getServerIndexFromName(const QString& connectionName); + Q_INVOKABLE ctkDICOMServerNodeWidget2* serverSettingsWidget(); + Q_INVOKABLE ctkCollapsibleGroupBox* serverSettingsGroupBox(); + ///@} + + ///@{ + /// Query Filters + /// Empty by default + void setFilteringPatientID(const QString& filteringPatientID); + QString filteringPatientID() const; + ///@} + + ///@{ + /// Empty by default + void setFilteringPatientName(const QString& filteringPatientName); + QString filteringPatientName() const; + ///@} + + ///@{ + /// Empty by default + void setFilteringStudyDescription(const QString& filteringStudyDescription); + QString filteringStudyDescription() const; + ///@} + + ///@{ + /// Available values: + /// Any, + /// Today, + /// Yesterday, + /// LastWeek, + /// LastMonth, + /// LastYear. + /// Any by default. + /// \sa ctkDICOMPatientItemWidget::DateType + void setFilteringDate(const ctkDICOMPatientItemWidget::DateType& filteringDate); + ctkDICOMPatientItemWidget::DateType filteringDate() const; + ///@] + + ///@{ + /// Empty by default + void setFilteringSeriesDescription(const QString& filteringSeriesDescription); + QString filteringSeriesDescription() const; + ///@} + + ///@{ + /// ["Any", "CR", "CT", "MR", "NM", "US", "PT", "XA"] by default + void setFilteringModalities(const QStringList& filteringModalities); + QStringList filteringModalities() const; + ///@} + + ///@{ + /// Number of non collapsed studies per patient + /// 2 by default + void setNumberOfStudiesPerPatient(int numberOfStudiesPerPatient); + int numberOfStudiesPerPatient() const; + ///@} + + ///@{ + /// Set the thumbnail size: small, medium, large + /// medium by default + void setThumbnailSize(const ctkDICOMStudyItemWidget::ThumbnailSizeOption& thumbnailSize); + ctkDICOMStudyItemWidget::ThumbnailSizeOption thumbnailSize() const; + ///@} + + ///@{ + /// Set if send action on right click context menu is available + /// false by default + void setSendActionVisible(bool visible); + bool isSendActionVisible() const; + ///@} + + ///@{ + /// Set if cancel action on right click context menu is available + /// true by default + void setDeleteActionVisible(bool visible); + bool isDeleteActionVisible() const; + ///@} + + ///@{ + /// Add/Remove Patient item widget + Q_INVOKABLE void addPatientItemWidget(const QString& patientItem); + Q_INVOKABLE void removePatientItemWidget(const QString& patientItem); + ///@} + + /// Get Patient item widget + Q_INVOKABLE ctkDICOMPatientItemWidget* getPatientItemWidgetByPatientName(const QString& patientName); + + ///@{ + /// Accessors to status of last directory import operation + int patientsAddedDuringImport(); + int studiesAddedDuringImport(); + int seriesAddedDuringImport(); + int instancesAddedDuringImport(); + ///@} + + /// Set counters of imported patients, studies, series, instances to zero. + void resetItemsAddedDuringImportCounters(); + + enum ImportDirectoryMode + { + ImportDirectoryCopy = 0, + ImportDirectoryAddLink + }; + + /// \brief Get value of ImportDirectoryMode settings. + /// + /// \sa setImportDirectoryMode(ctkDICOMBrowser::ImportDirectoryMode) + ImportDirectoryMode importDirectoryMode() const; + + /// \brief Return instance of import dialog. + /// + /// \internal + Q_INVOKABLE ctkFileDialog* importDialog() const; + +public Q_SLOTS: + /// \brief Set value of ImportDirectoryMode settings. + /// + /// Setting the value will update the comboBox found at the bottom + /// of the import dialog. + /// + /// \sa importDirectoryMode() + void setImportDirectoryMode(ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); + + void setDatabaseDirectory(const QString& directory); + + /// \brief Pop-up file dialog allowing to select and import one or multiple + /// DICOM directories. + /// + /// The dialog is extended with two additional controls: + /// + /// * **ImportDirectoryMode** combox: Allow user to select "Add Link" or "Copy" mode. + /// Associated settings is stored using key `DICOM/ImportDirectoryMode`. + void openImportDialog(); + + /// \brief Import directories + /// + /// This can be used to externally trigger an import (i.e. for testing or to support drag-and-drop) + /// + /// By default, \a mode is ImportDirectoryMode::ImportDirectoryAddLink is set. + /// + /// \sa importDirectory(QString directory, int mode) + void importDirectories(const QStringList& directories, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode = ImportDirectoryAddLink); + + /// \brief Import a directory + /// + /// This can be used to externally trigger an import (i.e. for testing or to support drag-and-drop) + /// + /// By default, \a mode is ImportDirectoryMode::ImportDirectoryAddLink is set. + void importDirectory(const QString& directory, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode = ImportDirectoryAddLink); + + /// \brief Import a list of files + /// + /// This can be used to externally trigger an import (i.e. for testing or to support drag-and-drop) + /// + /// By default, \a mode is ImportDirectoryMode::ImportDirectoryAddLink is set. + void importFiles(const QStringList& files, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode = ImportDirectoryAddLink); + + /// Wait for all import operations to complete. + /// Number of imported patients, studies, series, images since the last resetItemsAddedDuringImportCounters + /// can be retrieved by calling patientsAddedDuringImport(), studiesAddedDuringImport(), seriesAddedDuringImport(), + /// instancesAddedDuringImport() methods. + void waitForImportFinished(); + + ///@{ + /// slots to capture status updates from the database during an + /// import operation + void onIndexingProgress(int); + void onIndexingProgressStep(const QString&); + void onIndexingProgressDetail(const QString&); + void onIndexingComplete(int patientsAdded, int studiesAdded, int seriesAdded, int imagesAdded); + ///@} + + /// Show pop-up window for the user to select database directory + void selectDatabaseDirectory(); + + /// Create new database directory. + /// Current database directory used as a basis. + void createNewDatabaseDirectory(); + + /// Update database in-place to required schema version + void updateDatabase(); + + void onFilteringPatientIDChanged(); + void onFilteringPatientNameChanged(); + void onFilteringStudyDescriptionChanged(); + void onFilteringSeriesDescriptionChanged(); + void onFilteringModalityCheckableComboBoxChanged(); + void onFilteringDateComboBoxChanged(int); + void onQueryPatients(); + void onShowPatients(); + void updateGUIFromScheduler(const QVariant&); + void onTaskFailed(const QVariant&); + void onPatientItemChanged(int); + void onClose(); + void onLoad(); + void onImport(); + void onStop(bool stopPersistentTasks = false); + void setCurrentTabWidget(ctkDICOMPatientItemWidget* patientItemWidget); + +Q_SIGNALS: + /// Emitted when directory is changed + void databaseDirectoryChanged(const QString&); + /// Emitted when retrieveSeries finish to retrieve the series. + void seriesRetrieved(const QStringList& seriesInstanceUIDs); + /// Emitted when user requested network send. String list contains list of files to be exported. + void sendRequested(const QStringList&); + /// Emitted when the directory import operation has completed + void directoryImported(); + +protected: + void closeEvent(QCloseEvent*); + QScopedPointer d_ptr; + + /// Confirm with the user that they wish to delete the selected uids. + /// Add information about the selected UIDs to a message box, checks + /// for patient name, series description, study description, if all + /// empty, uses the UID. + /// Returns true if the user confirms the delete, false otherwise. + /// Remembers if the user doesn't want to show the confirmation again. + bool confirmDeleteSelectedUIDs(const QStringList& uids); + + /// Get file list for right click selection + QStringList fileListForCurrentSelection(ctkDICOMModel::IndexType level, const QList& selectedWidget); + /// Show window that displays DICOM fields of all selected items + void showMetadata(const QStringList& fileList); + /// Remove items (both database and widget) + void removeSelectedItems(ctkDICOMModel::IndexType level, const QList& selectedWidgets = QList()); + /// Export the items associated with the selected widget + void exportSelectedItems(ctkDICOMModel::IndexType level, const QList& selectedWidgets); + /// Export the series associated with the selected UIDs + void exportSeries(const QString& dirPath, const QStringList& uids); + +protected Q_SLOTS: + ///@{ + /// \brief Import directories + /// + /// This is used when user selected one or multiple + /// directories from the Import Dialog. + /// + /// \sa importDirectories(QString directory, int mode) + void onImportDirectoriesSelected(const QStringList& directories); + void onImportDirectoryComboBoxCurrentIndexChanged(int index); + ///@} + + /// Called when a right mouse click is made on a tab of the patient tab widget + void showPatientContextMenu(const QPoint& point); + /// Called when a right mouse click is made in the studies table + void showStudyContextMenu(const QPoint& point); + /// Called when a right mouse click is made in the studies table + void showSeriesContextMenu(const QPoint& point); + /// Called when clicking patients tab menu + void onPatientsTabMenuToolButtonClicked(); + +private: + Q_DECLARE_PRIVATE(ctkDICOMVisualBrowserWidget); + Q_DISABLE_COPY(ctkDICOMVisualBrowserWidget); +}; + +#endif diff --git a/Libs/Widgets/Resources/UI/ctkThumbnailLabel.ui b/Libs/Widgets/Resources/UI/ctkThumbnailLabel.ui index b655e6e29c..3e0a3d2d3a 100644 --- a/Libs/Widgets/Resources/UI/ctkThumbnailLabel.ui +++ b/Libs/Widgets/Resources/UI/ctkThumbnailLabel.ui @@ -6,36 +6,143 @@ 0 0 - 141 - 133 + 244 + 170
Thumbnail - - + + + 4 + + + 4 + + + 4 + + 4 0 - - - - Qt::AlignCenter + + + + + 0 + 0 + + + + QFrame::NoFrame + + QFrame::Raised + + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 7 + + + + + 16777215 + 7 + + + + + + + 0 + + + Qt::AlignCenter + + + false + + + Qt::Horizontal + + + false + + + QProgressBar::TopToBottom + + + + - - - - Qt::AlignCenter + + + + + 0 + 0 + + + + false + + + true + + + ctkPushButton + QPushButton +
ctkPushButton.h
+
+
diff --git a/Libs/Widgets/ctkDirectoryButton.cpp b/Libs/Widgets/ctkDirectoryButton.cpp index 85ebc1135b..672b0c3db9 100644 --- a/Libs/Widgets/ctkDirectoryButton.cpp +++ b/Libs/Widgets/ctkDirectoryButton.cpp @@ -250,7 +250,7 @@ void ctkDirectoryButton::setAcceptMode(QFileDialog::AcceptMode mode) } //----------------------------------------------------------------------------- -void ctkDirectoryButton::browse() +QString ctkDirectoryButton::browse() { // See https://bugreports.qt-project.org/browse/QTBUG-10244 class ExcludeReadOnlyFilterProxyModel : public QSortFilterProxyModel @@ -308,9 +308,10 @@ void ctkDirectoryButton::browse() // An empty directory means either that the user cancelled the dialog or the selected directory is readonly if (dir.isEmpty()) { - return; + return ""; } this->setDirectory(dir); + return dir; } //----------------------------------------------------------------------------- diff --git a/Libs/Widgets/ctkDirectoryButton.h b/Libs/Widgets/ctkDirectoryButton.h index 6d7dd58d09..2d92dfb36d 100644 --- a/Libs/Widgets/ctkDirectoryButton.h +++ b/Libs/Widgets/ctkDirectoryButton.h @@ -166,7 +166,7 @@ class CTK_WIDGETS_EXPORT ctkDirectoryButton: public QWidget public Q_SLOTS: /// browse() opens a pop up where the user can select a new directory for the /// button. browse() is automatically called when the button is clicked. - void browse(); + QString browse(); Q_SIGNALS: /// directoryChanged is emitted when the current directory changes. diff --git a/Libs/Widgets/ctkThumbnailLabel.cpp b/Libs/Widgets/ctkThumbnailLabel.cpp index 71879d211a..a53e8dd4fe 100644 --- a/Libs/Widgets/ctkThumbnailLabel.cpp +++ b/Libs/Widgets/ctkThumbnailLabel.cpp @@ -88,14 +88,43 @@ void ctkThumbnailLabelPrivate::setupUi(QWidget* widget) q->layout()->setSizeConstraint(QLayout::SetNoConstraint); // no text by default q->setText(QString()); + this->OperationProgressBar->hide(); } //---------------------------------------------------------------------------- void ctkThumbnailLabelPrivate::updateThumbnail() { + Q_Q(ctkThumbnailLabel); + QSize size = q->size(); + + if (this->TextPushButton->isVisible()) + { + if (this->TextPosition & Qt::AlignTop) + { + size.setHeight(size.height() - this->TextPushButton->height()); + } + else if (this->TextPosition & Qt::AlignBottom) + { + size.setHeight(size.height() - this->TextPushButton->height()); + } + else if (this->TextPosition & Qt::AlignLeft) + { + size.setWidth(size.width() - this->TextPushButton->width()); + } + else if (this->TextPosition & Qt::AlignRight) + { + size.setWidth(size.width() - this->TextPushButton->width()); + } + } + + if (this->OperationProgressBar->isVisible()) + { + size.setHeight(size.height() - this->OperationProgressBar->height()); + } + this->PixmapLabel->setPixmap( this->OriginalThumbnail.isNull() ? QPixmap() : - this->OriginalThumbnail.scaled(this->PixmapLabel->size(), + this->OriginalThumbnail.scaled(size, Qt::KeepAspectRatio, this->TransformationMode)); } @@ -118,13 +147,41 @@ ctkThumbnailLabel::~ctkThumbnailLabel() { } +//---------------------------------------------------------------------------- +ctkPushButton* ctkThumbnailLabel::textPushButton() +{ + Q_D(ctkThumbnailLabel); + return d->TextPushButton; +} + +//---------------------------------------------------------------------------- +QFrame *ctkThumbnailLabel::pixmapFrame() +{ + Q_D(ctkThumbnailLabel); + return d->PixmapFrame; +} + +//---------------------------------------------------------------------------- +QLabel* ctkThumbnailLabel::pixmapLabel() +{ + Q_D(ctkThumbnailLabel); + return d->PixmapLabel; +} + +//---------------------------------------------------------------------------- +QProgressBar *ctkThumbnailLabel::operationProgressBar() +{ + Q_D(ctkThumbnailLabel); + return d->OperationProgressBar; +} + //---------------------------------------------------------------------------- void ctkThumbnailLabel::setText(const QString &text) { Q_D(ctkThumbnailLabel); - d->TextLabel->setText(text); - d->TextLabel->setVisible(!text.isEmpty() && + d->TextPushButton->setText(text); + d->TextPushButton->setVisible(!text.isEmpty() && ! (d->TextPosition & Qt::AlignHCenter && d->TextPosition & Qt::AlignVCenter) ); } @@ -133,7 +190,7 @@ void ctkThumbnailLabel::setText(const QString &text) QString ctkThumbnailLabel::text()const { Q_D(const ctkThumbnailLabel); - return d->TextLabel->text(); + return d->TextPushButton->text(); } //---------------------------------------------------------------------------- @@ -144,7 +201,7 @@ void ctkThumbnailLabel::setTextPosition(const Qt::Alignment& position) int textIndex = -1; for (textIndex = 0; textIndex < this->layout()->count(); ++textIndex) { - if (this->layout()->itemAt(textIndex)->widget() == d->TextLabel) + if (this->layout()->itemAt(textIndex)->widget() == d->TextPushButton) { break; } @@ -182,11 +239,11 @@ void ctkThumbnailLabel::setTextPosition(const Qt::Alignment& position) } if (row == 1 && col == 1) { - d->TextLabel->setVisible(false); + d->TextPushButton->setVisible(false); } else { - gridLayout->addWidget(d->TextLabel,row, col); + gridLayout->addWidget(d->TextPushButton,row, col); } } @@ -214,6 +271,20 @@ const QPixmap* ctkThumbnailLabel::pixmap()const return d->OriginalThumbnail.isNull() ? 0 : &(d->OriginalThumbnail); } +//---------------------------------------------------------------------------- +int ctkThumbnailLabel::operationProgress() const +{ + Q_D(const ctkThumbnailLabel); + return d->OperationProgressBar->value(); +} + +//---------------------------------------------------------------------------- +void ctkThumbnailLabel::setOperationProgress(const int &progress) +{ + Q_D(ctkThumbnailLabel); + d->OperationProgressBar->setValue(progress); +} + //---------------------------------------------------------------------------- Qt::TransformationMode ctkThumbnailLabel::transformationMode()const { @@ -278,10 +349,10 @@ QColor ctkThumbnailLabel::selectedColor()const QSize ctkThumbnailLabel::minimumSizeHint()const { Q_D(const ctkThumbnailLabel); - if (d->TextLabel->isVisibleTo(const_cast(this)) && - !d->TextLabel->text().isEmpty()) + if (d->TextPushButton->isVisibleTo(const_cast(this)) && + !d->TextPushButton->text().isEmpty()) { - return d->TextLabel->minimumSizeHint(); + return d->TextPushButton->minimumSizeHint(); } return QSize(); } diff --git a/Libs/Widgets/ctkThumbnailLabel.h b/Libs/Widgets/ctkThumbnailLabel.h index ed22a94ba1..584188c8c3 100644 --- a/Libs/Widgets/ctkThumbnailLabel.h +++ b/Libs/Widgets/ctkThumbnailLabel.h @@ -27,8 +27,13 @@ #include "ctkWidgetsExport.h" +class ctkPushButton; class ctkThumbnailLabelPrivate; +class QFrame; +class QLabel; +class QProgressBar; + /// \ingroup Widgets /// ctkThumbnailLabel is an advanced label that gives control over /// the pixmap size and text location. @@ -49,6 +54,8 @@ class CTK_WIDGETS_EXPORT ctkThumbnailLabel : public QWidget /// Optional pixmap for the label. /// No pixmap by default Q_PROPERTY(QPixmap pixmap READ pixmap WRITE setPixmap) + /// Progress bar status. + Q_PROPERTY(int operationProgress READ operationProgress WRITE setOperationProgress) /// Controls the quality of the resizing of the pixmap. /// Qt::FastTransformation by default Q_PROPERTY(Qt::TransformationMode transformationMode READ transformationMode WRITE setTransformationMode) @@ -64,6 +71,11 @@ class CTK_WIDGETS_EXPORT ctkThumbnailLabel : public QWidget explicit ctkThumbnailLabel(QWidget* parent=0); virtual ~ctkThumbnailLabel(); + Q_INVOKABLE ctkPushButton* textPushButton(); + Q_INVOKABLE QFrame* pixmapFrame(); + Q_INVOKABLE QLabel* pixmapLabel(); + Q_INVOKABLE QProgressBar* operationProgressBar(); + void setText(const QString& text); QString text()const; @@ -73,6 +85,9 @@ class CTK_WIDGETS_EXPORT ctkThumbnailLabel : public QWidget void setPixmap(const QPixmap& pixmap); const QPixmap* pixmap()const; + void setOperationProgress(const int& progress); + int operationProgress()const; + Qt::TransformationMode transformationMode()const; void setTransformationMode(Qt::TransformationMode mode);