From c59d59411278d6d5fb54c21cc9514d3b7e710cf4 Mon Sep 17 00:00:00 2001 From: Davide Punzo Date: Wed, 16 Aug 2023 12:32:38 +0200 Subject: [PATCH] ENH: Add Visual Dicom Browser --- .../ctkDICOMVisualBrowser/CMakeLists.txt | 40 + .../Testing/CMakeLists.txt | 1 + .../Testing/Cpp/CMakeLists.txt | 21 + .../Cpp/ctkDICOMVisualBrowserTest1.cpp | 58 + .../ctkDICOMVisualBrowserMain.cpp | 99 + .../target_libraries.cmake | 9 + CMake/ctkMacroSetupQt.cmake | 6 + CMakeLists.txt | 4 + Libs/Core/CMakeLists.txt | 6 + Libs/Core/ctkAbstractPoolManager.cpp | 35 + Libs/Core/ctkAbstractPoolManager.h | 52 + Libs/Core/ctkAbstractTask.cpp | 118 + Libs/Core/ctkAbstractTask.h | 96 + Libs/Core/ctkAbstractTaskPool.cpp | 35 + Libs/Core/ctkAbstractTaskPool.h | 52 + Libs/Core/ctkErrorLogLevel.h | 20 +- Libs/Core/ctkLogger.cpp | 189 +- Libs/Core/ctkLogger.h | 18 +- Libs/DICOM/Core/CMakeLists.txt | 32 + Libs/DICOM/Core/Resources/ctkDICOMCore.qrc | 13 +- Libs/DICOM/Core/Resources/dicom-schema.sql | 1 + Libs/DICOM/Core/Resources/storescp.cfg | 503 +++ Libs/DICOM/Core/ctkDICOMDatabase.cpp | 593 ++-- Libs/DICOM/Core/ctkDICOMDatabase.h | 38 +- Libs/DICOM/Core/ctkDICOMDatabase_p.h | 6 +- Libs/DICOM/Core/ctkDICOMEcho.cpp | 279 ++ Libs/DICOM/Core/ctkDICOMEcho.h | 91 + Libs/DICOM/Core/ctkDICOMIndexer.cpp | 251 +- Libs/DICOM/Core/ctkDICOMIndexer.h | 10 + Libs/DICOM/Core/ctkDICOMIndexer_p.h | 35 +- Libs/DICOM/Core/ctkDICOMItem.cpp | 26 +- Libs/DICOM/Core/ctkDICOMItem.h | 7 +- Libs/DICOM/Core/ctkDICOMQuery.cpp | 1146 +++++-- Libs/DICOM/Core/ctkDICOMQuery.h | 103 +- Libs/DICOM/Core/ctkDICOMQueryTask.cpp | 284 ++ Libs/DICOM/Core/ctkDICOMQueryTask.h | 136 + Libs/DICOM/Core/ctkDICOMQueryTask_p.h | 57 + Libs/DICOM/Core/ctkDICOMRetrieve.cpp | 738 ++++- Libs/DICOM/Core/ctkDICOMRetrieve.h | 114 +- Libs/DICOM/Core/ctkDICOMRetrieveTask.cpp | 326 ++ Libs/DICOM/Core/ctkDICOMRetrieveTask.h | 118 + Libs/DICOM/Core/ctkDICOMRetrieveTask_p.h | 58 + Libs/DICOM/Core/ctkDICOMServer.cpp | 348 ++ Libs/DICOM/Core/ctkDICOMServer.h | 121 + Libs/DICOM/Core/ctkDICOMStorageListener.cpp | 460 +++ Libs/DICOM/Core/ctkDICOMStorageListener.h | 116 + .../Core/ctkDICOMStorageListenerTask.cpp | 187 ++ Libs/DICOM/Core/ctkDICOMStorageListenerTask.h | 97 + .../Core/ctkDICOMStorageListenerTask_p.h | 49 + Libs/DICOM/Core/ctkDICOMTaskPool.cpp | 1472 +++++++++ Libs/DICOM/Core/ctkDICOMTaskPool.h | 232 ++ Libs/DICOM/Core/ctkDICOMTaskPool_p.h | 82 + Libs/DICOM/Core/ctkDICOMTaskResults.cpp | 349 ++ Libs/DICOM/Core/ctkDICOMTaskResults.h | 129 + Libs/DICOM/Widgets/CMakeLists.txt | 23 + Libs/DICOM/Widgets/Plugins/CMakeLists.txt | 4 + .../ctkDICOMVisualBrowserWidgetPlugin.cpp | 71 + .../ctkDICOMVisualBrowserWidgetPlugin.h | 48 + .../Widgets/Plugins/ctkDICOMWidgetsPlugins.h | 2 + .../Widgets/Resources/UI/Icons/accept.svg | 1 + Libs/DICOM/Widgets/Resources/UI/Icons/add.svg | 1 + .../Widgets/Resources/UI/Icons/cancel.svg | 1 + .../Widgets/Resources/UI/Icons/cloud.svg | 49 + .../Widgets/Resources/UI/Icons/delete.svg | 1 + Libs/DICOM/Widgets/Resources/UI/Icons/dns.svg | 1 + .../Resources/UI/Icons/downloading.svg | 39 + .../Widgets/Resources/UI/Icons/import.svg | 1 + .../DICOM/Widgets/Resources/UI/Icons/load.svg | 1 + .../Widgets/Resources/UI/Icons/loaded.svg | 50 + .../Widgets/Resources/UI/Icons/more_vert.svg | 1 + .../Widgets/Resources/UI/Icons/patient.svg | 1 + .../Widgets/Resources/UI/Icons/query.svg | 1 + .../DICOM/Widgets/Resources/UI/Icons/save.svg | 1 + .../Resources/UI/Icons/text_document.svg | 1 + .../Widgets/Resources/UI/Icons/visible.svg | 50 + .../DICOM/Widgets/Resources/UI/Icons/wait.svg | 1 + .../Widgets/Resources/UI/Icons/warning.svg | 1 + .../Resources/UI/ctkDICOMPatientItemWidget.ui | 365 +++ .../Resources/UI/ctkDICOMQueryWidget.ui | 2 +- .../Resources/UI/ctkDICOMSeriesItemWidget.ui | 101 + .../Resources/UI/ctkDICOMServerNodeWidget2.ui | 407 +++ .../Resources/UI/ctkDICOMStudyItemWidget.ui | 225 ++ .../UI/ctkDICOMVisualBrowserWidget.ui | 965 ++++++ .../Widgets/Resources/UI/ctkDICOMWidget.qrc | 22 + Libs/DICOM/Widgets/ctkDICOMBrowser.cpp | 9 +- .../Widgets/ctkDICOMObjectListWidget.cpp | 10 +- .../Widgets/ctkDICOMPatientItemWidget.cpp | 810 +++++ .../DICOM/Widgets/ctkDICOMPatientItemWidget.h | 158 + .../Widgets/ctkDICOMQueryRetrieveWidget.cpp | 35 +- .../Widgets/ctkDICOMSeriesItemWidget.cpp | 786 +++++ Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h | 133 + .../Widgets/ctkDICOMServerNodeWidget2.cpp | 1252 +++++++ .../DICOM/Widgets/ctkDICOMServerNodeWidget2.h | 124 + .../DICOM/Widgets/ctkDICOMStudyItemWidget.cpp | 685 ++++ Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h | 154 + .../Widgets/ctkDICOMThumbnailGenerator.cpp | 6 +- .../Widgets/ctkDICOMThumbnailGenerator.h | 2 +- .../Widgets/ctkDICOMVisualBrowserWidget.cpp | 2869 +++++++++++++++++ .../Widgets/ctkDICOMVisualBrowserWidget.h | 365 +++ .../Widgets/Resources/UI/ctkThumbnailLabel.ui | 111 +- Libs/Widgets/ctkCheckableHeaderView.cpp | 6 +- Libs/Widgets/ctkThumbnailLabel.cpp | 81 +- Libs/Widgets/ctkThumbnailLabel.h | 14 + 103 files changed, 18515 insertions(+), 998 deletions(-) create mode 100644 Applications/ctkDICOMVisualBrowser/CMakeLists.txt create mode 100644 Applications/ctkDICOMVisualBrowser/Testing/CMakeLists.txt create mode 100644 Applications/ctkDICOMVisualBrowser/Testing/Cpp/CMakeLists.txt create mode 100644 Applications/ctkDICOMVisualBrowser/Testing/Cpp/ctkDICOMVisualBrowserTest1.cpp create mode 100644 Applications/ctkDICOMVisualBrowser/ctkDICOMVisualBrowserMain.cpp create mode 100644 Applications/ctkDICOMVisualBrowser/target_libraries.cmake create mode 100644 Libs/Core/ctkAbstractPoolManager.cpp create mode 100644 Libs/Core/ctkAbstractPoolManager.h create mode 100644 Libs/Core/ctkAbstractTask.cpp create mode 100644 Libs/Core/ctkAbstractTask.h create mode 100644 Libs/Core/ctkAbstractTaskPool.cpp create mode 100644 Libs/Core/ctkAbstractTaskPool.h create mode 100644 Libs/DICOM/Core/Resources/storescp.cfg create mode 100644 Libs/DICOM/Core/ctkDICOMEcho.cpp create mode 100644 Libs/DICOM/Core/ctkDICOMEcho.h create mode 100644 Libs/DICOM/Core/ctkDICOMQueryTask.cpp create mode 100644 Libs/DICOM/Core/ctkDICOMQueryTask.h create mode 100644 Libs/DICOM/Core/ctkDICOMQueryTask_p.h create mode 100644 Libs/DICOM/Core/ctkDICOMRetrieveTask.cpp create mode 100644 Libs/DICOM/Core/ctkDICOMRetrieveTask.h create mode 100644 Libs/DICOM/Core/ctkDICOMRetrieveTask_p.h create mode 100644 Libs/DICOM/Core/ctkDICOMServer.cpp create mode 100644 Libs/DICOM/Core/ctkDICOMServer.h create mode 100644 Libs/DICOM/Core/ctkDICOMStorageListener.cpp create mode 100644 Libs/DICOM/Core/ctkDICOMStorageListener.h create mode 100644 Libs/DICOM/Core/ctkDICOMStorageListenerTask.cpp create mode 100644 Libs/DICOM/Core/ctkDICOMStorageListenerTask.h create mode 100644 Libs/DICOM/Core/ctkDICOMStorageListenerTask_p.h create mode 100644 Libs/DICOM/Core/ctkDICOMTaskPool.cpp create mode 100644 Libs/DICOM/Core/ctkDICOMTaskPool.h create mode 100644 Libs/DICOM/Core/ctkDICOMTaskPool_p.h create mode 100644 Libs/DICOM/Core/ctkDICOMTaskResults.cpp create mode 100644 Libs/DICOM/Core/ctkDICOMTaskResults.h create mode 100644 Libs/DICOM/Widgets/Plugins/ctkDICOMVisualBrowserWidgetPlugin.cpp create mode 100644 Libs/DICOM/Widgets/Plugins/ctkDICOMVisualBrowserWidgetPlugin.h create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/accept.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/add.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/cancel.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/cloud.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/delete.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/dns.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/downloading.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/import.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/load.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/loaded.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/patient.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/query.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/save.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/text_document.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/visible.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/Icons/warning.svg create mode 100644 Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui create mode 100644 Libs/DICOM/Widgets/Resources/UI/ctkDICOMSeriesItemWidget.ui create mode 100644 Libs/DICOM/Widgets/Resources/UI/ctkDICOMServerNodeWidget2.ui create mode 100644 Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui create mode 100644 Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui create mode 100644 Libs/DICOM/Widgets/Resources/UI/ctkDICOMWidget.qrc create mode 100644 Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp create mode 100644 Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h create mode 100644 Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp create mode 100644 Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h create mode 100644 Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp create mode 100644 Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h create mode 100644 Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp create mode 100644 Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h create mode 100644 Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp create mode 100644 Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h 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..82cf259c68 --- /dev/null +++ b/Applications/ctkDICOMVisualBrowser/Testing/Cpp/CMakeLists.txt @@ -0,0 +1,21 @@ +set(KIT ${PROJECT_NAME}) + +create_test_sourcelist(Tests ${KIT}CppTests.cpp + ctkDICOM2Test1.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..4b83205c78 --- /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("DirectoryButton")); + 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..e1e2ebe494 --- /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 + ) \ No newline at end of file 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 a2777f6a52..50263189f2 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..c3024bfe3d 100644 --- a/Libs/Core/CMakeLists.txt +++ b/Libs/Core/CMakeLists.txt @@ -30,6 +30,10 @@ set(KIT_SRCS ctkAbstractQObjectFactory.tpp ctkAbstractLibraryFactory.h ctkAbstractLibraryFactory.tpp + ctkAbstractTaskPool.cpp + ctkAbstractTaskPool.h + ctkAbstractTask.cpp + ctkAbstractTask.h ctkBackTrace.cpp ctkBooleanMapper.cpp ctkBooleanMapper.h @@ -100,6 +104,8 @@ endif() # Headers that should run through moc set(KIT_MOC_SRCS + ctkAbstractTask.h + ctkAbstractTaskPool.h ctkBooleanMapper.h ctkCallback.h ctkCommandLineParser.h diff --git a/Libs/Core/ctkAbstractPoolManager.cpp b/Libs/Core/ctkAbstractPoolManager.cpp new file mode 100644 index 0000000000..10c90b417f --- /dev/null +++ b/Libs/Core/ctkAbstractPoolManager.cpp @@ -0,0 +1,35 @@ +/*========================================================================= + + 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 "ctkAbstractTaskPool.h" + +// -------------------------------------------------------------------------- +ctkAbstractTaskPool::ctkAbstractTaskPool(QObject* parent) + : QObject(parent) +{ +} + +// -------------------------------------------------------------------------- +ctkAbstractTaskPool::~ctkAbstractTaskPool() +{ +} diff --git a/Libs/Core/ctkAbstractPoolManager.h b/Libs/Core/ctkAbstractPoolManager.h new file mode 100644 index 0000000000..67b7bc91c9 --- /dev/null +++ b/Libs/Core/ctkAbstractPoolManager.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 __ctkAbstractTaskPool_h +#define __ctkAbstractTaskPool_h + +// Qt includes +#include + +// CTK includes +#include "ctkCoreExport.h" + +//------------------------------------------------------------------------------ +/// \ingroup Core +class CTK_CORE_EXPORT ctkAbstractTaskPool : public QObject +{ + Q_OBJECT +public: + explicit ctkAbstractTaskPool(QObject* parent = 0); + virtual ~ctkAbstractTaskPool(); + +public Q_SLOTS: + virtual void taskStarted() = 0; + virtual void taskFinished() = 0; + virtual void taskCanceled() = 0; + +private: + Q_DISABLE_COPY(ctkAbstractTaskPool) +}; + + +#endif // ctkAbstractTaskPool_h diff --git a/Libs/Core/ctkAbstractTask.cpp b/Libs/Core/ctkAbstractTask.cpp new file mode 100644 index 0000000000..c645110efd --- /dev/null +++ b/Libs/Core/ctkAbstractTask.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). + +=========================================================================*/ + +#include "ctkAbstractTask.h" + +// -------------------------------------------------------------------------- +ctkAbstractTask::ctkAbstractTask() +{ + this->Stop = false; + this->Running = false; + this->Finished = false; + this->Persistent = false; + this->TaskUID = ""; + this->NumberOfRetry = 0; +} + +//---------------------------------------------------------------------------- +ctkAbstractTask::~ctkAbstractTask() +{ +} + +//---------------------------------------------------------------------------- +void ctkAbstractTask::setTaskUID(const QString &taskUID) +{ + this->TaskUID = taskUID; +} + +//---------------------------------------------------------------------------- +QString ctkAbstractTask::taskUID() const +{ + return this->TaskUID; +} + +//---------------------------------------------------------------------------- +bool ctkAbstractTask::isStopped() const +{ + return this->Stop; +} + +//---------------------------------------------------------------------------- +void ctkAbstractTask::setStop(const bool& stop) +{ + this->Stop = stop; +} + +//---------------------------------------------------------------------------- +bool ctkAbstractTask::isFinished() const +{ + return this->Finished; +} + +//---------------------------------------------------------------------------- +void ctkAbstractTask::setIsFinished(const bool& finished) +{ + if (finished && this->Running) + { + this->Running = false; + } + + this->Finished = finished; +} + +//---------------------------------------------------------------------------- +bool ctkAbstractTask::isRunning() const +{ + return this->Running; +} + +//---------------------------------------------------------------------------- +void ctkAbstractTask::setIsRunning(const bool& running) +{ + this->Running = running; +} + +//---------------------------------------------------------------------------- +bool ctkAbstractTask::isPersistent() const +{ + return this->Persistent; +} + +//---------------------------------------------------------------------------- +void ctkAbstractTask::setIsPersistent(const bool &persistent) +{ + this->Persistent = persistent; +} + +//---------------------------------------------------------------------------- +int ctkAbstractTask::numberOfRetry() const +{ + return this->NumberOfRetry; +} + +//---------------------------------------------------------------------------- +void ctkAbstractTask::setNumberOfRetry(const int& numberOfRetry) +{ + this->NumberOfRetry = numberOfRetry; +} + diff --git a/Libs/Core/ctkAbstractTask.h b/Libs/Core/ctkAbstractTask.h new file mode 100644 index 0000000000..bec5969c4d --- /dev/null +++ b/Libs/Core/ctkAbstractTask.h @@ -0,0 +1,96 @@ +/*========================================================================= + + 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 __ctkAbstractTask_h +#define __ctkAbstractTask_h + +// Qt includes +#include +#include + +// CTK includes +#include "ctkCoreExport.h" + +//------------------------------------------------------------------------------ +/// \ingroup Core +class CTK_CORE_EXPORT ctkAbstractTask : public QObject, public QRunnable +{ + Q_OBJECT + Q_PROPERTY(QString taskUID READ taskUID WRITE setTaskUID); + Q_PROPERTY(bool stop READ isStopped WRITE setStop); + Q_PROPERTY(bool running READ isRunning WRITE setIsRunning); + Q_PROPERTY(bool finished READ isFinished WRITE setIsFinished); + Q_PROPERTY(bool persistent READ isPersistent WRITE setIsPersistent); + Q_PROPERTY(bool numberOfRetry READ numberOfRetry WRITE setNumberOfRetry); + +public: + explicit ctkAbstractTask(); + virtual ~ctkAbstractTask(); + + /// Execute task + virtual void run() = 0; + + /// Task UID + QString taskUID() const; + virtual void setTaskUID(const QString& taskUID); + + /// Stop task + bool isStopped() const; + virtual void setStop(const bool& stop); + + /// Finished + bool isFinished() const; + void setIsFinished(const bool& finished); + + /// Running + bool isRunning() const; + void setIsRunning(const bool& running); + + /// Persistent + bool isPersistent() const; + void setIsPersistent(const bool& persistent); + + /// Number of retries: current counter of how many times + /// the task has been relunched on fails + int numberOfRetry() const; + void setNumberOfRetry(const int& numberOfRetry); + +Q_SIGNALS: + void started(); + void finished(); + void canceled(); + +protected: + QString TaskUID; + bool Stop; + bool Running; + bool Finished; + bool Persistent; + int NumberOfRetry; + +private: + Q_DISABLE_COPY(ctkAbstractTask) +}; + + +#endif // ctkAbstractTask_h diff --git a/Libs/Core/ctkAbstractTaskPool.cpp b/Libs/Core/ctkAbstractTaskPool.cpp new file mode 100644 index 0000000000..10c90b417f --- /dev/null +++ b/Libs/Core/ctkAbstractTaskPool.cpp @@ -0,0 +1,35 @@ +/*========================================================================= + + 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 "ctkAbstractTaskPool.h" + +// -------------------------------------------------------------------------- +ctkAbstractTaskPool::ctkAbstractTaskPool(QObject* parent) + : QObject(parent) +{ +} + +// -------------------------------------------------------------------------- +ctkAbstractTaskPool::~ctkAbstractTaskPool() +{ +} diff --git a/Libs/Core/ctkAbstractTaskPool.h b/Libs/Core/ctkAbstractTaskPool.h new file mode 100644 index 0000000000..220f8f4c06 --- /dev/null +++ b/Libs/Core/ctkAbstractTaskPool.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 __ctkAbstractTaskPool_h +#define __ctkAbstractTaskPool_h + +// Qt includes +#include + +// CTK includes +#include "ctkCoreExport.h" + +//------------------------------------------------------------------------------ +/// \ingroup Core +class CTK_CORE_EXPORT ctkAbstractTaskPool : public QObject +{ + Q_OBJECT +public: + explicit ctkAbstractTaskPool(QObject* parent = 0); + virtual ~ctkAbstractTaskPool(); + +public Q_SLOTS: + virtual void onTaskStarted() = 0; + virtual void onTaskFinished() = 0; + virtual void onTaskCanceled() = 0; + +private: + Q_DISABLE_COPY(ctkAbstractTaskPool) +}; + + +#endif // ctkAbstractTaskPool_h diff --git a/Libs/Core/ctkErrorLogLevel.h b/Libs/Core/ctkErrorLogLevel.h index 4a762da902..7cbe53e37b 100644 --- a/Libs/Core/ctkErrorLogLevel.h +++ b/Libs/Core/ctkErrorLogLevel.h @@ -39,16 +39,16 @@ class CTK_CORE_EXPORT ctkErrorLogLevel : public QObject enum LogLevel { - None = 0x0, - Unknown = 0x1, - Status = 0x2, - Trace = 0x4, - Debug = 0x8, - Info = 0x10, - Warning = 0x20, - Error = 0x40, - Critical = 0x80, - Fatal = 0x100 + Unknown = 0, + Status, + Trace, + Debug, + Info, + Warning, + Error, + Critical, + Fatal, + None, }; Q_DECLARE_FLAGS(LogLevels, LogLevel) diff --git a/Libs/Core/ctkLogger.cpp b/Libs/Core/ctkLogger.cpp index 735bc184df..4a0a345496 100644 --- a/Libs/Core/ctkLogger.cpp +++ b/Libs/Core/ctkLogger.cpp @@ -20,30 +20,38 @@ // Qt includes #include +#include // CTK includes #include - -// Log4Qt includes -//#include -//#include -//#include +#include //----------------------------------------------------------------------------- class ctkLoggerPrivate { public: - //Log4Qt::Logger *Logger; + ctkLoggerPrivate(); + ~ctkLoggerPrivate(); + ctkErrorLogLevel::LogLevel LogLevel; }; +//----------------------------------------------------------------------------- +ctkLoggerPrivate::ctkLoggerPrivate() +{ + this->LogLevel = ctkErrorLogLevel::LogLevel::Warning; +} +//----------------------------------------------------------------------------- +ctkLoggerPrivate::~ctkLoggerPrivate() +{ + +} + //----------------------------------------------------------------------------- ctkLogger::ctkLogger(QString name, QObject* _parent) : Superclass(_parent) , d_ptr(new ctkLoggerPrivate) { Q_UNUSED(name); - //Q_D(ctkLogger); - //d->Logger = Log4Qt::Logger::logger( name.toStdString().c_str()); } //----------------------------------------------------------------------------- @@ -51,154 +59,77 @@ ctkLogger::~ctkLogger() { } -////----------------------------------------------------------------------------- -//void ctkLogger::configure() -//{ -// //Log4Qt::BasicConfigurator::configure(); -//} - //----------------------------------------------------------------------------- void ctkLogger::debug(const QString& s) { - //Q_D(ctkLogger); - //d->Logger->debug(s); - qDebug().nospace() << qUtf8Printable(s); + Q_D(ctkLogger); + if (d->LogLevel <= ctkErrorLogLevel::LogLevel::Debug) + { + qDebug().nospace() << qUtf8Printable(s); + } } //----------------------------------------------------------------------------- void ctkLogger::info(const QString& s) { - //Q_D(ctkLogger); - //d->Logger->info(s); - qDebug().nospace() << qUtf8Printable(s); + Q_D(ctkLogger); + if (d->LogLevel <= ctkErrorLogLevel::LogLevel::Info) + { + qCritical().nospace() << qUtf8Printable(s); + } } //----------------------------------------------------------------------------- void ctkLogger::trace(const QString& s) { - //Q_D(ctkLogger); - //d->Logger->trace(s); - qDebug().nospace() << qUtf8Printable(s); + Q_D(ctkLogger); + if (d->LogLevel <= ctkErrorLogLevel::LogLevel::Trace) + { + qCritical().nospace() << qUtf8Printable(s); + } } //----------------------------------------------------------------------------- void ctkLogger::warn(const QString& s) { - //Q_D(ctkLogger); - //d->Logger->warn(s); - qWarning().nospace() << qUtf8Printable(s); + Q_D(ctkLogger); + if (d->LogLevel <= ctkErrorLogLevel::LogLevel::Warning) + { + qCritical().nospace() << qUtf8Printable(s); + } } //----------------------------------------------------------------------------- void ctkLogger::error(const QString& s) { - //Q_D(ctkLogger); - //d->Logger->error(s); - qCritical().nospace() << qUtf8Printable(s); + Q_D(ctkLogger); + if (d->LogLevel <= ctkErrorLogLevel::LogLevel::Error) + { + qCritical().nospace() << qUtf8Printable(s); + } } //----------------------------------------------------------------------------- void ctkLogger::fatal(const QString& s) { - //Q_D(ctkLogger); - //d->Logger->fatal(s); - qCritical().nospace() << qUtf8Printable(s); + Q_D(ctkLogger); + if (d->LogLevel <= ctkErrorLogLevel::LogLevel::Fatal) + { + qCritical().nospace() << qUtf8Printable(s); + } +} + +//----------------------------------------------------------------------------- +void ctkLogger::setLogLevel(const ctkErrorLogLevel::LogLevel& level) +{ + Q_D(ctkLogger); + d->LogLevel = level; +} + +//----------------------------------------------------------------------------- +ctkErrorLogLevel::LogLevel ctkLogger::logLevel() const +{ + Q_D(const ctkLogger); + return d->LogLevel; } -////----------------------------------------------------------------------------- -//void ctkLogger::setOff() -//{ -// //Q_D(ctkLogger); -// //d->Logger->setLevel(Log4Qt::Level(Log4Qt::Level::OFF_INT)); -//} - -////----------------------------------------------------------------------------- -//void ctkLogger::setDebug() -//{ -// //Q_D(ctkLogger); -// //d->Logger->setLevel(Log4Qt::Level(Log4Qt::Level::DEBUG_INT)); -//} - -////----------------------------------------------------------------------------- -//void ctkLogger::setInfo() -//{ -// //Q_D(ctkLogger); -// //d->Logger->setLevel(Log4Qt::Level(Log4Qt::Level::INFO_INT)); -//} - -////----------------------------------------------------------------------------- -//void ctkLogger::setTrace() -//{ -// //Q_D(ctkLogger); -// //d->Logger->setLevel(Log4Qt::Level(Log4Qt::Level::TRACE_INT)); -//} - -////----------------------------------------------------------------------------- -//void ctkLogger::setWarn() -//{ -// //Q_D(ctkLogger); -// //d->Logger->setLevel(Log4Qt::Level(Log4Qt::Level::WARN_INT)); -//} - -////----------------------------------------------------------------------------- -//void ctkLogger::setError() -//{ -// //Q_D(ctkLogger); -// //d->Logger->setLevel(Log4Qt::Level(Log4Qt::Level::ERROR_INT)); -//} - -////----------------------------------------------------------------------------- -//void ctkLogger::setFatal() -//{ -// //Q_D(ctkLogger); -// //d->Logger->setLevel(Log4Qt::Level(Log4Qt::Level::FATAL_INT)); -//} - -////----------------------------------------------------------------------------- -//bool ctkLogger::isOffEnabled() -//{ -// //Q_D(ctkLogger); //Not sure -// //return d->Logger->isEnabledFor(Log4Qt::Level(Log4Qt::Level::OFF_INT)); -//} - -////----------------------------------------------------------------------------- -//bool ctkLogger::isDebugEnabled() -//{ -// //Q_D(ctkLogger); -// //return d->Logger->isDebugEnabled(); -//} - -////----------------------------------------------------------------------------- -//bool ctkLogger::isInfoEnabled() -//{ -// //Q_D(ctkLogger); -// //return d->Logger->isInfoEnabled(); -//} - -////----------------------------------------------------------------------------- -//bool ctkLogger::isTraceEnabled() -//{ -// //Q_D(ctkLogger); -// //return d->Logger->isTraceEnabled(); -//} - -////----------------------------------------------------------------------------- -//bool ctkLogger::isWarnEnabled() -//{ -// //Q_D(ctkLogger); -// //return d->Logger->isWarnEnabled(); -//} - -////----------------------------------------------------------------------------- -//bool ctkLogger::isErrorEnabled() -//{ -// //Q_D(ctkLogger); -// //return d->Logger->isErrorEnabled(); -//} - -////----------------------------------------------------------------------------- -//bool ctkLogger::isFatalEnabled() -//{ -// //Q_D(ctkLogger); -// //return d->Logger->isFatalEnabled(); -//} diff --git a/Libs/Core/ctkLogger.h b/Libs/Core/ctkLogger.h index 8f95e27000..91098449e1 100644 --- a/Libs/Core/ctkLogger.h +++ b/Libs/Core/ctkLogger.h @@ -27,11 +27,10 @@ // CTK includes #include #include "ctkCoreExport.h" +#include "ctkErrorLogLevel.h" class ctkLoggerPrivate; -/// \deprecated This class was a wrapper around Log4Qt. Since Log4Qt dependency has been -/// removed, it is advised to use qDebug(), qWarning() and qCritical() instead. /// \ingroup Core class CTK_CORE_EXPORT ctkLogger : public QObject { @@ -42,12 +41,15 @@ class CTK_CORE_EXPORT ctkLogger : public QObject explicit ctkLogger(QString name, QObject* parent = 0); virtual ~ctkLogger (); - void debug(const QString& s); - void info(const QString& s); - void trace(const QString& s); - void warn(const QString& s); - void error(const QString& s); - void fatal(const QString& s); + Q_INVOKABLE void debug(const QString& s); + Q_INVOKABLE void info(const QString& s); + Q_INVOKABLE void trace(const QString& s); + Q_INVOKABLE void warn(const QString& s); + Q_INVOKABLE void error(const QString& s); + Q_INVOKABLE void fatal(const QString& s); + + Q_INVOKABLE void setLogLevel(const ctkErrorLogLevel::LogLevel& level); + Q_INVOKABLE ctkErrorLogLevel::LogLevel logLevel() const; protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Core/CMakeLists.txt b/Libs/DICOM/Core/CMakeLists.txt index 80a5422544..2e69c01036 100644 --- a/Libs/DICOM/Core/CMakeLists.txt +++ b/Libs/DICOM/Core/CMakeLists.txt @@ -16,6 +16,8 @@ set(KIT_SRCS ctkDICOMItem.h ctkDICOMDisplayedFieldGenerator.cpp ctkDICOMDisplayedFieldGenerator.h + ctkDICOMEcho.cpp + ctkDICOMEcho.h ctkDICOMFilterProxyModel.cpp ctkDICOMFilterProxyModel.h ctkDICOMIndexer.cpp @@ -29,8 +31,26 @@ set(KIT_SRCS ctkDICOMPersonName.h ctkDICOMQuery.cpp ctkDICOMQuery.h + ctkDICOMQueryTask.cpp + ctkDICOMQueryTask.h + ctkDICOMQueryTask_p.h ctkDICOMRetrieve.cpp ctkDICOMRetrieve.h + ctkDICOMRetrieveTask.cpp + ctkDICOMRetrieveTask.h + ctkDICOMRetrieveTask_p.h + ctkDICOMServer.cpp + ctkDICOMServer.h + ctkDICOMStorageListener.cpp + ctkDICOMStorageListener.h + ctkDICOMStorageListenerTask.cpp + ctkDICOMStorageListenerTask.h + ctkDICOMStorageListenerTask_p.h + ctkDICOMTaskPool.cpp + ctkDICOMTaskPool.h + ctkDICOMTaskPool_p.h + ctkDICOMTaskResults.cpp + ctkDICOMTaskResults.h ctkDICOMTester.cpp ctkDICOMTester.h ctkDICOMUtil.cpp @@ -66,12 +86,24 @@ set(KIT_MOC_SRCS ctkDICOMDisplayedFieldGenerator.h ctkDICOMDisplayedFieldGenerator_p.h ctkDICOMDisplayedFieldGeneratorRuleFactory.h + ctkDICOMEcho.h ctkDICOMIndexer.h ctkDICOMIndexer_p.h ctkDICOMFilterProxyModel.h ctkDICOMModel.h ctkDICOMQuery.h + ctkDICOMQueryTask.h + ctkDICOMQueryTask_p.h ctkDICOMRetrieve.h + ctkDICOMRetrieveTask.h + ctkDICOMRetrieveTask_p.h + ctkDICOMServer.h + ctkDICOMStorageListener.h + ctkDICOMStorageListenerTask.h + ctkDICOMStorageListenerTask_p.h + ctkDICOMTaskPool.h + ctkDICOMTaskPool_p.h + ctkDICOMTaskResults.h ctkDICOMTester.h ) diff --git a/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc b/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc index e9bd48459e..8ec68f4abd 100644 --- a/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc +++ b/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc @@ -1,8 +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 0f9d3da5b7..1f0bd35818 100644 --- a/Libs/DICOM/Core/Resources/dicom-schema.sql +++ b/Libs/DICOM/Core/Resources/dicom-schema.sql @@ -73,6 +73,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/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index ee0300b80b..8609de672d 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -1,4 +1,4 @@ -/*========================================================================= +/*========================================================================= Library: CTK @@ -36,6 +36,7 @@ #include "ctkDICOMDatabase_p.h" #include "ctkDICOMAbstractThumbnailGenerator.h" #include "ctkDICOMItem.h" +#include "ctkDICOMTaskResults.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) @@ -167,10 +167,7 @@ bool ctkDICOMDatabasePrivate::loggedExec(QSqlQuery& query, const QString& queryS } 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); } @@ -285,10 +279,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()) { @@ -323,13 +314,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 = ?"); @@ -342,15 +330,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; } @@ -380,10 +365,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; } } @@ -398,10 +380,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) ); @@ -443,10 +422,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; } @@ -459,17 +435,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) ); @@ -519,10 +489,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; } @@ -581,20 +548,18 @@ void ctkDICOMDatabasePrivate::precacheTags(const ctkDICOMItem& dataset, const QS { value = dataset.GetAllElementValuesAsString(tagKey); } + sopInstanceUIDs << sopInstanceUID; tags << tag; 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); @@ -659,10 +624,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); @@ -674,10 +636,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; @@ -687,7 +646,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(); @@ -754,20 +712,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)); @@ -775,10 +728,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); @@ -790,10 +740,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); } @@ -834,7 +781,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); @@ -846,7 +792,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 @@ -871,7 +816,7 @@ bool ctkDICOMDatabasePrivate::uidsForDataSet(QString& patientsName, QString& pat } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& filePath, bool storeFile, bool generateThumbnail) +void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, QString filePath, bool storeFile, bool generateThumbnail) { Q_Q(ctkDICOMDatabase); @@ -881,20 +826,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) @@ -918,10 +862,15 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& return; } + if (filePath.contains("server://") && storeFile) + { + filePath = ""; + } + // Store a copy of the dataset QString storedFilePath = filePath; if (storeFile && !seriesInstanceUID.isEmpty() && !q->isInMemory()) - { + { if (!this->storeDatasetFile(dataset, filePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID, storedFilePath)) { logger.error("Error saving file: " + filePath); @@ -930,20 +879,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(); } @@ -962,23 +907,32 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& storedFilePathInDatabase = storedFilePath; } - QSqlQuery insertImageStatement(Database); - insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )"); + QSqlQuery insertImageStatement(this->Database); + insertImageStatement.prepare("INSERT INTO Images " + "( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) " + "VALUES ( ?, ?, ?, ? )"); + insertImageStatement.addBindValue(sopInstanceUID); insertImageStatement.addBindValue(storedFilePathInDatabase); 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) @@ -993,138 +947,238 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::insert(const QList& indexingResults) +void ctkDICOMDatabase::insert(QList taskResultsList) { Q_D(ctkDICOMDatabase); + bool databaseWasChanged = false; d->TagCacheDatabase.transaction(); d->Database.transaction(); QDir databaseDirectory(this->databaseDirectory()); - foreach(const ctkDICOMDatabase::IndexingResult & indexingResult, indexingResults) + foreach (ctkDICOMTaskResults* taskResults, taskResultsList) { - const ctkDICOMItem& dataset = *indexingResult.dataset.data(); - QString filePath = indexingResult.filePath; + ctkDICOMTaskResults::TaskType typeOfTask = taskResults->typeOfTask(); + QString filePath = taskResults->filePath(); bool generateThumbnail = false; // thumbnail will be generated when needed, don't slow down import with that - bool storeFile = indexingResult.copyFile; + bool storeFile = taskResults->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) + QMap datasets = taskResults->datasets(); + for(QString key : datasets.keys()) { - // 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)) + ctkDICOMItem* dataset = datasets.value(key); + if (!dataset) { - // error occurred, message is already logged continue; } - } + QString patientID, patientName, studyInstanceUID, seriesInstanceUID;; + patientName = dataset->GetElementAsString(DCM_PatientName); + patientID = dataset->GetElementAsString(DCM_PatientID); + studyInstanceUID = dataset->GetElementAsString(DCM_StudyInstanceUID); + seriesInstanceUID = dataset->GetElementAsString(DCM_SeriesInstanceUID); - if (datasetInDatabase) - { - if (datasetUpToDate) + if (patientID.isEmpty()) { - continue; + if (typeOfTask == ctkDICOMTaskResults::TaskType::QueryPatients) + { + patientID = key; + } + else if (typeOfTask == ctkDICOMTaskResults::TaskType::QueryStudies || + typeOfTask == ctkDICOMTaskResults::TaskType::QuerySeries || + typeOfTask == ctkDICOMTaskResults::TaskType::QueryInstances || + typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveStudy || + typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveSeries || + typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveSOPInstance) + { + patientID = taskResults->patientID(); + } + + dataset->SetElementAsString(DCM_PatientID, patientID); } - // File is updated, delete record and re-index - if (!d->removeImage(sopInstanceUID)) + + if (studyInstanceUID.isEmpty()) { - logger.error("Failed to insert file into database (cannot update pre-existing item): " + filePath); + if (typeOfTask == ctkDICOMTaskResults::TaskType::QueryStudies) + { + studyInstanceUID = key; + } + else if (typeOfTask == ctkDICOMTaskResults::TaskType::QuerySeries || + typeOfTask == ctkDICOMTaskResults::TaskType::QueryInstances || + typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveStudy || + typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveSeries || + typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveSOPInstance) + { + studyInstanceUID = taskResults->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 (typeOfTask == ctkDICOMTaskResults::TaskType::QuerySeries) + { + seriesInstanceUID = key; + } + else if (typeOfTask == ctkDICOMTaskResults::TaskType::QueryInstances || + typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveSeries || + typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveSOPInstance) + { + seriesInstanceUID = taskResults->seriesInstanceUID(); + } + + dataset->SetElementAsString(DCM_SeriesInstanceUID, seriesInstanceUID); + } + + if (patientID.isEmpty()) + { + logger.error("ctkDICOMDatabase::insert: dataset has no patientID"); 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; - } + if (patientName.isEmpty()) + { + logger.error("ctkDICOMDatabase::insert: dataset has no patientName"); + 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)) + if (studyInstanceUID.isEmpty() && typeOfTask != ctkDICOMTaskResults::TaskType::QueryPatients) { + logger.error("ctkDICOMDatabase::insert: dataset has no studyInstanceUID"); continue; } - } - if (d->insertPatientStudySeries(dataset, patientID, patientsName)) - { - databaseWasChanged = true; - } + if (typeOfTask == ctkDICOMTaskResults::TaskType::QueryInstances) + { + filePath = "server://" + taskResults->connectionName() + + "_" + studyInstanceUID + + "_" + seriesInstanceUID + + "_" + key; + } - 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) + // Check to see if the file has already been loaded + QString sopInstanceUID(dataset->GetElementAsString(DCM_SOPInstanceUID)); + bool datasetInDatabase = false; + bool datasetUpToDate = false; + if (taskResults->overwriteExistingDataset()) { - unsigned short group, element; - this->tagToGroupElement(tag, group, element); - DcmTagKey tagKey(group, element); - QString value; - if (d->TagsToExcludeFromStorage.contains(tag)) + // 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)) { - if (dataset.TagExists(tagKey)) - { - value = ValueIsNotStored; - } - else - { - value = TagNotInInstance; - } + // error occurred, message is already logged + continue; } - else + } + + if (datasetInDatabase) + { + if (datasetUpToDate) { - value = dataset.GetAllElementValuesAsString(tagKey); + continue; } - insertTags.bindValue(1, tag); - if (value.isEmpty()) + // File is updated, delete record and re-index + if (!d->removeImage(sopInstanceUID)) { - insertTags.bindValue(2, TagNotInInstance); + logger.error("Failed to insert file into database (cannot update pre-existing item): " + filePath); + continue; } - else + } + + if (filePath.contains("server://") && storeFile) + { + filePath = ""; + } + + // Store a copy of the dataset + QString storedFilePath = filePath; + if (storeFile && !seriesInstanceUID.isEmpty() && !this->isInMemory()) + { + if (!d->storeDatasetFile(*dataset, filePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID, storedFilePath)) { - insertTags.bindValue(2, value); + continue; } - insertTags.exec(); } - // Insert image files - QSqlQuery insertImageStatement(d->Database); - insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )"); - insertImageStatement.addBindValue(sopInstanceUID); - insertImageStatement.addBindValue(d->internalPathFromAbsolute(storedFilePath)); - insertImageStatement.addBindValue(seriesInstanceUID); - insertImageStatement.addBindValue(QDateTime::currentDateTime()); - insertImageStatement.exec(); - emit instanceAdded(sopInstanceUID); - if (d->LoggedExecVerbose) + if (d->insertPatientStudySeries(*dataset, patientID, patientName)) { - qDebug() << "Instance Added"; + databaseWasChanged = true; } - databaseWasChanged = true; - if (generateThumbnail) + if (!sopInstanceUID.isEmpty() && !seriesInstanceUID.isEmpty() && !storedFilePath.isEmpty()) { - d->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); + 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', 'SeriesInstanceUID', 'InsertTimestamp' ) " + "VALUES ( ?, ?, ?, ? )"); + + insertImageStatement.addBindValue(sopInstanceUID); + insertImageStatement.addBindValue(storedFilePathInDatabase); + 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); + } } } } @@ -1136,6 +1190,62 @@ void ctkDICOMDatabase::insert(const QList& ind { emit this->databaseChanged(); } + + // for queries and retrieve instance, we need to notify when the data have been indexed, + // so the GUI will update and taskPool will delete the query task. + foreach (ctkDICOMTaskResults* taskResults, taskResultsList) + { + if (taskResults->typeOfTask() == ctkDICOMTaskResults::QueryPatients || + taskResults->typeOfTask() == ctkDICOMTaskResults::QueryStudies || + taskResults->typeOfTask() == ctkDICOMTaskResults::QuerySeries || + taskResults->typeOfTask() == ctkDICOMTaskResults::QueryInstances || + taskResults->typeOfTask() == ctkDICOMTaskResults::RetrieveSOPInstance || + taskResults->typeOfTask() == ctkDICOMTaskResults::StoreSOPInstance) + { + emit progressTaskDetail(taskResults); + } + } + + // For retrieve series, we need to notify when the full series has been indexed, + // so the GUI will update and taskPool will delete the retrieve task. + // NOTE: a retrieve task generate N taskResults, where N is the number of frames. + // Here we need to notify only once (since the progress bar is updated with progressBarTaskDetail + // signal in the retrieve handler as soon as the frame is fetched by the logic). + QStringList notifyList; + foreach (ctkDICOMTaskResults* taskResults, taskResultsList) + { + if (taskResults->typeOfTask() != ctkDICOMTaskResults::RetrieveSeries) + { + continue; + } + + QString taskUID = taskResults->taskUID(); + if (notifyList.contains(taskUID)) + { + continue; + } + + notifyList.append(taskUID); + emit progressTaskDetail(taskResults); + } + + notifyList.clear(); + foreach (ctkDICOMTaskResults* taskResults, taskResultsList) + { + if (taskResults->typeOfTask() != ctkDICOMTaskResults::StoreSeries) + { + continue; + } + + QString taskUID = taskResults->taskUID(); + if (notifyList.contains(taskUID)) + { + continue; + } + + notifyList.append(taskUID); + emit progressTaskDetail(taskResults); + } } //------------------------------------------------------------------------------ @@ -1433,6 +1543,11 @@ bool ctkDICOMDatabasePrivate::applyDisplayedFieldsChanges( QMapdatabaseDirectory()); } @@ -2283,7 +2398,7 @@ QString ctkDICOMDatabase::instanceValue(QString sopInstanceUID, QString tag) // Read value from file QString filePath = this->fileForInstance(sopInstanceUID); - if (filePath.isEmpty()) + if (filePath.isEmpty() || filePath.contains("server://")) { return ""; } @@ -2303,6 +2418,11 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) { Q_D(ctkDICOMDatabase); + if (fileName.isEmpty()) + { + return ""; + } + // Read from cache, if available QString sopInstanceUID = this->instanceForFile(fileName); QString value = this->cachedTag(sopInstanceUID, tag); @@ -2315,6 +2435,11 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) return value; } + if (fileName.contains("server://")) + { + return ""; + } + // Read value from file value = d->readValueFromFile(fileName, sopInstanceUID, tag); return value; @@ -2323,7 +2448,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); } @@ -2344,7 +2468,7 @@ bool ctkDICOMDatabase::instanceValueExists(const QString sopInstanceUID, const Q // Read value from file QString filePath = this->fileForInstance(sopInstanceUID); - if (filePath.isEmpty()) + if (filePath.isEmpty() || filePath.contains("server://")) { return false; } @@ -2363,6 +2487,12 @@ bool ctkDICOMDatabase::instanceValueExists(const QString sopInstanceUID, const u bool ctkDICOMDatabase::fileValueExists(const QString fileName, QString tag) { Q_D(ctkDICOMDatabase); + + if (fileName.isEmpty()) + { + return false; + } + QString sopInstanceUID = this->instanceForFile(fileName); QString value = this->cachedTag(sopInstanceUID, tag); if (value == TagNotInInstance || value == ValueIsEmptyString) @@ -2374,6 +2504,11 @@ bool ctkDICOMDatabase::fileValueExists(const QString fileName, QString tag) return true; } + if (fileName.contains("server://")) + { + return false; + } + // Read value from file value = d->readValueFromFile(fileName, sopInstanceUID, tag); return (value != TagNotInInstance && value != ValueIsEmptyString); @@ -2382,7 +2517,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); } @@ -2444,14 +2578,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) { @@ -2466,10 +2592,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; @@ -2577,7 +2700,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); @@ -2637,15 +2760,12 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clear QString thumbnailPath = fileToRemove.second; // check that the file is below our internal storage - if (QFileInfo(dbFilePath).isRelative()) + if (QFileInfo(dbFilePath).isRelative() && !dbFilePath.contains("server://")) { QString absPath = d->absolutePathFromInternal(dbFilePath); if (QFile(absPath).remove()) { - if (d->LoggedExecVerbose) - { - logger.debug("Removed file " + absPath); - } + logger.debug("Removed file " + absPath); QString fileFolder = QFileInfo(absPath).absoluteDir().path(); if (foldersToRemove.isEmpty() || foldersToRemove.last() != fileFolder) { @@ -2680,7 +2800,10 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clear QDir().rmpath(folderToRemove); } - this->cleanup(); + if (cleanup) + { + this->cleanup(); + } d->resetLastInsertedValues(); @@ -2706,7 +2829,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); @@ -2723,7 +2846,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; } @@ -2733,7 +2856,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); @@ -2750,7 +2873,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; } @@ -3260,3 +3383,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 98ce3aa369..c1ca7a1740 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 ctkDICOMTaskResults; /// \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,17 +64,11 @@ 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: - struct IndexingResult - { - QString filePath; - QSharedPointer dataset; - bool copyFile; - bool overwriteExistingDataset; - }; - explicit ctkDICOMDatabase(QObject *parent = 0); explicit ctkDICOMDatabase(QString databaseFile); virtual ~ctkDICOMDatabase(); @@ -250,17 +244,12 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// Q_INVOKABLE void insert( const ctkDICOMItem& ctkDataset, bool storeFile, bool generateThumbnail); - void insert ( DcmItem *item, - bool storeFile = true, bool generateThumbnail = true); + void insert ( DcmItem *item, bool storeFile = true, bool generateThumbnail = true); Q_INVOKABLE void insert ( const QString& filePath, 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 taskResultsList); /// 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. @@ -306,9 +295,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. @@ -394,6 +383,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. @@ -444,6 +441,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// Indicate displayed fields update finished void displayedFieldsUpdated(); + /// Indicate that data from a Task Result has been inserted + void progressTaskDetail(ctkDICOMTaskResults*); + protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Core/ctkDICOMDatabase_p.h b/Libs/DICOM/Core/ctkDICOMDatabase_p.h index e76b94480a..299409e132 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); @@ -83,7 +82,7 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabasePrivate /// Dataset must be set always /// \param filePath It has to be set if this is an import of an actual file - void insert ( const ctkDICOMItem& dataset, const QString& filePath, bool storeFile = true, bool generateThumbnail = true); + void insert ( const ctkDICOMItem& dataset, QString filePath, bool storeFile = true, bool generateThumbnail = true); /// Copy the complete list of files to an extra table QStringList allFilesInDatabase(); @@ -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..7cb7525ccc --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMEcho.cpp @@ -0,0 +1,279 @@ +/*========================================================================= + + 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 +#include +#include +#include +#include +#include +#include /* for class OFStandard */ +#include /* for class DicomDirInterface */ + +static ctkLogger logger ( "org.commontk.dicom.DICOMEcho" ); + +//------------------------------------------------------------------------------ +class ctkDICOMEchoPrivate +{ +public: + ctkDICOMEchoPrivate(); + ~ctkDICOMEchoPrivate(); + + QString ConnectionName; + QString CallingAETitle; + QString CalledAETitle; + QString Host; + int Port; + DcmSCU SCU; +}; + +//------------------------------------------------------------------------------ +// ctkDICOMEchoPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMEchoPrivate::ctkDICOMEchoPrivate() +{ + this->Port = 0; + + this->SCU.setACSETimeout(3); + this->SCU.setConnectionTimeout(3); +} + +//------------------------------------------------------------------------------ +ctkDICOMEchoPrivate::~ctkDICOMEchoPrivate() +{ +} + +//------------------------------------------------------------------------------ +// ctkDICOMEcho methods + +//------------------------------------------------------------------------------ +ctkDICOMEcho::ctkDICOMEcho(QObject* parentObject) + : QObject(parentObject) + , d_ptr(new ctkDICOMEchoPrivate) +{ + Q_D(ctkDICOMEcho); + + d->SCU.setVerbosePCMode(false); + + this->setDCMTKLogLevel(logger.logLevel()); +} + +//------------------------------------------------------------------------------ +ctkDICOMEcho::~ctkDICOMEcho() +{ +} + +//----------------------------------------------------------------------------- +void ctkDICOMEcho::setDCMTKLogLevel(const ctkErrorLogLevel::LogLevel& level) +{ + OFLogger::LogLevel dcmtkLogLevel = OFLogger::OFF_LOG_LEVEL; + if (level == ctkErrorLogLevel::LogLevel::Fatal) + { + dcmtkLogLevel = OFLogger::FATAL_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Critical || + level == ctkErrorLogLevel::LogLevel::Error) + { + dcmtkLogLevel = OFLogger::ERROR_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Warning) + { + dcmtkLogLevel = OFLogger::WARN_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Info) + { + dcmtkLogLevel = OFLogger::INFO_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Debug) + { + dcmtkLogLevel = OFLogger::DEBUG_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Trace || + level == ctkErrorLogLevel::LogLevel::Status) + { + dcmtkLogLevel = OFLogger::TRACE_LOG_LEVEL; + } + + OFLog::configure(dcmtkLogLevel); +} + +//----------------------------------------------------------------------------- +ctkErrorLogLevel::LogLevel ctkDICOMEcho::DCMTKLogLevel() const +{ + return logger.logLevel(); +} + +/// Set methods for connectivity +//------------------------------------------------------------------------------ +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(const 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.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"); + + 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"); + 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; + } + + Uint16 presentationContext = 0; + // Check for any accepted presentation context for ECHO 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"); + } + else + { + logger.info("Found useful presentation context"); + } + + logger.debug("Seding Echo"); + OFCondition status = d->SCU.sendECHORequest(presentationContext); + 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..c9b60ffcc5 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMEcho.h @@ -0,0 +1,91 @@ +/*========================================================================= + + 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" +#include "ctkErrorLogLevel.h" + +class ctkDICOMEchoPrivate; +class ctkDICOMTaskResults; + +/// \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(); + + /// Set methods for connectivity. + /// Empty by default + void setConnectionName(const QString& connectionName); + QString connectionName() const; + /// Empty by default + void setCallingAETitle(const QString& callingAETitle); + QString callingAETitle()const; + /// Empty by default + void setCalledAETitle(const QString& calledAETitle); + QString calledAETitle() const; + /// 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. + /// 0 by default. + void setPort(int port); + int port() const; + /// connection timeout, default 3 sec. + void setConnectionTimeout(const int timeout); + int connectionTimeout() const; + + /// Log level for dcmtk. Default: Error. + Q_INVOKABLE void setDCMTKLogLevel(const ctkErrorLogLevel::LogLevel& level); + Q_INVOKABLE ctkErrorLogLevel::LogLevel DCMTKLogLevel() 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..e9a9fdd80a 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer.cpp +++ b/Libs/DICOM/Core/ctkDICOMIndexer.cpp @@ -54,7 +54,7 @@ //------------------------------------------------------------------------------ -static ctkLogger logger("org.commontk.dicom.DICOMIndexer" ); +static ctkLogger logger("org.commontk.dicom.DICOMIndexer"); /// How many files to parse before inserting results into the database. /// Increasing cache size increases maximum memory usage, very low cache size @@ -80,6 +80,7 @@ ctkDICOMIndexerPrivateWorker::ctkDICOMIndexerPrivateWorker(DICOMIndexingQueue* q ctkDICOMIndexerPrivateWorker::~ctkDICOMIndexerPrivateWorker() { this->RequestQueue->setStopRequested(true); + this->RequestQueue->clear(); } //------------------------------------------------------------------------------ @@ -87,6 +88,7 @@ void ctkDICOMIndexerPrivateWorker::start() { emit updatingDatabase(true); ctkDICOMDatabase database; + connect(&database, SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), this, SIGNAL(progressTaskDetail(ctkDICOMTaskResults*))); database.openDatabase(this->RequestQueue->databaseFilename()); database.setTagsToPrecache(this->RequestQueue->tagsToPrecache()); database.setTagsToExcludeFromStorage(this->RequestQueue->tagsToExcludeFromStorage()); @@ -107,6 +109,7 @@ void ctkDICOMIndexerPrivateWorker::start() // Make a local copy to avoid the need of frequent locking this->RequestQueue->modifiedTimeForFilepath(this->ModifiedTimeForFilepath); this->CompletedRequestCount = 0; + do { if (this->RequestQueue->isStopRequested()) @@ -138,8 +141,10 @@ 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.info( + QString("DICOM indexer has updated display fields for %1 files [%6s]") + .arg(imagesCountAfter-imagesCountBefore).arg(QString::number(elapsedTimeInSeconds, 'f',6)) + ); // restart if new requests has been queued during displayed fields update } while (!this->RequestQueue->isEmpty()); @@ -186,6 +191,8 @@ void ctkDICOMIndexerPrivateWorker::processIndexingRequest(DICOMIndexingQueue::In int currentFileIndex = 0; int alreadyAddedFileCount = 0; QStringList alreadyAddedFiles; + + QList indexingResults; foreach(const QString& filePath, indexingRequest.inputFilesPath) { int percent = int(this->TimePercentageIndexing * (this->CompletedRequestCount + double(currentFileIndex++) / double(indexingRequest.inputFilesPath.size())) @@ -206,20 +213,20 @@ void ctkDICOMIndexerPrivateWorker::processIndexingRequest(DICOMIndexingQueue::In } this->ModifiedTimeForFilepath[filePath] = fileModifiedTime; - ctkDICOMDatabase::IndexingResult indexingResult; - indexingResult.dataset = QSharedPointer(new ctkDICOMItem); - indexingResult.dataset->InitializeFromFile(filePath); - if (indexingResult.dataset->IsInitialized()) + ctkDICOMTaskResults* indexingResult = new ctkDICOMTaskResults; + indexingResults.append(indexingResult); + indexingResult->setFilePath(filePath); + ctkDICOMItem* dataset = indexingResult->dataset(); + if (dataset && dataset->IsInitialized()) { - indexingResult.filePath = filePath; - indexingResult.copyFile = indexingRequest.copyFile; - indexingResult.overwriteExistingDataset = datasetAlreadyInDatabase; - int resultsCount = this->RequestQueue->pushIndexingResult(indexingResult); + indexingResult->setTypeOfTask(ctkDICOMTaskResults::TaskType::FileIndexing); + indexingResult->setCopyFile(indexingRequest.copyFile); + indexingResult->setOverwriteExistingDataset(datasetAlreadyInDatabase); + int resultsCount = this->RequestQueue->pushTaskResult(indexingResult); if (resultsCount >= REQUEST_RESULTS_CACHE_MAXIMUM_SIZE) { emit progressStep(ctkDICOMIndexer::tr("Updating database fields")); - this->writeIndexingResultsToDatabase(database); - emit progressStep(ctkDICOMIndexer::tr("Parsing DICOM files")); + this->writeTaskResultsToDatabase(database); } } else @@ -235,7 +242,7 @@ void ctkDICOMIndexerPrivateWorker::processIndexingRequest(DICOMIndexingQueue::In if (alreadyAddedFileCount > 0) { - logger.debug( + logger.info( QString("Skipped %1 files that were already in the database: %2...") .arg(alreadyAddedFileCount) .arg(alreadyAddedFiles.join(", ")) @@ -245,26 +252,18 @@ void ctkDICOMIndexerPrivateWorker::processIndexingRequest(DICOMIndexingQueue::In if (this->RequestQueue->isIndexingRequestsEmpty()) { emit progressStep(ctkDICOMIndexer::tr("Updating database fields")); - this->writeIndexingResultsToDatabase(database); - emit progressStep(ctkDICOMIndexer::tr("Parsing DICOM files")); + emit this->progress(80); + this->writeTaskResultsToDatabase(database); + emit this->progress(100); + qDeleteAll(indexingResults); + indexingResults.clear(); } - - float elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0; - qDebug() << QString("DICOM indexer has successfully processed %1 files [%2s]") - .arg(currentFileIndex).arg(QString::number(elapsedTimeInSeconds, 'f', 2)); } //------------------------------------------------------------------------------ -void ctkDICOMIndexerPrivateWorker::writeIndexingResultsToDatabase(ctkDICOMDatabase& database) +void ctkDICOMIndexerPrivateWorker::writeTaskResultsToDatabase(ctkDICOMDatabase& database) { - QList indexingResults; - this->RequestQueue->popAllIndexingResults(indexingResults); - if (indexingResults.isEmpty()) - { - return; - } - #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QElapsedTimer timeProbe; #else @@ -272,16 +271,30 @@ void ctkDICOMIndexerPrivateWorker::writeIndexingResultsToDatabase(ctkDICOMDataba #endif timeProbe.start(); - this->NumberOfInstancesToInsert = indexingResults.size(); + this->NumberOfInstancesToInsert = this->RequestQueue->taskResultsCount(); + int initialTaskResultToInsert = this->NumberOfInstancesToInsert; this->NumberOfInstancesInserted = 0; - database.insert(indexingResults); + + + QList taskResultsList; + while (this->NumberOfInstancesToInsert > 0) + { + ctkDICOMTaskResults* taskResults = this->RequestQueue->popTaskResult(); + taskResultsList.append(taskResults); + this->NumberOfInstancesToInsert = this->RequestQueue->taskResultsCount(); + this->NumberOfInstancesInserted++; + } + + database.insert(taskResultsList); + this->NumberOfInstancesToInsert = 0; 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.info( + QString("DICOM indexer has inserted %1 datasets [%6s]") + .arg(initialTaskResultToInsert).arg(QString::number(elapsedTimeInSeconds, 'f', 6)) + ); } //------------------------------------------------------------------------------ @@ -304,10 +317,11 @@ ctkDICOMIndexerPrivate::ctkDICOMIndexerPrivate(ctkDICOMIndexer& o) connect(worker, &ctkDICOMIndexerPrivateWorker::progress, q_ptr, &ctkDICOMIndexer::progress); connect(worker, &ctkDICOMIndexerPrivateWorker::progressDetail, q_ptr, &ctkDICOMIndexer::progressDetail); connect(worker, &ctkDICOMIndexerPrivateWorker::progressStep, q_ptr, &ctkDICOMIndexer::progressStep); + connect(worker, &ctkDICOMIndexerPrivateWorker::progressTaskDetail, q_ptr, &ctkDICOMIndexer::progressTaskDetail); connect(worker, &ctkDICOMIndexerPrivateWorker::updatingDatabase, q_ptr, &ctkDICOMIndexer::updatingDatabase); connect(worker, &ctkDICOMIndexerPrivateWorker::indexingComplete, q_ptr, &ctkDICOMIndexer::indexingComplete); - this->WorkerThread.start(); + this->WorkerThread.start(QThread::HighestPriority); } //------------------------------------------------------------------------------ @@ -323,7 +337,6 @@ ctkDICOMIndexerPrivate::~ctkDICOMIndexerPrivate() //------------------------------------------------------------------------------ void ctkDICOMIndexerPrivate::pushIndexingRequest(const DICOMIndexingQueue::IndexingRequest& request) { - Q_Q(ctkDICOMIndexer); this->RequestQueue.pushIndexingRequest(request); if (!this->RequestQueue.isIndexing()) { @@ -336,6 +349,41 @@ void ctkDICOMIndexerPrivate::pushIndexingRequest(const DICOMIndexingQueue::Index } } +//------------------------------------------------------------------------------ +void ctkDICOMIndexerPrivate::pushTaskResults(QList taskResultsList) +{ + foreach (ctkDICOMTaskResults* taskResults, taskResultsList) + { + this->RequestQueue.pushTaskResult(taskResults); + } + + if (!this->RequestQueue.isIndexing()) + { + // Start background indexing + this->RequestQueue.setIndexing(true); + QMap modifiedTimeForFilepath; + this->Database->allFilesModifiedTimes(modifiedTimeForFilepath); + this->RequestQueue.setModifiedTimeForFilepath(modifiedTimeForFilepath); + emit startWorker(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexerPrivate::pushTaskResults(ctkDICOMTaskResults *taskResults) +{ + this->RequestQueue.pushTaskResult(taskResults); + + if (!this->RequestQueue.isIndexing()) + { + // Start background indexing + this->RequestQueue.setIndexing(true); + QMap modifiedTimeForFilepath; + this->Database->allFilesModifiedTimes(modifiedTimeForFilepath); + this->RequestQueue.setModifiedTimeForFilepath(modifiedTimeForFilepath); + emit startWorker(); + } +} + //------------------------------------------------------------------------------ CTK_GET_CPP(ctkDICOMIndexer, bool, isBackgroundImportEnabled, BackgroundImportEnabled); CTK_SET_CPP(ctkDICOMIndexer, bool, setBackgroundImportEnabled, BackgroundImportEnabled); @@ -398,54 +446,78 @@ ctkDICOMDatabase* ctkDICOMIndexer::database() return d->Database; } - //------------------------------------------------------------------------------ - void ctkDICOMIndexer::databaseFilenameChanged() - { - Q_D(ctkDICOMIndexer); - if (d->Database) - { - d->RequestQueue.setDatabaseFilename(d->Database->databaseFilename()); - } - else - { - d->RequestQueue.setDatabaseFilename(QString()); - } - } - - //------------------------------------------------------------------------------ - void ctkDICOMIndexer::tagsToPrecacheChanged() - { - Q_D(ctkDICOMIndexer); - if (d->Database) - { - d->RequestQueue.setTagsToPrecache(d->Database->tagsToPrecache()); - } - else - { - d->RequestQueue.setTagsToPrecache(QStringList()); - } - } - - //------------------------------------------------------------------------------ - void ctkDICOMIndexer::tagsToExcludeFromStorageChanged() - { - Q_D(ctkDICOMIndexer); - if (d->Database) - { - d->RequestQueue.setTagsToExcludeFromStorage(d->Database->tagsToExcludeFromStorage()); - } - else - { - d->RequestQueue.setTagsToExcludeFromStorage(QStringList()); - } - } - - //------------------------------------------------------------------------------ - void ctkDICOMIndexer::addFile(ctkDICOMDatabase* db, const QString filePath, bool copyFile/*=false*/) - { - this->setDatabase(db); - this->addFile(filePath, copyFile); - } +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::databaseFilenameChanged() +{ + Q_D(ctkDICOMIndexer); + if (d->Database) + { + d->RequestQueue.setDatabaseFilename(d->Database->databaseFilename()); + } + else + { + d->RequestQueue.setDatabaseFilename(QString()); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::tagsToPrecacheChanged() +{ + Q_D(ctkDICOMIndexer); + if (d->Database) + { + d->RequestQueue.setTagsToPrecache(d->Database->tagsToPrecache()); + } + else + { + d->RequestQueue.setTagsToPrecache(QStringList()); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::tagsToExcludeFromStorageChanged() +{ + Q_D(ctkDICOMIndexer); + if (d->Database) + { + d->RequestQueue.setTagsToExcludeFromStorage(d->Database->tagsToExcludeFromStorage()); + } + else + { + d->RequestQueue.setTagsToExcludeFromStorage(QStringList()); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::addFile(ctkDICOMDatabase* db, const QString filePath, bool copyFile/*=false*/) +{ + this->setDatabase(db); + this->addFile(filePath, copyFile); +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::insertTaskResults(QList taskResultsList) +{ + Q_D(ctkDICOMIndexer); + + d->pushTaskResults(taskResultsList); + if (!d->BackgroundImportEnabled) + { + this->waitForImportFinished(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::insertTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMIndexer); + + d->pushTaskResults(taskResults); + if (!d->BackgroundImportEnabled) + { + this->waitForImportFinished(); + } +} //------------------------------------------------------------------------------ void ctkDICOMIndexer::addFile(const QString filePath, bool copyFile/*=false*/) @@ -527,6 +599,8 @@ bool ctkDICOMIndexer::addDicomdir(ctkDICOMDatabase* db, const QString& directory //------------------------------------------------------------------------------ bool ctkDICOMIndexer::addDicomdir(const QString& directoryName, bool copyFile/*=false*/) { + Q_D(ctkDICOMIndexer); + //Initialize dicomdir with directory path QString dcmFilePath = directoryName; dcmFilePath.append("/DICOMDIR"); @@ -587,7 +661,6 @@ bool ctkDICOMIndexer::addDicomdir(const QString& directoryName, bool copyFile/*= continue; } logger.debug( "Study instance UID: " + QString(studyInstanceUID.c_str()) ); - while ((seriesRecord = studyRecord->nextSub(seriesRecord)) != NULL) { logger.debug( "Reading new Series:" ); @@ -604,7 +677,6 @@ bool ctkDICOMIndexer::addDicomdir(const QString& directoryName, bool copyFile/*= continue; } logger.debug( "Series instance UID: " + QString(seriesInstanceUID.c_str()) ); - while ((fileRecord = seriesRecord->nextSub(fileRecord)) != NULL) { if (fileRecord->findAndGetOFStringArray(DCM_ReferencedSOPInstanceUIDInFile, sopInstanceUID).bad() @@ -634,10 +706,11 @@ 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.info( + QString("DICOM indexer has processed DICOMDIR in %1 [%6s]") + .arg(directoryName) + .arg(QString::number(elapsedTimeInSeconds,'f', 6)) + ); this->addListOfFiles(listOfInstances, copyFile); } return success; diff --git a/Libs/DICOM/Core/ctkDICOMIndexer.h b/Libs/DICOM/Core/ctkDICOMIndexer.h index a53f1c0256..9d12641e0d 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer.h +++ b/Libs/DICOM/Core/ctkDICOMIndexer.h @@ -27,6 +27,7 @@ #include "ctkDICOMCoreExport.h" #include "ctkDICOMDatabase.h" +#include "ctkErrorLogLevel.h" class ctkDICOMIndexerPrivate; @@ -121,11 +122,20 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMIndexer : public QObject /// msecTimeout specifies a maximum timeout. If <0 then it means wait indefinitely. Q_INVOKABLE void waitForImportFinished(int msecTimeout = -1); + /// \brief Add taskResults to the queue. A worker in Qthread will process + /// the insert into the dicom database sequentially. + /// + /// This utulity method is used if we are not importing files or folders, but using query and retrieve ctk classes + Q_INVOKABLE void insertTaskResults(QList taskResultsList); + Q_INVOKABLE void insertTaskResults(ctkDICOMTaskResults* taskResults); + Q_SIGNALS: /// Description of current phase of the indexing (parsing, importing, ...) void progressStep(QString); /// Detailed information about the current progress (e.g., name of currently processed file) void progressDetail(QString); + /// Detailed information about the current task insert progress + void progressTaskDetail(ctkDICOMTaskResults*); /// Progress in percentage void progress(int); /// Indexing is completed. diff --git a/Libs/DICOM/Core/ctkDICOMIndexer_p.h b/Libs/DICOM/Core/ctkDICOMIndexer_p.h index bec7104153..07c3d513b8 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer_p.h +++ b/Libs/DICOM/Core/ctkDICOMIndexer_p.h @@ -25,6 +25,7 @@ #include "ctkDICOMIndexer.h" #include "ctkDICOMItem.h" +#include "ctkDICOMTaskResults.h" class ctkDICOMDatabase; class ctkDataset; @@ -98,7 +99,8 @@ class DICOMIndexingQueue { QMutexLocker locker(&this->Mutex); this->IndexingRequests.clear(); - this->IndexingResults.clear(); + qDeleteAll(this->TaskResultsList); + this->TaskResultsList.clear(); } int popIndexingRequest(IndexingRequest& indexingRequest) @@ -118,24 +120,30 @@ class DICOMIndexingQueue this->IndexingRequests.push_back(indexingRequest); } - int indexingResultsCount() + int taskResultsCount() { QMutexLocker locker(&this->Mutex); - return this->IndexingResults.size(); + return this->TaskResultsList.size(); } - void popAllIndexingResults(QList& indexingResults) + ctkDICOMTaskResults* popTaskResult() { QMutexLocker locker(&this->Mutex); - indexingResults = this->IndexingResults; - this->IndexingResults.clear(); + if (this->TaskResultsList.empty()) + { + return nullptr; + } + + ctkDICOMTaskResults* taskResults = this->TaskResultsList[0]; + this->TaskResultsList.erase(this->TaskResultsList.begin()); + return taskResults; } - int pushIndexingResult(const ctkDICOMDatabase::IndexingResult& indexingResult) + int pushTaskResult(ctkDICOMTaskResults* taskResult) { QMutexLocker locker(&this->Mutex); - this->IndexingResults.push_back(indexingResult); - return this->IndexingResults.size(); + this->TaskResultsList.push_back(taskResult); + return this->TaskResultsList.size(); } void modifiedTimeForFilepath(QMap& timesForPaths) @@ -165,7 +173,7 @@ class DICOMIndexingQueue bool isEmpty() { QMutexLocker locker(&this->Mutex); - return (this->IndexingRequests.isEmpty() && this->IndexingResults.isEmpty()); + return (this->IndexingRequests.isEmpty() && this->TaskResultsList.isEmpty()); } bool isIndexingRequestsEmpty() @@ -189,7 +197,7 @@ class DICOMIndexingQueue QMap ModifiedTimeForFilepath; QList IndexingRequests; - QList IndexingResults; + QList TaskResultsList; QString DatabaseFilename; QStringList TagsToPrecache; @@ -217,13 +225,14 @@ public Q_SLOTS: void progress(int); void progressDetail(QString); void progressStep(QString); + void progressTaskDetail(ctkDICOMTaskResults*); void updatingDatabase(bool); void indexingComplete(int, int, int, int); private: void processIndexingRequest(DICOMIndexingQueue::IndexingRequest& request, ctkDICOMDatabase& database); - void writeIndexingResultsToDatabase(ctkDICOMDatabase& database); + void writeTaskResultsToDatabase(ctkDICOMDatabase& database); DICOMIndexingQueue* RequestQueue; int NumberOfInstancesToInsert; @@ -255,6 +264,8 @@ class ctkDICOMIndexerPrivate : public QObject ~ctkDICOMIndexerPrivate(); void pushIndexingRequest(const DICOMIndexingQueue::IndexingRequest& request); + void pushTaskResults(QList taskResultsList); + void pushTaskResults(ctkDICOMTaskResults* taskResults); Q_SIGNALS: void startWorker(); diff --git a/Libs/DICOM/Core/ctkDICOMItem.cpp b/Libs/DICOM/Core/ctkDICOMItem.cpp index 75744ff3b7..a7b4deb224 100644 --- a/Libs/DICOM/Core/ctkDICOMItem.cpp +++ b/Libs/DICOM/Core/ctkDICOMItem.cpp @@ -90,21 +90,21 @@ void ctkDICOMItem::InitializeFromItem(DcmItem *dataset, bool takeOwnership) { d->m_SpecificCharacterSet = encoding.c_str(); } - } - if (d->m_SpecificCharacterSet.isEmpty()) - { - /// - /// see Bug # 6458: - /// There are cases, where different studies of the same person get encoded both with and without the SpecificCharacterSet attribute set. - /// DICOM says: default is ASCII / ISO_IR 6 / ISO 646 - /// Since we experienced such mixed data, we supplement missing characterset information with the ISO_IR 100 / Latin1 character set. - /// Since Latin1 is a superset of ASCII, this will not cause problems. PLUS in most cases (Europe) we will guess right and suppress - /// "double patients" in applications. - /// - SetElementAsString( DCM_SpecificCharacterSet, "ISO_IR 100" ); - } + } + if (d->m_SpecificCharacterSet.isEmpty()) + { + /// + /// see Bug # 6458: + /// There are cases, where different studies of the same person get encoded both with and without the SpecificCharacterSet attribute set. + /// DICOM says: default is ASCII / ISO_IR 6 / ISO 646 + /// Since we experienced such mixed data, we supplement missing characterset information with the ISO_IR 100 / Latin1 character set. + /// Since Latin1 is a superset of ASCII, this will not cause problems. PLUS in most cases (Europe) we will guess right and suppress + /// "double patients" in applications. + /// + SetElementAsString( DCM_SpecificCharacterSet, "ISO_IR 100" ); } } +} void ctkDICOMItem::InitializeFromFile(const QString& filename, diff --git a/Libs/DICOM/Core/ctkDICOMItem.h b/Libs/DICOM/Core/ctkDICOMItem.h index f15c01beb1..ecce2c4983 100644 --- a/Libs/DICOM/Core/ctkDICOMItem.h +++ b/Libs/DICOM/Core/ctkDICOMItem.h @@ -247,6 +247,11 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMItem /// static QString TagVR( const DcmTag& tag ); + /// + /// \brief return dcm item + /// + DcmItem& GetDcmItem() const; + protected: /// @@ -267,8 +272,6 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMItem QScopedPointer d_ptr; - DcmItem& GetDcmItem() const; - private: Q_DECLARE_PRIVATE(ctkDICOMItem); }; diff --git a/Libs/DICOM/Core/ctkDICOMQuery.cpp b/Libs/DICOM/Core/ctkDICOMQuery.cpp index c8b0f6c914..9f6083a781 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 "ctkDICOMTaskResults.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; + DcmDataset* QueryDcmDataset; + QList> StudyAndSeriesInstanceUIDPairList; + QMap StudyDatasets; + bool Canceled; + int MaximumPatientsQuery; + QString TaskUID; + QList TaskResultsList; }; //------------------------------------------------------------------------------ @@ -112,29 +112,33 @@ class ctkDICOMQueryPrivate //------------------------------------------------------------------------------ ctkDICOMQueryPrivate::ctkDICOMQueryPrivate() { - this->Query = new DcmDataset(); + this->QueryDcmDataset = new DcmDataset(); + this->PresentationContext = 0; this->Port = 0; this->Canceled = false; - this->PreferCGET = false; + this->MaximumPatientsQuery = 2; + + this->SCU.setACSETimeout(10); + this->SCU.setConnectionTimeout(10); } //------------------------------------------------------------------------------ ctkDICOMQueryPrivate::~ctkDICOMQueryPrivate() { - delete this->Query; + delete this->QueryDcmDataset; + this->TaskResultsList.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,17 +150,76 @@ 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 + + this->setDCMTKLogLevel(logger.logLevel()); } //------------------------------------------------------------------------------ ctkDICOMQuery::~ctkDICOMQuery() { + this->clearTaskResults(); +} + +//----------------------------------------------------------------------------- +void ctkDICOMQuery::setDCMTKLogLevel(const ctkErrorLogLevel::LogLevel& level) +{ + OFLogger::LogLevel dcmtkLogLevel = OFLogger::OFF_LOG_LEVEL; + if (level == ctkErrorLogLevel::LogLevel::Fatal) + { + dcmtkLogLevel = OFLogger::FATAL_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Critical || + level == ctkErrorLogLevel::LogLevel::Error) + { + dcmtkLogLevel = OFLogger::ERROR_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Warning) + { + dcmtkLogLevel = OFLogger::WARN_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Info) + { + dcmtkLogLevel = OFLogger::INFO_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Debug) + { + dcmtkLogLevel = OFLogger::DEBUG_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Trace || + level == ctkErrorLogLevel::LogLevel::Status) + { + dcmtkLogLevel = OFLogger::TRACE_LOG_LEVEL; + } + + OFLog::configure(dcmtkLogLevel); +} + +//----------------------------------------------------------------------------- +ctkErrorLogLevel::LogLevel ctkDICOMQuery::DCMTKLogLevel() const +{ + return logger.logLevel(); } /// 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 +233,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 +247,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 +261,7 @@ QString ctkDICOMQuery::host() const } //------------------------------------------------------------------------------ -void ctkDICOMQuery::setPort ( int port ) +void ctkDICOMQuery::setPort(int port) { Q_D(ctkDICOMQuery); d->Port = port; @@ -211,18 +274,40 @@ int ctkDICOMQuery::port()const return d->Port; } +//----------------------------------------------------------------------------- +void ctkDICOMQuery::setConnectionTimeout(const 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(const 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; } //------------------------------------------------------------------------------ @@ -247,276 +332,897 @@ QList< QPair > ctkDICOMQuery::studyAndSeriesInstanceUIDQueried( } //------------------------------------------------------------------------------ -bool ctkDICOMQuery::query(ctkDICOMDatabase& database ) +QList ctkDICOMQuery::taskResultsList()const +{ + Q_D(const ctkDICOMQuery); + return d->TaskResultsList; +} + +//------------------------------------------------------------------------------ +void ctkDICOMQuery::addTaskResults(ctkDICOMTaskResults* results) +{ + Q_D(ctkDICOMQuery); + d->TaskResultsList.append(results); +} + +//------------------------------------------------------------------------------ +void ctkDICOMQuery::deleteTaskResults(ctkDICOMTaskResults *taskResults) { - // turn on logging if needed for debug: - //ctk::setDICOMLogLevel(ctkErrorLogLevel::Debug); + Q_D(ctkDICOMQuery); + if (!taskResults) + { + return; + } + + d->TaskResultsList.removeOne(taskResults); +} + +//------------------------------------------------------------------------------ +void ctkDICOMQuery::deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMQuery); + if (!taskResults) + { + return; + } + + foreach (ctkDICOMTaskResults* seriesTaskResults, d->TaskResultsList) + { + if (!seriesTaskResults || + seriesTaskResults == taskResults || + seriesTaskResults->typeOfTask() != taskResults->typeOfTask() || + seriesTaskResults->studyInstanceUID() != taskResults->studyInstanceUID() || + seriesTaskResults->seriesInstanceUID() != taskResults->seriesInstanceUID()) + { + continue; + } + + this->deleteTaskResults(seriesTaskResults); + } + + this->deleteTaskResults(taskResults); +} + +//------------------------------------------------------------------------------ +void ctkDICOMQuery::clearTaskResults() +{ + Q_D(ctkDICOMQuery); + qDeleteAll(d->TaskResultsList); + d->TaskResultsList.clear(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMQuery::setTaskUID(const QString &taskUID) +{ + Q_D(ctkDICOMQuery); + d->TaskUID = taskUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMQuery::taskUID() const +{ + Q_D(const ctkDICOMQuery); + return d->TaskUID; +} - // 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, &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(); - int i = 0; + 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, &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 ); - // insert series dataset - database.insert ( dataset, false /* do not store */, false /* no thumbnail */ ); + dataset->insert(patientName, true); + dataset->insert(patientID, true); + // insert series dataset + 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->TaskResultsList.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; + } + + ctkDICOMTaskResults* taskResults = new ctkDICOMTaskResults; + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::QueryPatients); + taskResults->setConnectionName(d->ConnectionName); + taskResults->setTaskUID(d->TaskUID); + + QMap datasetsMap; + OFList responses; + OFCondition status = d->SCU.sendFINDRequest(presentationContext, d->QueryDcmDataset, &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); + } + } + + taskResults->setDatasets(datasetsMap); + d->TaskResultsList.append(taskResults); + + 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->TaskResultsList.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()); + + ctkDICOMTaskResults* taskResults = new ctkDICOMTaskResults; + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::QueryStudies); + taskResults->setPatientID(patientID.toStdString().c_str()); + taskResults->setConnectionName(d->ConnectionName); + taskResults->setTaskUID(d->TaskUID); + + QMap datasetsMap; + + OFList responses; + OFCondition status = d->SCU.sendFINDRequest(presentationContext, d->QueryDcmDataset, &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); + } + } + + taskResults->setDatasets(datasetsMap); + d->TaskResultsList.append(taskResults); + + 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->TaskResultsList.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()); + + ctkDICOMTaskResults* taskResults = new ctkDICOMTaskResults; + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::QuerySeries); + taskResults->setPatientID(patientID.toStdString().c_str()); + taskResults->setStudyInstanceUID(studyInstanceUID.toStdString().c_str()); + taskResults->setConnectionName(d->ConnectionName); + taskResults->setTaskUID(d->TaskUID); + + QMap datasetsMap; + + OFList responses; + OFCondition status = d->SCU.sendFINDRequest(presentationContext, d->QueryDcmDataset, &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); + } + } + + taskResults->setDatasets(datasetsMap); + d->TaskResultsList.append(taskResults); + + 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->TaskResultsList.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()); + + ctkDICOMTaskResults* taskResults = new ctkDICOMTaskResults; + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::QueryInstances); + taskResults->setPatientID(patientID.toStdString().c_str()); + taskResults->setStudyInstanceUID(studyInstanceUID.toStdString().c_str()); + taskResults->setSeriesInstanceUID(seriesInstanceUID.toStdString().c_str()); + taskResults->setConnectionName(d->ConnectionName); + taskResults->setTaskUID(d->TaskUID); + + QMap datasetsMap; + + OFList responses; + OFCondition status = d->SCU.sendFINDRequest(d->PresentationContext, d->QueryDcmDataset, &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); + } + + taskResults->setDatasets(datasetsMap); + d->TaskResultsList.append(taskResults); + + 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 613ac55629..b346b2b5c3 100644 --- a/Libs/DICOM/Core/ctkDICOMQuery.h +++ b/Libs/DICOM/Core/ctkDICOMQuery.h @@ -21,62 +21,108 @@ #ifndef __ctkDICOMQuery_h #define __ctkDICOMQuery_h -// Qt includes +// Qt includes #include #include #include -#include // CTK includes #include +// ctkDICOMCore includes #include "ctkDICOMCoreExport.h" #include "ctkDICOMDatabase.h" +#include "ctkErrorLogLevel.h" class ctkDICOMQueryPrivate; +class ctkDICOMTaskResults; /// \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); public: explicit ctkDICOMQuery(QObject* parent = 0); virtual ~ctkDICOMQuery(); - - /// Set methods for connectivity + + /// Set methods for connectivity. + /// Empty by default + void setConnectionName(const QString& connectionName); + QString connectionName() const; /// Empty by default - void setCallingAETitle ( const QString& callingAETitle ); + void setCallingAETitle(const QString& callingAETitle); QString callingAETitle()const; /// Empty by default - void setCalledAETitle ( const QString& calledAETitle ); - QString calledAETitle()const; + 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 + void setPort(int port); + int port() const; + /// connection timeout, default 10 sec. + void setConnectionTimeout(const int timeout); + int connectionTimeout() const; + /// maximum number of responses allowed in one query + /// when query is at Patient level. Default is 25. + void setMaximumPatientsQuery(const int maximumPatientsQuery); + int maximumPatientsQuery(); + + /// operation is canceled? + Q_INVOKABLE bool wasCanceled(); + + /// Log level for dcmtk. Default: Error. + Q_INVOKABLE void setDCMTKLogLevel(const ctkErrorLogLevel::LogLevel& level); + Q_INVOKABLE ctkErrorLogLevel::LogLevel DCMTKLogLevel() 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; + /// 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() + 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() + 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() + 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() + Q_INVOKABLE bool queryInstances(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID); + + /// Access the list of study and series instance UIDs from the last query. + QList> studyAndSeriesInstanceUIDQueried()const; + + /// Access the list of datasets from the last query. + Q_INVOKABLE QList taskResultsList() const; + Q_INVOKABLE void addTaskResults(ctkDICOMTaskResults* results); + Q_INVOKABLE void deleteTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void clearTaskResults(); + Q_INVOKABLE void setTaskUID(const QString& taskUID); + Q_INVOKABLE QString taskUID() const; /// /// Filters are keyword/value pairs as generated by @@ -87,7 +133,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 +141,8 @@ 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; Q_SIGNALS: /// Signal is emitted inside the query() function. It ranges from 0 to 100. @@ -105,12 +151,12 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMQuery : public QObject /// Signal is emitted inside the query() function. It sends the different step /// the function is at. void progress(const QString& message); - /// Signal is emitted inside the query() function. It sends + /// Signal is emitted inside the query() function. It sends /// detailed feedback for debugging void debug(const QString& message); /// Signal is emitted inside the query() function. It send any error messages void error(const QString& message); - /// Signal is emitted inside the query() function when finished with value + /// Signal is emitted inside the query() function when finished with value /// true for success or false for error void done(const bool& error); @@ -118,6 +164,9 @@ public Q_SLOTS: void cancel(); protected: + QString applyFilters(); + bool initializeSCU(); + QScopedPointer d_ptr; private: diff --git a/Libs/DICOM/Core/ctkDICOMQueryTask.cpp b/Libs/DICOM/Core/ctkDICOMQueryTask.cpp new file mode 100644 index 0000000000..9067f4010f --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryTask.cpp @@ -0,0 +1,284 @@ +/*========================================================================= + + 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 "ctkDICOMQueryTask_p.h" +#include "ctkLogger.h" + +static ctkLogger logger ( "org.commontk.dicom.ctkDICOMQueryTask" ); + +//------------------------------------------------------------------------------ +// ctkDICOMQueryTaskPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMQueryTaskPrivate::ctkDICOMQueryTaskPrivate(ctkDICOMQueryTask* object) + : q_ptr(object) +{ + this->Query = new ctkDICOMQuery; + this->Server = new ctkDICOMServer; + this->QueryLevel = ctkDICOMQueryTask::DICOMLevel::Studies; + this->StudyInstanceUID = ""; + this->SeriesInstanceUID = ""; +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryTaskPrivate::~ctkDICOMQueryTaskPrivate() +{ + if (this->Query) + { + delete this->Query; + this->Query = nullptr; + } + + if (this->Server) + { + delete this->Server; + this->Server = nullptr; + } +} + +//------------------------------------------------------------------------------ +// ctkDICOMQueryTask methods + +//------------------------------------------------------------------------------ +ctkDICOMQueryTask::ctkDICOMQueryTask() + : d_ptr(new ctkDICOMQueryTaskPrivate(this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryTask::ctkDICOMQueryTask(ctkDICOMQueryTaskPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMQueryTask::~ctkDICOMQueryTask() +{ +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::setQueryLevel(DICOMLevel queryLevel) +{ + Q_D(ctkDICOMQueryTask); + d->QueryLevel = queryLevel; +} + +//---------------------------------------------------------------------------- +ctkDICOMQueryTask::DICOMLevel ctkDICOMQueryTask::queryLevel() const +{ + Q_D(const ctkDICOMQueryTask); + return d->QueryLevel; +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::setTaskUID(const QString &taskUID) +{ + Q_D(const ctkDICOMQueryTask); + + Superclass::setTaskUID(taskUID); + d->Query->setTaskUID(taskUID); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::setStop(const bool& stop) +{ + Q_D(const ctkDICOMQueryTask); + ctkAbstractTask::setStop(stop); + d->Query->cancel(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::run() +{ + Q_D(const ctkDICOMQueryTask); + if (this->isStopped()) + { + this->setIsFinished(true); + emit canceled(); + return; + } + + this->setIsRunning(true); + emit started(); + + logger.debug("ctkDICOMQueryTask : running task on thread id " + + QString::number(reinterpret_cast(QThread::currentThreadId()), 16)); + switch(d->QueryLevel) + { + case DICOMLevel::Patients: + if (!d->Query->queryPatients()) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + case DICOMLevel::Studies: + if (!d->Query->queryStudies(d->PatientID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + case DICOMLevel::Series: + if (!d->Query->querySeries(d->PatientID, + d->StudyInstanceUID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + case DICOMLevel::Instances: + if (!d->Query->queryInstances(d->PatientID, + d->StudyInstanceUID, + d->SeriesInstanceUID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + } + + this->setIsFinished(true); + emit finished(); +} + +//---------------------------------------------------------------------------- +QList ctkDICOMQueryTask::taskResultsList() const +{ + Q_D(const ctkDICOMQueryTask); + return d->Query->taskResultsList(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::deleteTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(const ctkDICOMQueryTask); + d->Query->deleteTaskResults(taskResults); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(const ctkDICOMQueryTask); + d->Query->deleteSeriesTaskResults(taskResults); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::setFilters(const QMap &filters) +{ + Q_D(ctkDICOMQueryTask); + d->Query->setFilters(filters); +} + +//---------------------------------------------------------------------------- +QMap ctkDICOMQueryTask::filters() const +{ + Q_D(const ctkDICOMQueryTask); + return d->Query->filters(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::setServer(ctkDICOMServer *server) +{ + Q_D(ctkDICOMQueryTask); + if (!server) + { + logger.debug("server is null"); + return; + } + + if (d->Server == server) + { + return; + } + + d->Server->deepCopy(server); + + d->Query->setConnectionName(d->Server->connectionName()); + d->Query->setCallingAETitle(d->Server->callingAETitle()); + d->Query->setCalledAETitle(d->Server->calledAETitle()); + d->Query->setHost(d->Server->host()); + d->Query->setPort(d->Server->port()); + d->Query->setConnectionTimeout(d->Server->connectionTimeout()); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMQueryTask::server()const +{ + Q_D(const ctkDICOMQueryTask); + return d->Server; +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::setPatientID(const QString &patientID) +{ + Q_D(ctkDICOMQueryTask); + d->PatientID = patientID; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMQueryTask::patientID() const +{ + Q_D(const ctkDICOMQueryTask); + return d->PatientID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::setStudyInstanceUID(const QString& studyInstanceUID) +{ + Q_D(ctkDICOMQueryTask); + d->StudyInstanceUID = studyInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMQueryTask::studyInstanceUID() const +{ + Q_D(const ctkDICOMQueryTask); + return d->StudyInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMQueryTask::setSeriesInstanceUID(const QString& seriesInstanceUID) +{ + Q_D(ctkDICOMQueryTask); + d->SeriesInstanceUID = seriesInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMQueryTask::seriesInstanceUID() const +{ + Q_D(const ctkDICOMQueryTask); + return d->SeriesInstanceUID; +} + +//------------------------------------------------------------------------------ +ctkDICOMQuery *ctkDICOMQueryTask::querier() const +{ + Q_D(const ctkDICOMQueryTask); + return d->Query; +} diff --git a/Libs/DICOM/Core/ctkDICOMQueryTask.h b/Libs/DICOM/Core/ctkDICOMQueryTask.h new file mode 100644 index 0000000000..bba162ed1b --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryTask.h @@ -0,0 +1,136 @@ +/*========================================================================= + + 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 __ctkDICOMQueryTask_h +#define __ctkDICOMQueryTask_h + +// Qt includes +#include +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMServer.h" +#include + +class ctkDICOMQuery; +class ctkDICOMQueryTaskPrivate; +class ctkDICOMTaskResults; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMQueryTask : public ctkAbstractTask +{ + Q_OBJECT + Q_ENUMS(DICOMLevel) + 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(DICOMLevel queryLevel READ queryLevel WRITE setQueryLevel); + +public: + typedef ctkAbstractTask Superclass; + explicit ctkDICOMQueryTask(); + virtual ~ctkDICOMQueryTask(); + + enum DICOMLevel{ + Patients, + Studies, + Series, + Instances + }; + + /// Query Level + void setQueryLevel(DICOMLevel queryLevel); + DICOMLevel queryLevel() const; + + /// Task UID + void setTaskUID(const QString& taskUID); + + /// Stop task + void setStop(const bool& stop); + + /// Execute task + void run(); + + /// + /// 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; + + /// Server + Q_INVOKABLE ctkDICOMServer* server()const; + Q_INVOKABLE void setServer(ctkDICOMServer* server); + + /// 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; + + /// Access the list of datasets from the last query. + Q_INVOKABLE QList taskResultsList() const; + Q_INVOKABLE void deleteTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults); + + /// Querier + 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. + ctkDICOMQueryTask(ctkDICOMQueryTaskPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMQueryTask); + Q_DISABLE_COPY(ctkDICOMQueryTask); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMQueryTask_p.h b/Libs/DICOM/Core/ctkDICOMQueryTask_p.h new file mode 100644 index 0000000000..d202ef073e --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMQueryTask_p.h @@ -0,0 +1,57 @@ +/*========================================================================= + + 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 __ctkDICOMQueryTaskPrivate_h +#define __ctkDICOMQueryTaskPrivate_h + +// ctkDICOMCore includes +#include "ctkDICOMQuery.h" + +// ctkDICOMCore includes +#include "ctkDICOMQueryTask.h" + +//------------------------------------------------------------------------------ +class ctkDICOMQueryTaskPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMQueryTask) + +protected: + ctkDICOMQueryTask* const q_ptr; + +public: + ctkDICOMQueryTaskPrivate(ctkDICOMQueryTask* object); + ~ctkDICOMQueryTaskPrivate(); + + ctkDICOMServer* Server; + ctkDICOMQuery *Query; + + QString PatientID; + QString StudyInstanceUID; + QString SeriesInstanceUID; + int MaximumPatientsQuery; + + ctkDICOMQueryTask::DICOMLevel QueryLevel; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMRetrieve.cpp b/Libs/DICOM/Core/ctkDICOMRetrieve.cpp index b5e186fa0f..94bc5358f6 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 "ctkDICOMTaskResults.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,61 @@ 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->taskUID().isEmpty() && !this->retrieve->wasCanceled()) + { + ctkDICOMTaskResults* taskResults = new ctkDICOMTaskResults; + if (this->retrieve->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSOPInstance) + { + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::RetrieveSOPInstance); + } + else if (this->retrieve->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSeries) + { + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::RetrieveSeries); + } + else if (this->retrieve->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveStudy) { - this->retrieve->database()->insert(incomingObject); - return EC_Normal; + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::RetrieveStudy); } - else + taskResults->setPatientID(this->retrieve->patientID()); + taskResults->setStudyInstanceUID(this->retrieve->studyInstanceUID()); + taskResults->setSeriesInstanceUID(this->retrieve->seriesInstanceUID()); + taskResults->setSOPInstanceUID(qInstanceUID); + taskResults->setConnectionName(this->retrieve->connectionName()); + taskResults->setDataset(incomingObject); + taskResults->setTaskUID(this->retrieve->taskUID()); + taskResults->setCopyFile(true); + + if (this->retrieve->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSeries) { - return this->DcmSCU::handleSTORERequest( - presID, incomingObject, continueCGETSession, cStoreReturnStatus); + emit this->retrieve->progressBarTaskDetail(taskResults); } + + this->retrieve->addTaskResults(taskResults); + 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 @@ -122,10 +150,8 @@ class ctkDICOMRetrieveSCUPrivate : public DcmSCU { 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; }; }; @@ -134,7 +160,7 @@ class ctkDICOMRetrieveSCUPrivate : public DcmSCU //------------------------------------------------------------------------------ class ctkDICOMRetrievePrivate: public QObject { - Q_DECLARE_PUBLIC( ctkDICOMRetrieve ); + Q_DECLARE_PUBLIC(ctkDICOMRetrieve); protected: ctkDICOMRetrieve* const q_ptr; @@ -142,27 +168,42 @@ class ctkDICOMRetrievePrivate: public QObject public: ctkDICOMRetrievePrivate(ctkDICOMRetrieve& obj); ~ctkDICOMRetrievePrivate(); - /// Keep the currently negotiated connection to the + + /// 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 ConnectionName; + QString TaskUID; + 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 TaskResultsList; + + 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 +214,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->TaskUID = ""; // Register the JPEG libraries in case we need them // (registration only happens once, so it's okay to call repeatedly) @@ -189,21 +236,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 ); + 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++) { - this->SCU.addPresentationContext(dcmLongSCUStorageSOPClassUIDs[i], - transferSyntaxes, ASC_SC_ROLE_SCP); + this->SCU.addPresentationContext(dcmLongSCUStorageSOPClassUIDs[i], + transferSyntaxes, ASC_SC_ROLE_SCP); } + + this->SCU.setACSETimeout(3); + this->SCU.setConnectionTimeout(3); + this->SCU.setStorageDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString().c_str()); } //------------------------------------------------------------------------------ @@ -212,114 +263,173 @@ 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->TaskResultsList.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()) { // Check and initialize networking parameters in DCMTK - if ( !this->SCU.initNetwork().good() ) + if (!this->SCU.initNetwork().good()) { - logger.error ( "Error initializing the network" ); + logger.error("Error initializing the network"); return false; } // Negotiate (i.e. start the) association - logger.debug ( "Negotiating Association" ); + logger.debug("Negotiating Association"); - if ( !this->SCU.negotiateAssociation().good() ) + if ( !this->SCU.negotiateAssociation().good()) { - logger.error ( "Error negotiating association" ); - return false;; + logger.error("Error negotiating association"); + return false; } } this->ConnectionParamsChanged = false; // Setup query about what to be received from the PACS - logger.debug ( "Setting Retrieve Parameters" ); - if ( retrieveType == RetrieveSeries ) + logger.debug ("Setting Retrieve Parameters"); + 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() ); + 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->TaskResultsList.clear(); + this->PatientID = patientID; + this->StudyInstanceUID = studyInstanceUID; + this->SeriesInstanceUID = seriesInstanceUID; + + 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" ); + 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; // If we do not receive a single response, something is fishy - if ( responses.begin() == responses.end() ) + if (responses.begin() == responses.end()) { - logger.error ( "No responses received at all! (at least one empty response always expected)" ); + logger.error("No responses received at all! (at least one empty response always expected)"); //throw std::runtime_error( std::string("No responses received from server!") ); return false; } + 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: @@ -328,25 +438,24 @@ bool ctkDICOMRetrievePrivate::move ( const QString& studyInstanceUID, * 3) Error code, i.e. no images transferred * 4) Warning (one or more failures, i.e. some images transferred) */ - if ( responses.size() == 1 ) + if (responses.size() == 1) { RetrieveResponse* rsp = *responses.begin(); - logger.debug ( "MOVE response receveid with status: " + + logger.debug ( "MOVE response receveid with status: " + QString(DU_cmoveStatusString(rsp->m_status)) ); - if ( (rsp->m_status == STATUS_Success) - || (rsp->m_status == STATUS_MOVE_Warning_SubOperationsCompleteOneOrMoreFailures)) + if ((rsp->m_status == STATUS_Success) || + (rsp->m_status == STATUS_MOVE_Warning_SubOperationsCompleteOneOrMoreFailures)) { if (rsp->m_numberOfCompletedSubops == 0) { - logger.error ( "No images transferred by PACS!" ); - //throw std::runtime_error( std::string("No images transferred by PACS!") ); + logger.error("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,13 +464,13 @@ 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; } } - // Select the last MOVE response to output meaningful status information - OFListIterator(RetrieveResponse*) it = responses.begin(); + + // Select the last MOVE response to output meaningful status information + OFListIterator(RetrieveResponse*) it = responses.begin(); size_t numResults = responses.size(); for (size_t i = 1; i < numResults; i++) { @@ -377,20 +486,69 @@ 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 + ctkDICOMTaskResults* taskResults = new ctkDICOMTaskResults; + if (q->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSOPInstance) + { + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::RetrieveSOPInstance); + } + else if (q->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSeries) + { + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::RetrieveSeries); + } + else if (q->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveStudy) + { + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::RetrieveStudy); + } + taskResults->setStudyInstanceUID(q->studyInstanceUID()); + taskResults->setSeriesInstanceUID(q->seriesInstanceUID()); + taskResults->setConnectionName(q->connectionName()); + taskResults->setTaskUID(q->taskUID()); + q->addTaskResults(taskResults); + 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->TaskResultsList.clear(); + this->PatientID = patientID; + this->StudyInstanceUID = studyInstanceUID; + this->SeriesInstanceUID = seriesInstanceUID; + + 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 +557,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( - UID_GETStudyRootQueryRetrieveInformationModel, + 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" ); + 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,13 +588,13 @@ 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; // If we do not receive a single response, something is fishy - if ( responses.begin() == responses.end() ) + if (responses.begin() == responses.end()) { logger.error ( "No responses received at all! (at least one empty response always expected)" ); //throw std::runtime_error( std::string("No responses received from server!") ); @@ -440,6 +602,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); @@ -451,25 +618,24 @@ bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, * 3) Error code, i.e. no images transferred * 4) Warning (one or more failures, i.e. some images transferred) */ - if ( responses.size() == 1 ) + if (responses.size() == 1) { RetrieveResponse* rsp = *responses.begin(); - logger.debug ( "GET response receveid with status: " + - QString(DU_cmoveStatusString(rsp->m_status)) ); + logger.debug("GET response receveid with status: " + + QString(DU_cmoveStatusString(rsp->m_status))); - if ( (rsp->m_status == STATUS_Success) - || (rsp->m_status == STATUS_GET_Warning_SubOperationsCompleteOneOrMoreFailures)) + if ((rsp->m_status == STATUS_Success) || + (rsp->m_status == STATUS_GET_Warning_SubOperationsCompleteOneOrMoreFailures)) { if (rsp->m_numberOfCompletedSubops == 0) { - logger.error ( "No images transferred by PACS!" ); - //throw std::runtime_error( std::string("No images transferred by PACS!") ); + logger.error("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 +644,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 +655,7 @@ bool ctkDICOMRetrievePrivate::get ( const QString& studyInstanceUID, { it++; } + logger.debug ( QString("GET responses report for study: %1\n" "%2 images transferred, and\n" @@ -516,17 +682,36 @@ 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 + + this->setDCMTKLogLevel(logger.logLevel()); } //------------------------------------------------------------------------------ ctkDICOMRetrieve::~ctkDICOMRetrieve() { + this->clearTaskResults(); } -//------------------------------------------------------------------------------ /// 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())) @@ -544,10 +729,10 @@ QString ctkDICOMRetrieve::callingAETitle() const } //------------------------------------------------------------------------------ -void ctkDICOMRetrieve::setCalledAETitle( const QString& calledAETitle ) +void ctkDICOMRetrieve::setCalledAETitle(const QString& calledAETitle) { Q_D(ctkDICOMRetrieve); - if (strcmp(calledAETitle.toStdString().c_str(),d->SCU.getPeerAETitle().c_str())) + if (strcmp(calledAETitle.toStdString().c_str(), d->SCU.getPeerAETitle().c_str())) { d->SCU.setPeerAETitle(calledAETitle.toStdString().c_str()); d->ConnectionParamsChanged = true; @@ -555,14 +740,14 @@ void ctkDICOMRetrieve::setCalledAETitle( const QString& calledAETitle ) } //------------------------------------------------------------------------------ -QString ctkDICOMRetrieve::calledAETitle()const +QString ctkDICOMRetrieve::calledAETitle() const { Q_D(const ctkDICOMRetrieve); return d->SCU.getPeerAETitle().c_str(); } //------------------------------------------------------------------------------ -void ctkDICOMRetrieve::setHost( const QString& host ) +void ctkDICOMRetrieve::setHost(const QString& host) { Q_D(ctkDICOMRetrieve); if (strcmp(host.toStdString().c_str(), d->SCU.getPeerHostName().c_str())) @@ -573,14 +758,14 @@ void ctkDICOMRetrieve::setHost( const QString& host ) } //------------------------------------------------------------------------------ -QString ctkDICOMRetrieve::host()const +QString ctkDICOMRetrieve::host() const { Q_D(const ctkDICOMRetrieve); return d->SCU.getPeerHostName().c_str(); } //------------------------------------------------------------------------------ -void ctkDICOMRetrieve::setPort( int port ) +void ctkDICOMRetrieve::setPort(int port) { Q_D(ctkDICOMRetrieve); if (d->SCU.getPeerPort() != port) @@ -591,14 +776,14 @@ void ctkDICOMRetrieve::setPort( int port ) } //------------------------------------------------------------------------------ -int ctkDICOMRetrieve::port()const +int ctkDICOMRetrieve::port() const { Q_D(const ctkDICOMRetrieve); return d->SCU.getPeerPort(); } //------------------------------------------------------------------------------ -void ctkDICOMRetrieve::setMoveDestinationAETitle( const QString& moveDestinationAETitle ) +void ctkDICOMRetrieve::setMoveDestinationAETitle(const QString& moveDestinationAETitle) { Q_D(ctkDICOMRetrieve); if (moveDestinationAETitle != d->MoveDestinationAETitle) @@ -608,7 +793,7 @@ void ctkDICOMRetrieve::setMoveDestinationAETitle( const QString& moveDestination } } //------------------------------------------------------------------------------ -QString ctkDICOMRetrieve::moveDestinationAETitle()const +QString ctkDICOMRetrieve::moveDestinationAETitle() const { Q_D(const ctkDICOMRetrieve); return d->MoveDestinationAETitle; @@ -636,12 +821,121 @@ 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::taskResultsList() const +{ + Q_D(const ctkDICOMRetrieve); + return d->TaskResultsList; +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::addTaskResults(ctkDICOMTaskResults* results) +{ + Q_D(ctkDICOMRetrieve); + d->TaskResultsList.append(results); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::deleteTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMRetrieve); + if (!taskResults) + { + return; + } + + d->TaskResultsList.removeOne(taskResults); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMRetrieve); + if (!taskResults) + { + return; + } + + foreach (ctkDICOMTaskResults* seriesTaskResults, d->TaskResultsList) + { + if (!seriesTaskResults || + seriesTaskResults == taskResults || + seriesTaskResults->typeOfTask() != taskResults->typeOfTask() || + seriesTaskResults->studyInstanceUID() != taskResults->studyInstanceUID() || + seriesTaskResults->seriesInstanceUID() != taskResults->seriesInstanceUID()) + { + continue; + } + + this->deleteTaskResults(seriesTaskResults); + } + + this->deleteTaskResults(taskResults); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::clearTaskResults() +{ + Q_D(ctkDICOMRetrieve); + qDeleteAll(d->TaskResultsList); + d->TaskResultsList.clear(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::setTaskUID(const QString &taskUID) +{ + Q_D(ctkDICOMRetrieve); + d->TaskUID = taskUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieve::taskUID() const +{ + Q_D(const ctkDICOMRetrieve); + return d->TaskUID; +} + +//------------------------------------------------------------------------------ +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; +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieve::RetrieveType ctkDICOMRetrieve::getLastRetrieveType() const +{ + Q_D(const ctkDICOMRetrieve); + return d->LastRetrieveType; +} + //------------------------------------------------------------------------------ void ctkDICOMRetrieve::setKeepAssociationOpen(const bool keepOpen) { @@ -650,83 +944,179 @@ 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(const 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; +} + +//----------------------------------------------------------------------------- +void ctkDICOMRetrieve::setDCMTKLogLevel(const ctkErrorLogLevel::LogLevel& level) +{ + OFLogger::LogLevel dcmtkLogLevel = OFLogger::OFF_LOG_LEVEL; + if (level == ctkErrorLogLevel::LogLevel::Fatal) + { + dcmtkLogLevel = OFLogger::FATAL_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Critical || + level == ctkErrorLogLevel::LogLevel::Error) + { + dcmtkLogLevel = OFLogger::ERROR_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Warning) + { + dcmtkLogLevel = OFLogger::WARN_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Info) + { + dcmtkLogLevel = OFLogger::INFO_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Debug) + { + dcmtkLogLevel = OFLogger::DEBUG_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Trace || + level == ctkErrorLogLevel::LogLevel::Status) + { + dcmtkLogLevel = OFLogger::TRACE_LOG_LEVEL; + } + + OFLog::configure(dcmtkLogLevel); +} + +//----------------------------------------------------------------------------- +ctkErrorLogLevel::LogLevel ctkDICOMRetrieve::DCMTKLogLevel() const +{ + return logger.logLevel(); } //------------------------------------------------------------------------------ -bool ctkDICOMRetrieve::moveStudy(const QString& studyInstanceUID) +bool ctkDICOMRetrieve::moveStudy(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 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); } //------------------------------------------------------------------------------ -void ctkDICOMRetrieve::cancel() +bool ctkDICOMRetrieve::moveSOPInstance(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& SOPInstanceUID, + const QString& patientID) { + if (studyInstanceUID.isEmpty() || + seriesInstanceUID.isEmpty() || + SOPInstanceUID.isEmpty()) + { + logger.error("Cannot receive SOPInstance: Study, Series or SOP Instance UID empty."); + return false; + } Q_D(ctkDICOMRetrieve); - d->WasCanceled = true; + 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) +{ + if (studyInstanceUID.isEmpty() || + seriesInstanceUID.isEmpty() || + SOPInstanceUID.isEmpty()) + { + logger.error("Cannot receive SOPInstance: Study, Series or SOP Instance UID empty."); + return false; + } + Q_D(ctkDICOMRetrieve); + logger.debug("Starting getSOPInstance"); + return d->get(patientID, studyInstanceUID, seriesInstanceUID, SOPInstanceUID, ctkDICOMRetrieve::RetrieveSOPInstance); +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieve::cancel() +{ + Q_D(ctkDICOMRetrieve); + 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 dca96054c9..0449e95986 100644 --- a/Libs/DICOM/Core/ctkDICOMRetrieve.h +++ b/Libs/DICOM/Core/ctkDICOMRetrieve.h @@ -31,70 +31,130 @@ // CTK Core includes #include "ctkDICOMDatabase.h" +#include "ctkErrorLogLevel.h" class ctkDICOMRetrievePrivate; +class ctkDICOMTaskResults; /// \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 taskUID READ taskUID WRITE setTaskUID); public: explicit ctkDICOMRetrieve(QObject* parent = 0); virtual ~ctkDICOMRetrieve(); /// Set methods for connectivity - /// CTK_AE - the AE string by which the peer host might + /// 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; + void setCalledAETitle(const QString& calledAETitle); + QString calledAETitle() const; /// peer hostname being connected to - Q_INVOKABLE void setHost( const QString& host ); - Q_INVOKABLE QString host() const; + 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(const int timeout); + int connectionTimeout() const; + + /// operation is canceled? Q_INVOKABLE bool wasCanceled(); + + /// Log level for dcmtk. Default: Error. + Q_INVOKABLE void setDCMTKLogLevel(const ctkErrorLogLevel::LogLevel& level); + Q_INVOKABLE ctkErrorLogLevel::LogLevel DCMTKLogLevel() const; + /// 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 get operation. + Q_INVOKABLE QList taskResultsList() const; + Q_INVOKABLE void addTaskResults(ctkDICOMTaskResults* results); + Q_INVOKABLE void deleteTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void clearTaskResults(); + Q_INVOKABLE void setTaskUID(const QString& taskUID); + Q_INVOKABLE QString taskUID() const; + + /// Patient ID from from the last get operation. + QString patientID() const; + + /// Study instance UID from from the last get operation. + QString studyInstanceUID() const; + + /// Series instance UID from from the last get operation. + QString seriesInstanceUID() 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(); @@ -105,14 +165,16 @@ public Q_SLOTS: /// Signal is emitted inside the retrieve() function. It sends the different step /// the function is at. void progress(const QString& message); - /// Signal is emitted inside the retrieve() function. It sends + /// Signal is emitted inside the retrieve() function. It sends /// detailed feedback for debugging void debug(const QString& message); /// Signal is emitted inside the retrieve() function. It send any error messages void error(const QString& message); - /// Signal is emitted inside the retrieve() function when finished with value + /// 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 progressBarTaskDetail(ctkDICOMTaskResults*); protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveTask.cpp b/Libs/DICOM/Core/ctkDICOMRetrieveTask.cpp new file mode 100644 index 0000000000..d830b182ae --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveTask.cpp @@ -0,0 +1,326 @@ +/*========================================================================= + + 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 "ctkDICOMRetrieveTask_p.h" +#include "ctkLogger.h" + +static ctkLogger logger ( "org.commontk.dicom.ctkDICOMRetrieveTask" ); + +//------------------------------------------------------------------------------ +// ctkDICOMRetrieveTaskPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveTaskPrivate::ctkDICOMRetrieveTaskPrivate(ctkDICOMRetrieveTask* object) + : q_ptr(object) +{ + this->Retrieve = new ctkDICOMRetrieve; + this->Server = new ctkDICOMServer; + this->RetrieveLevel = ctkDICOMRetrieveTask::DICOMLevel::Studies; + + this->PatientID = ""; + this->StudyInstanceUID = ""; + this->SeriesInstanceUID = ""; + this->SOPInstanceUID = ""; +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveTaskPrivate::~ctkDICOMRetrieveTaskPrivate() +{ + if (this->Retrieve) + { + delete this->Retrieve; + this->Retrieve = nullptr; + } + + if (this->Server) + { + delete this->Server; + this->Server = nullptr; + } +} + +//------------------------------------------------------------------------------ +// ctkDICOMRetrieveTask methods + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveTask::ctkDICOMRetrieveTask() + : d_ptr(new ctkDICOMRetrieveTaskPrivate(this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveTask::ctkDICOMRetrieveTask(ctkDICOMRetrieveTaskPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveTask::~ctkDICOMRetrieveTask() +{ +} + +//------------------------------------------------------------------------------ +void ctkDICOMRetrieveTask::setRetrieveLevel(const DICOMLevel& retrieveLevel) +{ + Q_D(ctkDICOMRetrieveTask); + d->RetrieveLevel = retrieveLevel; +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieveTask::DICOMLevel ctkDICOMRetrieveTask::retrieveLevel() const +{ + Q_D(const ctkDICOMRetrieveTask); + return d->RetrieveLevel; +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::setTaskUID(const QString &taskUID) +{ + Q_D(const ctkDICOMRetrieveTask); + + Superclass::setTaskUID(taskUID); + d->Retrieve->setTaskUID(taskUID); +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::setStop(const bool& stop) +{ + Q_D(const ctkDICOMRetrieveTask); + ctkAbstractTask::setStop(stop); + d->Retrieve->cancel(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::run() +{ + Q_D(const ctkDICOMRetrieveTask); + if (this->isStopped() || !d->Server) + { + this->setIsFinished(true); + emit canceled(); + return; + } + + this->setIsRunning(true); + emit started(); + + logger.debug("ctkDICOMRetrieveTask : running task on thread id " + + QString::number(reinterpret_cast(QThread::currentThreadId()), 16)); + + switch (d->Server->retrieveProtocol()) + { + case ctkDICOMServer::CGET: + switch(d->RetrieveLevel) + { + case DICOMLevel::Studies: + if (!d->Retrieve->getStudy(d->StudyInstanceUID, + d->PatientID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + case DICOMLevel::Series: + if (!d->Retrieve->getSeries(d->StudyInstanceUID, + d->SeriesInstanceUID, + d->PatientID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + case DICOMLevel::Instances: + if (!d->Retrieve->getSOPInstance(d->StudyInstanceUID, + d->SeriesInstanceUID, + d->SOPInstanceUID, + d->PatientID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + } + break; + case ctkDICOMServer::CMOVE: + switch(d->RetrieveLevel) + { + case DICOMLevel::Studies: + if (!d->Retrieve->moveStudy(d->StudyInstanceUID, + d->PatientID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + case DICOMLevel::Series: + if (!d->Retrieve->moveSeries(d->StudyInstanceUID, + d->SeriesInstanceUID, + d->PatientID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + case DICOMLevel::Instances: + if (!d->Retrieve->moveSOPInstance(d->StudyInstanceUID, + d->SeriesInstanceUID, + d->SOPInstanceUID, + d->PatientID)) + { + this->setIsFinished(true); + emit canceled(); + return; + } + break; + } + break; + //case ctkDICOMServer::WADO: // To Do + } + + this->setIsFinished(true); + emit finished(); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMRetrieveTask::server()const +{ + Q_D(const ctkDICOMRetrieveTask); + return d->Server; +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::setServer(ctkDICOMServer* server) +{ + Q_D(ctkDICOMRetrieveTask); + if (!server) + { + logger.debug("server is null"); + return; + } + + if (d->Server == server) + { + return; + } + + d->Server->deepCopy(server); + + d->Retrieve->setConnectionName(d->Server->connectionName()); + d->Retrieve->setCallingAETitle(d->Server->callingAETitle()); + d->Retrieve->setCalledAETitle(d->Server->calledAETitle()); + d->Retrieve->setHost(d->Server->host()); + d->Retrieve->setPort(d->Server->port()); + d->Retrieve->setConnectionTimeout(d->Server->connectionTimeout()); + d->Retrieve->setMoveDestinationAETitle(d->Server->moveDestinationAETitle()); + d->Retrieve->setKeepAssociationOpen(d->Server->keepAssociationOpen()); +} + +//---------------------------------------------------------------------------- +QList ctkDICOMRetrieveTask::taskResultsList() const +{ + Q_D(const ctkDICOMRetrieveTask); + return d->Retrieve->taskResultsList(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::deleteTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(const ctkDICOMRetrieveTask); + d->Retrieve->deleteTaskResults(taskResults); +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(const ctkDICOMRetrieveTask); + d->Retrieve->deleteSeriesTaskResults(taskResults); +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::setPatientID(const QString &patientID) +{ + Q_D(ctkDICOMRetrieveTask); + d->PatientID = patientID; +} + +//---------------------------------------------------------------------------- +QString ctkDICOMRetrieveTask::patientID() const +{ + Q_D(const ctkDICOMRetrieveTask); + return d->PatientID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::setStudyInstanceUID(const QString& studyInstanceUID) +{ + Q_D(ctkDICOMRetrieveTask); + d->StudyInstanceUID = studyInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieveTask::studyInstanceUID() const +{ + Q_D(const ctkDICOMRetrieveTask); + return d->StudyInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::setSeriesInstanceUID(const QString& seriesInstanceUID) +{ + Q_D(ctkDICOMRetrieveTask); + d->SeriesInstanceUID = seriesInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieveTask::seriesInstanceUID() const +{ + Q_D(const ctkDICOMRetrieveTask); + return d->SeriesInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMRetrieveTask::setSOPInstanceUID(const QString& sopInstanceUID) +{ + Q_D(ctkDICOMRetrieveTask); + d->SOPInstanceUID = sopInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMRetrieveTask::sopInstanceUID() const +{ + Q_D(const ctkDICOMRetrieveTask); + return d->SOPInstanceUID; +} + +//------------------------------------------------------------------------------ +ctkDICOMRetrieve *ctkDICOMRetrieveTask::retriever() const +{ + Q_D(const ctkDICOMRetrieveTask); + return d->Retrieve; +} diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveTask.h b/Libs/DICOM/Core/ctkDICOMRetrieveTask.h new file mode 100644 index 0000000000..711bb0e57c --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveTask.h @@ -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). + +=========================================================================*/ + +#ifndef __ctkDICOMRetrieveTask_h +#define __ctkDICOMRetrieveTask_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" + +#include + +class ctkDICOMRetrieve; +class ctkDICOMRetrieveTaskPrivate; +class ctkDICOMServer; +class ctkDICOMTaskResults; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMRetrieveTask : public ctkAbstractTask +{ + 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(DICOMLevel retrieveLevel READ retrieveLevel WRITE setRetrieveLevel); + +public: + typedef ctkAbstractTask Superclass; + explicit ctkDICOMRetrieveTask(); + virtual ~ctkDICOMRetrieveTask(); + + enum DICOMLevel{ + Studies, + Series, + Instances + }; + + /// Retrieve Level + void setRetrieveLevel(const DICOMLevel& retrieveLevel); + DICOMLevel retrieveLevel() const; + + /// Task UID + void setTaskUID(const QString& taskUID); + + /// Stop task + void setStop(const bool& stop); + + /// Execute task + void run(); + + /// Server + Q_INVOKABLE ctkDICOMServer* server() const; + Q_INVOKABLE void setServer(ctkDICOMServer* server); + + /// 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 datasets from the last retrieve. + Q_INVOKABLE QList taskResultsList() const; + Q_INVOKABLE void deleteTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults); + + /// Retriever + 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. + ctkDICOMRetrieveTask(ctkDICOMRetrieveTaskPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMRetrieveTask); + Q_DISABLE_COPY(ctkDICOMRetrieveTask); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveTask_p.h b/Libs/DICOM/Core/ctkDICOMRetrieveTask_p.h new file mode 100644 index 0000000000..b8f30244a2 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMRetrieveTask_p.h @@ -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. + + 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 __ctkDICOMRetrieveTaskPrivate_h +#define __ctkDICOMRetrieveTaskPrivate_h + +// ctkDICOMCore includes +#include "ctkDICOMRetrieve.h" +#include "ctkDICOMServer.h" + +// ctkDICOMCore includes +#include "ctkDICOMRetrieveTask.h" + +//------------------------------------------------------------------------------ +class ctkDICOMRetrieveTaskPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMRetrieveTask) + +protected: + ctkDICOMRetrieveTask* const q_ptr; + +public: + ctkDICOMRetrieveTaskPrivate(ctkDICOMRetrieveTask* object); + ~ctkDICOMRetrieveTaskPrivate(); + + ctkDICOMServer *Server; + ctkDICOMRetrieve *Retrieve; + + QString PatientID; + QString StudyInstanceUID; + QString SeriesInstanceUID; + QString SOPInstanceUID; + + ctkDICOMRetrieveTask::DICOMLevel RetrieveLevel; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMServer.cpp b/Libs/DICOM/Core/ctkDICOMServer.cpp new file mode 100644 index 0000000000..0f025b8130 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMServer.cpp @@ -0,0 +1,348 @@ +/*========================================================================= + + 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 "ctkDICOMServer.h" +#include "ctkLogger.h" + +// Qt includes +#include + +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(); + + QString ConnectionName; + bool QueryRetrieveEnabled; + bool StorageEnabled; + QString CallingAETitle; + QString CalledAETitle; + QString Host; + int Port; + ctkDICOMServer::RetrieveProtocol RetrieveProtocol; + bool KeepAssociationOpen; + QString MoveDestinationAETitle; + int ConnectionTimeout; + ctkDICOMServer* ProxyServer; +}; + +//------------------------------------------------------------------------------ +// ctkDICOMServerPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMServerPrivate::ctkDICOMServerPrivate(ctkDICOMServer& obj) + : q_ptr(&obj) +{ + this->QueryRetrieveEnabled = true; + this->StorageEnabled = true; + this->KeepAssociationOpen = false; + this->ConnectionTimeout = 10; + this->RetrieveProtocol = ctkDICOMServer::RetrieveProtocol::CGET; + this->ProxyServer = nullptr; +} + +//------------------------------------------------------------------------------ +ctkDICOMServerPrivate::~ctkDICOMServerPrivate() +{ + if (this->ProxyServer) + { + delete this->ProxyServer; + this->ProxyServer = nullptr; + } +} + +//------------------------------------------------------------------------------ +// ctkDICOMServer methods + +//------------------------------------------------------------------------------ +ctkDICOMServer::ctkDICOMServer(QObject* parent) + : QObject(parent), + d_ptr(new ctkDICOMServerPrivate(*this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMServer::~ctkDICOMServer() +{ +} + +//------------------------------------------------------------------------------ +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(QString protocolString) +{ + Q_D(ctkDICOMServer); + + if (protocolString == "CGET") + { + d->RetrieveProtocol = RetrieveProtocol::CGET; + } + else if (protocolString == "CMOVE") + { + d->RetrieveProtocol = RetrieveProtocol::CMOVE; + } + /*else if (protocolString == "WADO") + { + 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: + 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(const bool keepOpen) +{ + Q_D(ctkDICOMServer); + d->KeepAssociationOpen = keepOpen; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMServer::keepAssociationOpen() +{ + Q_D(const ctkDICOMServer); + return d->KeepAssociationOpen; +} + +//----------------------------------------------------------------------------- +void ctkDICOMServer::setConnectionTimeout(const int timeout) +{ + Q_D(ctkDICOMServer); + d->ConnectionTimeout = timeout; +} + +//----------------------------------------------------------------------------- +int ctkDICOMServer::connectionTimeout() +{ + Q_D(const ctkDICOMServer); + return d->ConnectionTimeout; +} + +//----------------------------------------------------------------------------- +void ctkDICOMServer::setProxyServer(ctkDICOMServer* proxyServer) +{ + Q_D(ctkDICOMServer); + if (!proxyServer) + { + return; + } + + if (d->ProxyServer == proxyServer) + { + return; + } + + if (!d->ProxyServer) + { + d->ProxyServer = new ctkDICOMServer; + } + + d->ProxyServer->deepCopy(proxyServer); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMServer::proxyServer() const +{ + Q_D(const ctkDICOMServer); + return d->ProxyServer; +} + +//---------------------------------------------------------------------------- +void ctkDICOMServer::deepCopy(ctkDICOMServer *server) +{ + if (!server) + { + logger.debug("ctkDICOMServer::deepCopy: server is null"); + return; + } + + this->setConnectionName(server->connectionName()); + this->setQueryRetrieveEnabled(server->queryRetrieveEnabled()); + this->setStorageEnabled(server->storageEnabled()); + this->setCallingAETitle(server->callingAETitle()); + this->setCalledAETitle(server->calledAETitle()); + this->setHost(server->host()); + this->setPort(server->port()); + this->setRetrieveProtocol(server->retrieveProtocol()); + this->setMoveDestinationAETitle(server->moveDestinationAETitle()); + this->setKeepAssociationOpen(server->keepAssociationOpen()); + this->setConnectionTimeout(server->connectionTimeout()); + this->setProxyServer(server->proxyServer()); +} diff --git a/Libs/DICOM/Core/ctkDICOMServer.h b/Libs/DICOM/Core/ctkDICOMServer.h new file mode 100644 index 0000000000..f9dc13e1e9 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMServer.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. + + 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 + +#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(); + + /// Set methods for connectivity + 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 - e.g. 11112 + 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(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(const bool keepOpen); + bool keepAssociationOpen(); + /// connection timeout in seconds, default 10 s. + void setConnectionTimeout(const int timeout); + int connectionTimeout(); + /// proxy server + Q_INVOKABLE ctkDICOMServer* proxyServer() const; + Q_INVOKABLE void setProxyServer(ctkDICOMServer* proxyServer); + + /// deep copy + Q_INVOKABLE void deepCopy(ctkDICOMServer* server); + +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..6f9ad1beda --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListener.cpp @@ -0,0 +1,460 @@ +/*========================================================================= + + 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 "ctkDICOMStorageListener.h" +#include "ctkDICOMTaskResults.h" +#include "ctkLogger.h" + +// DCMTK includes +#include "dcmtk/dcmnet/dstorscp.h" /* 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() {}; + + virtual OFCondition acceptAssociations() + { + return DcmSCP::acceptAssociations(); + } + + virtual OFBool stopAfterCurrentAssociation() + { + if (!this->listener || this->listener->wasCanceled()) + { + return OFTrue; + } + + return OFFalse; + } + + virtual OFBool stopAfterConnectionTimeout() + { + if (!this->listener || this->listener->wasCanceled()) + { + return OFTrue; + } + + return OFFalse; + } + + virtual OFCondition 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->taskUID().isEmpty() && !this->listener->wasCanceled()) + { + ctkDICOMTaskResults* taskResults = new ctkDICOMTaskResults; + taskResults->setTypeOfTask(ctkDICOMTaskResults::TaskType::StoreSeries); + taskResults->setStudyInstanceUID(studyUID.c_str()); + taskResults->setSeriesInstanceUID(seriesUID.c_str()); + taskResults->setSOPInstanceUID(instanceUID.c_str()); + taskResults->setConnectionName(this->listener->AETitle()); + taskResults->setDataset(reqDataset); + taskResults->setTaskUID(this->listener->taskUID()); + taskResults->setCopyFile(true); + + this->listener->addTaskResults(taskResults); + emit this->listener->progressBarTaskDetail(taskResults); + } + // 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; + } +}; + +//------------------------------------------------------------------------------ +class ctkDICOMStorageListenerPrivate +{ +public: + ctkDICOMStorageListenerPrivate(); + ~ctkDICOMStorageListenerPrivate(); + + QString findFile(const QStringList& nameFilters, const QString& subDir)const; + QString defaultConfigFile() const; + + QString ConnectionName; + QString AETitle; + int Port; + QString TaskUID; + QList TaskResultsList; + + 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); +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerPrivate::~ctkDICOMStorageListenerPrivate() +{ +} + +//------------------------------------------------------------------------------ +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 + + this->setDCMTKLogLevel(logger.logLevel()); +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListener::~ctkDICOMStorageListener() +{ + this->clearTaskResults(); +} + +//----------------------------------------------------------------------------- +void ctkDICOMStorageListener::setDCMTKLogLevel(const ctkErrorLogLevel::LogLevel& level) +{ + OFLogger::LogLevel dcmtkLogLevel = OFLogger::OFF_LOG_LEVEL; + if (level == ctkErrorLogLevel::LogLevel::Fatal) + { + dcmtkLogLevel = OFLogger::FATAL_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Critical || + level == ctkErrorLogLevel::LogLevel::Error) + { + dcmtkLogLevel = OFLogger::ERROR_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Warning) + { + dcmtkLogLevel = OFLogger::WARN_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Info) + { + dcmtkLogLevel = OFLogger::INFO_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Debug) + { + dcmtkLogLevel = OFLogger::DEBUG_LOG_LEVEL; + } + else if (level == ctkErrorLogLevel::LogLevel::Trace || + level == ctkErrorLogLevel::LogLevel::Status) + { + dcmtkLogLevel = OFLogger::TRACE_LOG_LEVEL; + } + + OFLog::configure(dcmtkLogLevel); +} + +//----------------------------------------------------------------------------- +ctkErrorLogLevel::LogLevel ctkDICOMStorageListener::DCMTKLogLevel() const +{ + return logger.logLevel(); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMStorageListener::listen() +{ + Q_D(ctkDICOMStorageListener); + if (!this->initializeSCU()) + { + return false; + } + + OFCondition status = d->SCU.listen(); + if (status.bad() || d->Canceled) + { + logger.error(QString("Cannot start SCP and listen 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(const 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::taskResultsList() const +{ + Q_D(const ctkDICOMStorageListener); + return d->TaskResultsList; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::addTaskResults(ctkDICOMTaskResults* results) +{ + Q_D(ctkDICOMStorageListener); + d->TaskResultsList.append(results); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::deleteTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMStorageListener); + if (!taskResults) + { + return; + } + + d->TaskResultsList.removeOne(taskResults); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMStorageListener); + if (!taskResults) + { + return; + } + + foreach (ctkDICOMTaskResults* seriesTaskResults, d->TaskResultsList) + { + if (!seriesTaskResults || + seriesTaskResults == taskResults || + seriesTaskResults->typeOfTask() != taskResults->typeOfTask() || + seriesTaskResults->studyInstanceUID() != taskResults->studyInstanceUID() || + seriesTaskResults->seriesInstanceUID() != taskResults->seriesInstanceUID()) + { + continue; + } + + this->deleteTaskResults(seriesTaskResults); + } + + this->deleteTaskResults(taskResults); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::clearTaskResults() +{ + Q_D(ctkDICOMStorageListener); + qDeleteAll(d->TaskResultsList); + d->TaskResultsList.clear(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListener::setTaskUID(const QString &taskUID) +{ + Q_D(ctkDICOMStorageListener); + d->TaskUID = taskUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMStorageListener::taskUID() const +{ + Q_D(const ctkDICOMStorageListener); + return d->TaskUID; +} diff --git a/Libs/DICOM/Core/ctkDICOMStorageListener.h b/Libs/DICOM/Core/ctkDICOMStorageListener.h new file mode 100644 index 0000000000..930692ca89 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListener.h @@ -0,0 +1,116 @@ +/*========================================================================= + + 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 + +// CTK includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkErrorLogLevel.h" + +class ctkDICOMStorageListenerPrivate; +class ctkDICOMTaskResults; + +/// \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(const int timeout); + int connectionTimeout(); + + /// Access the list of datasets from the last get operation. + Q_INVOKABLE QList taskResultsList() const; + Q_INVOKABLE void addTaskResults(ctkDICOMTaskResults* results); + Q_INVOKABLE void deleteTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void clearTaskResults(); + Q_INVOKABLE void setTaskUID(const QString& taskUID); + Q_INVOKABLE QString taskUID() const; + + /// Log level for dcmtk. Default: Error. + Q_INVOKABLE void setDCMTKLogLevel(const ctkErrorLogLevel::LogLevel& level); + Q_INVOKABLE ctkErrorLogLevel::LogLevel DCMTKLogLevel() 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(const bool& error); + /// Signal is emitted inside the listener() function when a frame has been fetched + void progressBarTaskDetail(ctkDICOMTaskResults*); + +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/ctkDICOMStorageListenerTask.cpp b/Libs/DICOM/Core/ctkDICOMStorageListenerTask.cpp new file mode 100644 index 0000000000..74e28d7bdb --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerTask.cpp @@ -0,0 +1,187 @@ +/*========================================================================= + + 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 "ctkDICOMStorageListenerTask_p.h" +#include "ctkLogger.h" + +static ctkLogger logger ( "org.commontk.dicom.ctkDICOMStorageListenerTask" ); + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerTaskPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerTaskPrivate::ctkDICOMStorageListenerTaskPrivate(ctkDICOMStorageListenerTask* object) + : q_ptr(object) +{ + this->Listener = new ctkDICOMStorageListener; +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerTaskPrivate::~ctkDICOMStorageListenerTaskPrivate() +{ + if (this->Listener) + { + delete this->Listener; + this->Listener = nullptr; + } +} + +//------------------------------------------------------------------------------ +// ctkDICOMStorageListenerTask methods + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerTask::ctkDICOMStorageListenerTask() + : d_ptr(new ctkDICOMStorageListenerTaskPrivate(this)) +{ + this->Persistent = true; +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerTask::ctkDICOMStorageListenerTask(ctkDICOMStorageListenerTaskPrivate* pimpl) + : d_ptr(pimpl) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListenerTask::~ctkDICOMStorageListenerTask() +{ +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerTask::setTaskUID(const QString &taskUID) +{ + Q_D(const ctkDICOMStorageListenerTask); + + Superclass::setTaskUID(taskUID); + d->Listener->setTaskUID(taskUID); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerTask::setStop(const bool& stop) +{ + Q_D(const ctkDICOMStorageListenerTask); + ctkAbstractTask::setStop(stop); + d->Listener->cancel(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerTask::setPort(const int port) +{ + Q_D(ctkDICOMStorageListenerTask); + d->Listener->setPort(port); +} + +//---------------------------------------------------------------------------- +int ctkDICOMStorageListenerTask::port() const +{ + Q_D(const ctkDICOMStorageListenerTask); + return d->Listener->port(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerTask::setAETitle(const QString &AETitle) +{ + Q_D(ctkDICOMStorageListenerTask); + d->Listener->setAETitle(AETitle); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMStorageListenerTask::AETitle() const +{ + Q_D(const ctkDICOMStorageListenerTask); + return d->Listener->AETitle(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerTask::setConnectionTimeout(const int timeout) +{ + Q_D(ctkDICOMStorageListenerTask); + d->Listener->setConnectionTimeout(timeout); +} + +//---------------------------------------------------------------------------- +int ctkDICOMStorageListenerTask::connectionTimeout() +{ + Q_D(const ctkDICOMStorageListenerTask); + return d->Listener->connectionTimeout(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStorageListenerTask::run() +{ + Q_D(ctkDICOMStorageListenerTask); + if (this->isStopped()) + { + this->setIsFinished(true); + emit canceled(); + return; + } + + this->setIsRunning(true); + emit started(); + + logger.debug("ctkDICOMStorageListenerTask : running task on thread id " + + QString::number(reinterpret_cast(QThread::currentThreadId()), 16)); + + if (!d->Listener->listen()) + { + this->setIsFinished(true); + emit canceled(); + return; + } + + this->setIsFinished(true); + emit finished(); +} + +//------------------------------------------------------------------------------ +QList ctkDICOMStorageListenerTask::taskResultsList() const +{ + Q_D(const ctkDICOMStorageListenerTask); + return d->Listener->taskResultsList(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListenerTask::deleteTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(const ctkDICOMStorageListenerTask); + d->Listener->deleteTaskResults(taskResults); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStorageListenerTask::deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults) +{ + Q_D(const ctkDICOMStorageListenerTask); + d->Listener->deleteSeriesTaskResults(taskResults); +} + +//------------------------------------------------------------------------------ +ctkDICOMStorageListener *ctkDICOMStorageListenerTask::listener() const +{ + Q_D(const ctkDICOMStorageListenerTask); + return d->Listener; +} diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerTask.h b/Libs/DICOM/Core/ctkDICOMStorageListenerTask.h new file mode 100644 index 0000000000..248e979817 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerTask.h @@ -0,0 +1,97 @@ +/*========================================================================= + + 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 __ctkDICOMStorageListenerTask_h +#define __ctkDICOMStorageListenerTask_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" + +#include + +class ctkDICOMStorageListener; +class ctkDICOMStorageListenerTaskPrivate; +class ctkDICOMTaskResults; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMStorageListenerTask : public ctkAbstractTask +{ + 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: + typedef ctkAbstractTask Superclass; + explicit ctkDICOMStorageListenerTask(); + virtual ~ctkDICOMStorageListenerTask(); + + /// Task UID + void setTaskUID(const QString& taskUID); + + /// Stop task + void setStop(const bool& stop); + + /// Port, default: 11112 + void setPort(const int port); + int port() const; + + /// AETitle, default: CTKSTORE + void setAETitle(const QString& AETitle); + QString AETitle() const; + + /// Connection timeout, default 3 sec. + void setConnectionTimeout(const int timeout); + int connectionTimeout(); + + /// Execute task + void run(); + + /// Access the list of datasets from the last retrieve. + Q_INVOKABLE QList taskResultsList() const; + Q_INVOKABLE void deleteTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults); + + /// Retriever + Q_INVOKABLE ctkDICOMStorageListener* listener() 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. + ctkDICOMStorageListenerTask(ctkDICOMStorageListenerTaskPrivate* pimpl); + +private: + Q_DECLARE_PRIVATE(ctkDICOMStorageListenerTask); + Q_DISABLE_COPY(ctkDICOMStorageListenerTask); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerTask_p.h b/Libs/DICOM/Core/ctkDICOMStorageListenerTask_p.h new file mode 100644 index 0000000000..36a0d53e6f --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerTask_p.h @@ -0,0 +1,49 @@ +/*========================================================================= + + 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 __ctkDICOMStorageListenerTaskPrivate_h +#define __ctkDICOMStorageListenerTaskPrivate_h + +// ctkDICOMCore includes +#include "ctkDICOMStorageListener.h" + +// ctkDICOMCore includes +#include "ctkDICOMStorageListenerTask.h" + +//------------------------------------------------------------------------------ +class ctkDICOMStorageListenerTaskPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(ctkDICOMStorageListenerTask) + +protected: + ctkDICOMStorageListenerTask* const q_ptr; + +public: + ctkDICOMStorageListenerTaskPrivate(ctkDICOMStorageListenerTask* object); + ~ctkDICOMStorageListenerTaskPrivate(); + + ctkDICOMStorageListener *Listener; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMTaskPool.cpp b/Libs/DICOM/Core/ctkDICOMTaskPool.cpp new file mode 100644 index 0000000000..5ff4f86509 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMTaskPool.cpp @@ -0,0 +1,1472 @@ +/*========================================================================= + + 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 "ctkDICOMTaskPool.h" +#include "ctkDICOMTaskPool_p.h" +#include "ctkDICOMQuery.h" +#include "ctkDICOMQueryTask.h" +#include "ctkDICOMRetrieve.h" +#include "ctkDICOMRetrieveTask.h" +#include "ctkDICOMServer.h" +#include "ctkDICOMStorageListener.h" +#include "ctkDICOMStorageListenerTask.h" +#include "ctkDICOMTaskResults.h" +#include "ctkDICOMUtil.h" +#include "ctkLogger.h" + +#include + + +static ctkLogger logger ( "org.commontk.dicom.DICOMTaskPool" ); + +//------------------------------------------------------------------------------ +// ctkDICOMTaskPoolPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMTaskPoolPrivate::ctkDICOMTaskPoolPrivate(ctkDICOMTaskPool& obj) + : q_ptr(&obj) +{ + ctk::setDICOMLogLevel(ctkErrorLogLevel::Info); + + this->DicomDatabase = nullptr; + + this->Indexer = QSharedPointer (new ctkDICOMIndexer); + + this->Indexer->setBackgroundImportEnabled(true); + this->ThreadPool = QSharedPointer (new QThreadPool()); + this->ThreadPool->setMaxThreadCount(20); + + this->RetryDelay = 100; + this->MaximumNumberOfRetry = 3; + this->MaximumPatientsQuery = 25; +} + +//------------------------------------------------------------------------------ +ctkDICOMTaskPoolPrivate::~ctkDICOMTaskPoolPrivate() +{ + Q_Q(ctkDICOMTaskPool); + + QObject::disconnect(this->Indexer.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + q, SIGNAL(progressTaskDetail(ctkDICOMTaskResults*))); + q->removeAllServers(); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskPoolPrivate::generateUniqueTaskUID() +{ + return QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskPoolPrivate::loggerQueryReport(ctkDICOMQueryTask *queryTask, + const QString& status) +{ + if (!queryTask) + { + return QString(""); + } + + switch (queryTask->queryLevel()) + { + case ctkDICOMQueryTask::DICOMLevel::Patients: + return QString("ctkDICOMTaskPool: query task at patients level %1.\n" + "TaskUID: %2\n" + "Server: %3") + .arg(status) + .arg(queryTask->taskUID()) + .arg(queryTask->server()->connectionName()); + break; + case ctkDICOMQueryTask::DICOMLevel::Studies: + return QString("ctkDICOMTaskPool: query task at studies level %1.\n" + "TaskUID: %2\n" + "Server: %3\n" + "PatientID: %4") + .arg(status) + .arg(queryTask->taskUID()) + .arg(queryTask->server()->connectionName()) + .arg(queryTask->patientID()); + break; + case ctkDICOMQueryTask::DICOMLevel::Series: + return QString("ctkDICOMTaskPool: query task at series level %1.\n" + "TaskUID: %2\n" + "Server: %3\n" + "PatientID: %4\n" + "StudyInstanceUID: %5") + .arg(status) + .arg(queryTask->taskUID()) + .arg(queryTask->server()->connectionName()) + .arg(queryTask->patientID()) + .arg(queryTask->studyInstanceUID()); + break; + case ctkDICOMQueryTask::DICOMLevel::Instances: + return QString("ctkDICOMTaskPool: query task at series level %1.\n" + "TaskUID: %2\n" + "Server: %3\n" + "PatientID: %4\n" + "StudyInstanceUID: %5\n" + "SeriesInstanceUID: %6") + .arg(status) + .arg(queryTask->taskUID()) + .arg(queryTask->server()->connectionName()) + .arg(queryTask->patientID()) + .arg(queryTask->studyInstanceUID()) + .arg(queryTask->seriesInstanceUID()); + break; + } + + return QString(""); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskPoolPrivate::loggerRetrieveReport(ctkDICOMRetrieveTask *retrieveTask, + const QString& status) +{ + if (!retrieveTask) + { + return QString(""); + } + + switch (retrieveTask->retrieveLevel()) + { + case ctkDICOMRetrieveTask::DICOMLevel::Studies: + return QString("ctkDICOMTaskPool: retrieve task at studies level %1.\n" + "TaskUID: %2\n" + "Server: %3\n" + "StudyInstanceUID: %4") + .arg(status) + .arg(retrieveTask->taskUID()) + .arg(retrieveTask->server()->connectionName()) + .arg(retrieveTask->studyInstanceUID()); + break; + case ctkDICOMRetrieveTask::DICOMLevel::Series: + return QString("ctkDICOMTaskPool: retrieve task at series level %1.\n" + "TaskUID: %2\n" + "Server: %3\n" + "StudyInstanceUID: %4\n" + "SeriesInstanceUID: %5") + .arg(status) + .arg(retrieveTask->taskUID()) + .arg(retrieveTask->server()->connectionName()) + .arg(retrieveTask->studyInstanceUID()) + .arg(retrieveTask->seriesInstanceUID()); + break; + case ctkDICOMRetrieveTask::DICOMLevel::Instances: + return QString("ctkDICOMTaskPool: retrieve task at instances level %1.\n" + "TaskUID: %2\n" + "Server: %3\n" + "StudyInstanceUID: %4\n" + "SeriesInstanceUID: %5\n" + "SOPInstanceUID: %6") + .arg(status) + .arg(retrieveTask->taskUID()) + .arg(retrieveTask->server()->connectionName()) + .arg(retrieveTask->studyInstanceUID()) + .arg(retrieveTask->seriesInstanceUID()) + .arg(retrieveTask->sopInstanceUID()); + break; + } + + return QString(""); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskPoolPrivate::loggerListenerReport(ctkDICOMStorageListenerTask *listenerTask, + const QString& status) +{ + if (!listenerTask) + { + return QString(""); + } + + return QString("ctkDICOMTaskPool: listener task %1.\n" + "TaskUID: %2") + .arg(status) + .arg(listenerTask->taskUID()); +} + +//------------------------------------------------------------------------------ +ctkDICOMServer *ctkDICOMTaskPoolPrivate::getServerFromProxyServersByConnectionName(const QString &connectionName) +{ + foreach (QSharedPointer server, this->Servers) + { + ctkDICOMServer* proxyServer = server->proxyServer(); + if (proxyServer && proxyServer->connectionName() == connectionName) + { + return proxyServer; + } + } + + return nullptr; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMTaskPoolPrivate::isAThumbnailUIDs(const QString &studyInstanceUID, + const QString &seriesInstanceUID, + const QString &SOPInstanceUID) +{ + foreach (ThumbnailUID thumbnailUID, this->ThumbnailUIDs) + { + if (thumbnailUID.studyInstanceUID == studyInstanceUID && + thumbnailUID.seriesInstanceUID == seriesInstanceUID && + thumbnailUID.SOPInstanceUID == SOPInstanceUID) + { + return true; + } + } + + return false; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskPoolPrivate::init() +{ + Q_Q(ctkDICOMTaskPool); + + QObject::connect(this->Indexer.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + q, SIGNAL(progressTaskDetail(ctkDICOMTaskResults*))); +} + +//------------------------------------------------------------------------------ +// ctkDICOMTaskPool methods + +//------------------------------------------------------------------------------ +ctkDICOMTaskPool::ctkDICOMTaskPool(QObject* parentObject) + : Superclass(parentObject) + , d_ptr(new ctkDICOMTaskPoolPrivate(*this)) +{ + Q_D(ctkDICOMTaskPool); + + d->init(); +} + +//------------------------------------------------------------------------------ +ctkDICOMTaskPool::~ctkDICOMTaskPool() +{ + this->stopAllTasks(true); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::queryPatients(QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + foreach(QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + ctkDICOMQueryTask *task = new ctkDICOMQueryTask(); + task->setServer(server.data()); + task->querier()->setMaximumPatientsQuery(d->MaximumPatientsQuery); + task->setFilters(d->Filters); + task->setQueryLevel(ctkDICOMQueryTask::DICOMLevel::Patients); + task->setAutoDelete(false); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + + QString taskUID = d->generateUniqueTaskUID(); + task->setTaskUID(taskUID); + d->Tasks.insert(taskUID, task); + + d->ThreadPool->start(task, priority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::queryStudies(const QString& patientID, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + foreach(QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + ctkDICOMQueryTask *task = new ctkDICOMQueryTask(); + task->setServer(server.data()); + task->setFilters(d->Filters); + task->setQueryLevel(ctkDICOMQueryTask::DICOMLevel::Studies); + task->setPatientID(patientID); + task->setAutoDelete(false); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + + QString taskUID = d->generateUniqueTaskUID(); + task->setTaskUID(taskUID); + d->Tasks.insert(taskUID, task); + + d->ThreadPool->start(task, priority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::querySeries(const QString& patientID, + const QString& studyInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + foreach(QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + ctkDICOMQueryTask *task = new ctkDICOMQueryTask(); + task->setServer(server.data()); + task->setFilters(d->Filters); + task->setQueryLevel(ctkDICOMQueryTask::DICOMLevel::Series); + task->setPatientID(patientID); + task->setStudyInstanceUID(studyInstanceUID); + task->setAutoDelete(false); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + + QString taskUID = d->generateUniqueTaskUID(); + task->setTaskUID(taskUID); + d->Tasks.insert(taskUID, task); + + d->ThreadPool->start(task, priority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::queryInstances(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + foreach(QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + ctkDICOMQueryTask *task = new ctkDICOMQueryTask(); + task->setServer(server.data()); + task->setFilters(d->Filters); + task->setQueryLevel(ctkDICOMQueryTask::DICOMLevel::Instances); + task->setPatientID(patientID); + task->setStudyInstanceUID(studyInstanceUID); + task->setSeriesInstanceUID(seriesInstanceUID); + task->setAutoDelete(false); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + + QString taskUID = d->generateUniqueTaskUID(); + task->setTaskUID(taskUID); + d->Tasks.insert(taskUID, task); + + d->ThreadPool->start(task, priority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::retrieveStudy(const QString &patientID, + const QString &studyInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + foreach(QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + ctkDICOMRetrieveTask *task = new ctkDICOMRetrieveTask(); + task->setServer(server.data()); + task->setRetrieveLevel(ctkDICOMRetrieveTask::DICOMLevel::Studies); + task->setPatientID(patientID); + task->setStudyInstanceUID(studyInstanceUID); + task->setAutoDelete(false); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + + QString taskUID = d->generateUniqueTaskUID(); + task->setTaskUID(taskUID); + d->Tasks.insert(taskUID, task); + + d->ThreadPool->start(task, priority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::retrieveSeries(const QString &patientID, + const QString &studyInstanceUID, + const QString &seriesInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + foreach(QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + ctkDICOMRetrieveTask *task = new ctkDICOMRetrieveTask(); + task->setServer(server.data()); + task->setRetrieveLevel(ctkDICOMRetrieveTask::DICOMLevel::Series); + task->setPatientID(patientID); + task->setStudyInstanceUID(studyInstanceUID); + task->setSeriesInstanceUID(seriesInstanceUID); + task->setAutoDelete(false); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + QObject::connect(task->retriever(), SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), + this, SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), Qt::QueuedConnection); + + QString taskUID = d->generateUniqueTaskUID(); + task->setTaskUID(taskUID); + d->Tasks.insert(taskUID, task); + + d->ThreadPool->start(task, priority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::retrieveSOPInstance(const QString &patientID, + const QString &studyInstanceUID, + const QString &seriesInstanceUID, + const QString &SOPInstanceUID, + bool trackThumbnailUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + if (trackThumbnailUID) + { + ThumbnailUID centralThumbnail; + centralThumbnail.studyInstanceUID = studyInstanceUID; + centralThumbnail.seriesInstanceUID = seriesInstanceUID; + centralThumbnail.SOPInstanceUID = SOPInstanceUID; + d->ThumbnailUIDs.append(centralThumbnail); + } + + foreach(QSharedPointer server, d->Servers) + { + if (!server->queryRetrieveEnabled()) + { + continue; + } + + ctkDICOMRetrieveTask *task = new ctkDICOMRetrieveTask(); + task->setServer(server.data()); + task->setRetrieveLevel(ctkDICOMRetrieveTask::DICOMLevel::Instances); + task->setPatientID(patientID); + task->setStudyInstanceUID(studyInstanceUID); + task->setSeriesInstanceUID(seriesInstanceUID); + task->setSOPInstanceUID(SOPInstanceUID); + task->setAutoDelete(false); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + + QString taskUID = d->generateUniqueTaskUID(); + task->setTaskUID(taskUID); + d->Tasks.insert(taskUID, task); + + d->ThreadPool->start(task, priority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::startListener(const int port, + const QString &AETitle, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + ctkDICOMStorageListenerTask *task = new ctkDICOMStorageListenerTask(); + task->setPort(port); + task->setAETitle(AETitle); + task->setAutoDelete(false); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + QObject::connect(task->listener(), SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(onListenerProgressBarTaskDetail(ctkDICOMTaskResults*)), Qt::QueuedConnection); + + QString taskUID = d->generateUniqueTaskUID(); + task->setTaskUID(taskUID); + d->Tasks.insert(taskUID, task); + + d->ThreadPool->start(task, priority); +} + +//---------------------------------------------------------------------------- +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* ctkDICOMTaskPool::dicomDatabase()const +{ + Q_D(const ctkDICOMTaskPool); + return d->DicomDatabase.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMTaskPool::dicomDatabaseShared()const +{ + Q_D(const ctkDICOMTaskPool); + return d->DicomDatabase; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::setDicomDatabase(ctkDICOMDatabase& dicomDatabase) +{ + Q_D(ctkDICOMTaskPool); + d->DicomDatabase = QSharedPointer(&dicomDatabase, skipDelete); + if (d->Indexer) + { + d->Indexer->setDatabase(d->DicomDatabase.data()); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::setDicomDatabase(QSharedPointer dicomDatabase) +{ + Q_D(ctkDICOMTaskPool); + d->DicomDatabase = dicomDatabase; + if (d->Indexer) + { + d->Indexer->setDatabase(d->DicomDatabase.data()); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::setFilters(const QMap &filters) +{ + Q_D(ctkDICOMTaskPool); + d->Filters = filters; +} + +//---------------------------------------------------------------------------- +QMap ctkDICOMTaskPool::filters() const +{ + Q_D(const ctkDICOMTaskPool); + return d->Filters; +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::getNumberOfServers() +{ + Q_D(ctkDICOMTaskPool); + return d->Servers.size(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::getNumberOfQueryRetrieveServers() +{ + Q_D(ctkDICOMTaskPool); + int numberOfServers = 0; + foreach (QSharedPointer server, d->Servers) + { + if (server && server->queryRetrieveEnabled()) + { + numberOfServers++; + } + } + return numberOfServers; +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::getNumberOfStorageServers() +{ + Q_D(ctkDICOMTaskPool); + int numberOfServers = 0; + foreach (QSharedPointer server, d->Servers) + { + if (server && server->storageEnabled()) + { + numberOfServers++; + } + } + return numberOfServers; +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMTaskPool::getNthServer(int id) +{ + Q_D(ctkDICOMTaskPool); + if (id < 0 || id > d->Servers.size() - 1) + { + return nullptr; + } + return d->Servers[id].data(); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMTaskPool::getServer(const char *connectionName) +{ + Q_D(ctkDICOMTaskPool); + ctkDICOMServer* server = this->getNthServer(this->getServerIndexFromName(connectionName)); + if (!server) + { + server = d->getServerFromProxyServersByConnectionName(connectionName); + } + return server; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::addServer(ctkDICOMServer* server) +{ + Q_D(ctkDICOMTaskPool); + QSharedPointer QsharedServer = QSharedPointer(server); + d->Servers.push_back(QsharedServer); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::removeServer(const char *connectionName) +{ + this->removeNthServer(this->getServerIndexFromName(connectionName)); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::removeNthServer(int id) +{ + Q_D(ctkDICOMTaskPool); + if (id < 0 || id > d->Servers.size() - 1) + { + return; + } + + d->Servers.erase(d->Servers.begin() + id); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::removeAllServers() +{ + Q_D(ctkDICOMTaskPool); + d->Servers.clear(); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMTaskPool::getServerNameFromIndex(int id) +{ + Q_D(ctkDICOMTaskPool); + if (id < 0 || id > d->Servers.size() - 1) + { + return ""; + } + + QSharedPointer server = d->Servers[id]; + if (!server) + { + return ""; + } + + return server->connectionName(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::getServerIndexFromName(const char *connectionName) +{ + Q_D(ctkDICOMTaskPool); + if (!connectionName) + { + return -1; + } + for(int serverIndex = 0; serverIndex < d->Servers.size(); ++serverIndex) + { + QSharedPointer server = d->Servers[serverIndex]; + if (server && server->connectionName() == connectionName) + { + // found + return serverIndex; + } + } + return -1; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::waitForFinish() +{ + Q_D(ctkDICOMTaskPool); + + if (!d->ThreadPool) + { + return; + } + + while(d->ThreadPool->activeThreadCount() > this->numberOfPersistentTasks()) + { + QCoreApplication::processEvents(); + d->ThreadPool->waitForDone(300); + } + + // wait UI updates, which clean the tasks (To Do: we need to clean the design, task deletion have be independent from the UI updates) + QCoreApplication::processEvents(); + d->ThreadPool->waitForDone(300); + + // force inserting results from listener + this->onInsertListenerTaskResults(this->listenerTask()); + + // wait for any result being indexed. + d->Indexer->waitForImportFinished(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::totalTasks() +{ + Q_D(ctkDICOMTaskPool); + return d->Tasks.count(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::numberOfPersistentTasks() +{ + Q_D(ctkDICOMTaskPool); + int cont = 0; + foreach (ctkAbstractTask* task, d->Tasks) + { + if (task->isPersistent()) + { + cont++; + } + } + + return cont; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::stopAllTasksNotStarted() +{ + Q_D(ctkDICOMTaskPool); + + d->ThreadPool->clear(); + foreach (ctkAbstractTask* task, d->Tasks) + { + if (task->isRunning() || task->isFinished() || task->isPersistent()) + { + continue; + } + + task->setStop(true); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::stopAllTasks(bool stopPersistentTasks) +{ + Q_D(ctkDICOMTaskPool); + + d->ThreadPool->clear(); + foreach (ctkAbstractTask* task, d->Tasks) + { + if (task->isPersistent() && !stopPersistentTasks) + { + continue; + } + + // here for query and retrieve a sendCANCELRequest will be sent + // for the listener the loop in listen will be stopped + task->setStop(true); + if (!d->ThreadPool->tryTake(task)) + { + logger.debug(QString("ctkDICOMTaskPool::stopAllTasks : failed to cancel task %1").arg(task->taskUID())); + } + } + + if (stopPersistentTasks) + { + d->ThreadPool->waitForDone(1500); + } + else + { + d->ThreadPool->waitForDone(300); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::deleteTask(QString taskUID) +{ + Q_D(ctkDICOMTaskPool); + + QMap::iterator it = d->Tasks.find(taskUID); + if (it == d->Tasks.end()) + { + return; + } + + ctkAbstractTask* task = d->Tasks.value(taskUID); + if (!task) + { + return; + } + + logger.debug(QString("ctkDICOMTaskPool: deleting task object %1").arg(taskUID)); + + task->disconnect(); + d->Tasks.remove(taskUID); + task->deleteLater(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::deleteTaskResults(ctkDICOMTaskResults *taskResults) +{ + if (!taskResults) + { + return; + } + + ctkAbstractTask* task = this->getTaskByUID(taskResults->taskUID()); + ctkDICOMQueryTask* queryTask = qobject_cast(task); + if (queryTask) + { + queryTask->deleteTaskResults(taskResults); + } + + ctkDICOMRetrieveTask* retrieveTask = qobject_cast(task); + if (retrieveTask) + { + retrieveTask->deleteTaskResults(taskResults); + } + + ctkDICOMStorageListenerTask* listenerTask = qobject_cast(task); + if (listenerTask) + { + listenerTask->deleteTaskResults(taskResults); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults) +{ + if (!taskResults) + { + return; + } + + ctkAbstractTask* task = this->getTaskByUID(taskResults->taskUID()); + ctkDICOMQueryTask* queryTask = qobject_cast(task); + if (queryTask) + { + queryTask->deleteSeriesTaskResults(taskResults); + } + + ctkDICOMRetrieveTask* retrieveTask = qobject_cast(task); + if (retrieveTask) + { + retrieveTask->deleteSeriesTaskResults(taskResults); + } + + ctkDICOMStorageListenerTask* listenerTask = qobject_cast(task); + if (listenerTask) + { + listenerTask->deleteSeriesTaskResults(taskResults); + } +} + +//---------------------------------------------------------------------------- +ctkAbstractTask* ctkDICOMTaskPool::getTaskByUID(QString taskUID) +{ + Q_D(ctkDICOMTaskPool); + + QMap::iterator it = d->Tasks.find(taskUID); + if (it == d->Tasks.end()) + { + return nullptr; + } + + return d->Tasks.value(taskUID); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::deleteAllTasks() +{ + Q_D(ctkDICOMTaskPool); + + logger.debug("deleting all tasks"); + foreach (ctkAbstractTask* task, d->Tasks) + { + if (!task) + { + continue; + } + this->deleteTask(task->taskUID()); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::deleteStoppedTasks() +{ + Q_D(ctkDICOMTaskPool); + logger.debug("deleting stopped tasks"); + foreach (ctkAbstractTask* task, d->Tasks) + { + if (!task || !task->isStopped()) + { + continue; + } + this->deleteTask(task->taskUID()); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::stopTasks(const QString &studyInstanceUID, + const QString &seriesInstanceUID, + const QString &sopInstanceUID) +{ + Q_D(ctkDICOMTaskPool); + + foreach (ctkAbstractTask* task, d->Tasks) + { + ctkDICOMRetrieveTask* retrieveTask = qobject_cast(task); + if (retrieveTask) + { + if (retrieveTask->isFinished() || + retrieveTask->studyInstanceUID() != studyInstanceUID || + (!retrieveTask->seriesInstanceUID().isEmpty() && !seriesInstanceUID.isEmpty() && + retrieveTask->seriesInstanceUID() != seriesInstanceUID) || + (!retrieveTask->sopInstanceUID().isEmpty() && !sopInstanceUID.isEmpty() && + retrieveTask->sopInstanceUID() != sopInstanceUID)) + { + continue; + } + retrieveTask->setStop(true); + } + + ctkDICOMQueryTask* queryTask = qobject_cast(task); + if (queryTask) + { + if (queryTask->isFinished() || + queryTask->studyInstanceUID() != studyInstanceUID || + (!queryTask->seriesInstanceUID().isEmpty() && !seriesInstanceUID.isEmpty() && + queryTask->seriesInstanceUID() != seriesInstanceUID)) + { + continue; + } + + queryTask->setStop(true); + } + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::lowerPriorityToAllTasks() +{ + Q_D(ctkDICOMTaskPool); + + foreach (ctkAbstractTask* task, d->Tasks) + { + // Lower priority for all other retrieve tasks + if (d->ThreadPool->tryTake(task)) + { + d->ThreadPool->start(task, QThread::LowPriority); + } + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::raiseRetrieveFramesTasksPriorityForSeries(const QString &studyInstanceUID, + const QString &seriesInstanceUID, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + foreach (ctkAbstractTask* task, d->Tasks) + { + ctkDICOMRetrieveTask* retrieveTask = qobject_cast(task); + if (!retrieveTask || retrieveTask->isRunning() || retrieveTask->isFinished()) + { + continue; + } + + // Raise priority for the tasks associated to the selected thumbnail + if (retrieveTask->retrieveLevel() == ctkDICOMRetrieveTask::DICOMLevel::Series && + retrieveTask->studyInstanceUID() == studyInstanceUID && + retrieveTask->seriesInstanceUID() == seriesInstanceUID) + { + if (d->ThreadPool->tryTake(retrieveTask)) + { + d->ThreadPool->start(task, priority); + } + } + } +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::maximumThreadCount() const +{ + Q_D(const ctkDICOMTaskPool); + return d->ThreadPool->maxThreadCount(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::setMaximumThreadCount(const int &maximumThreadCount) +{ + Q_D(ctkDICOMTaskPool); + + d->ThreadPool->setMaxThreadCount(maximumThreadCount); +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::maximumNumberOfRetry() const +{ + Q_D(const ctkDICOMTaskPool); + return d->MaximumNumberOfRetry; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::setMaximumNumberOfRetry(const int &maximumNumberOfRetry) +{ + Q_D(ctkDICOMTaskPool); + d->MaximumNumberOfRetry = maximumNumberOfRetry; +} + +//---------------------------------------------------------------------------- +int ctkDICOMTaskPool::retryDelay() const +{ + Q_D(const ctkDICOMTaskPool); + return d->RetryDelay; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::setRetryDelay(const int &retryDelay) +{ + Q_D(ctkDICOMTaskPool); + d->RetryDelay = retryDelay; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskPool::setMaximumPatientsQuery(const int maximumPatientsQuery) +{ + Q_D(ctkDICOMTaskPool); + d->MaximumPatientsQuery = maximumPatientsQuery; +} + +//------------------------------------------------------------------------------ +int ctkDICOMTaskPool::maximumPatientsQuery() +{ + Q_D(const ctkDICOMTaskPool); + return d->MaximumPatientsQuery; +} + +//---------------------------------------------------------------------------- +ctkDICOMStorageListenerTask *ctkDICOMTaskPool::listenerTask() const +{ + Q_D(const ctkDICOMTaskPool); + foreach(ctkAbstractTask* task, d->Tasks) + { + ctkDICOMStorageListenerTask* listenerTask = qobject_cast(task); + if (listenerTask) + { + return listenerTask; + } + } + + return nullptr; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMTaskPool::isStorageListenerActive() const +{ + ctkDICOMStorageListenerTask* listenerTask = this->listenerTask(); + if (listenerTask && + listenerTask->isRunning() && + !listenerTask->isFinished() && + !listenerTask->isStopped()) + { + return true; + } + return false; +} + +//---------------------------------------------------------------------------- +ctkDICOMIndexer *ctkDICOMTaskPool::indexer() const +{ + Q_D(const ctkDICOMTaskPool); + return d->Indexer.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMTaskPool::indexerShared() const +{ + Q_D(const ctkDICOMTaskPool); + return d->Indexer; +} + +//---------------------------------------------------------------------------- +QThreadPool *ctkDICOMTaskPool::threadPool() const +{ + Q_D(const ctkDICOMTaskPool); + return d->ThreadPool.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMTaskPool::threadPoolShared() const +{ + Q_D(const ctkDICOMTaskPool); + return d->ThreadPool; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::onTaskStarted() +{ + Q_D(ctkDICOMTaskPool); + ctkAbstractTask* task = qobject_cast(this->sender()); + if (!task) + { + return; + } + + ctkDICOMQueryTask* queryTask = qobject_cast(this->sender()); + if (queryTask) + { + logger.debug(d->loggerQueryReport(queryTask, "started")); + emit this->taskStarted("Query"); + } + + ctkDICOMRetrieveTask* retrieveTask = qobject_cast(this->sender()); + if (retrieveTask) + { + logger.debug(d->loggerRetrieveReport(retrieveTask, "started")); + emit this->taskStarted("Retrieve"); + } + + ctkDICOMStorageListenerTask* listenerTask = qobject_cast(this->sender()); + if (listenerTask) + { + logger.debug(d->loggerListenerReport(listenerTask, "started")); + emit this->taskStarted("Listener"); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::onTaskFinished() +{ + Q_D(ctkDICOMTaskPool); + ctkAbstractTask* task = qobject_cast(this->sender()); + if (!task) + { + return; + } + + ctkDICOMQueryTask* queryTask = qobject_cast(this->sender()); + if (queryTask) + { + logger.debug(d->loggerQueryReport(queryTask, "finished")); + + if (queryTask->taskResultsList().count() > 0 && !queryTask->isStopped()) + { + d->Indexer->insertTaskResults(queryTask->taskResultsList()); + } + else + { + emit this->progressTaskDetail(nullptr); + } + emit this->taskFinished("Query"); + } + + ctkDICOMRetrieveTask* retrieveTask = qobject_cast(this->sender()); + if (retrieveTask) + { + logger.debug(d->loggerRetrieveReport(retrieveTask, "finished")); + + QList taskResultsList = retrieveTask->taskResultsList(); + ctkDICOMServer* server = retrieveTask->server(); + if (server && server->retrieveProtocol() == ctkDICOMServer::CMOVE && !retrieveTask->isStopped()) + { + // it is a move retrieve + // if the server owns a proxyServer: + // 1) notify UI + // 2) start a get operation to the proxy server. + + foreach (ctkDICOMTaskResults* taskResults, taskResultsList) + { + emit this->progressTaskDetail(taskResults); + } + + ctkDICOMServer* proxyServer = server->proxyServer(); + if (proxyServer && proxyServer->queryRetrieveEnabled()) + { + QString newTaskUID = d->generateUniqueTaskUID(); + ctkDICOMRetrieveTask* newRetrieveTask = new ctkDICOMRetrieveTask(); + newRetrieveTask->setServer(proxyServer); + newRetrieveTask->setRetrieveLevel(retrieveTask->retrieveLevel()); + newRetrieveTask->setPatientID(retrieveTask->patientID()); + newRetrieveTask->setStudyInstanceUID(retrieveTask->studyInstanceUID()); + newRetrieveTask->setSeriesInstanceUID(retrieveTask->seriesInstanceUID()); + newRetrieveTask->setSOPInstanceUID(retrieveTask->sopInstanceUID()); + newRetrieveTask->setTaskUID(newTaskUID); + newRetrieveTask->setAutoDelete(false); + + QObject::connect(newRetrieveTask, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(newRetrieveTask, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(newRetrieveTask, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + if (newRetrieveTask->retrieveLevel() == ctkDICOMRetrieveTask::DICOMLevel::Series) + { + QObject::connect(newRetrieveTask->retriever(), SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), + this, SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), Qt::QueuedConnection); + } + + d->Tasks.insert(newTaskUID, newRetrieveTask); + QThread::Priority priority = QThread::LowPriority; + if (newRetrieveTask->retriever()->getLastRetrieveType() == ctkDICOMRetrieve::RetrieveType::RetrieveSOPInstance) + { + priority = QThread::NormalPriority; + } + + d->ThreadPool->start(newRetrieveTask, priority); + } + else + { + this->onInsertListenerTaskResults(this->listenerTask()); + } + } + else if (taskResultsList.count() > 0 && !retrieveTask->isStopped()) + { + // it is a get retrieve -> insert the results in the database + d->Indexer->insertTaskResults(taskResultsList); + } + else + { + // no results from the retrieve + emit this->progressTaskDetail(nullptr); + } + emit this->taskFinished("Retrieve"); + } + + ctkDICOMStorageListenerTask* listenerTask = qobject_cast(this->sender()); + if (listenerTask) + { + logger.debug(d->loggerListenerReport(listenerTask, "finished")); + emit this->taskFinished("Listener"); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::onTaskCanceled() +{ + Q_D(ctkDICOMTaskPool); + + ctkAbstractTask* task = qobject_cast(this->sender()); + if (!task) + { + return; + } + + ctkDICOMQueryTask* queryTask = qobject_cast(task); + if (queryTask) + { + logger.debug(d->loggerQueryReport(queryTask, "canceled")); + emit this->taskCanceled("Query"); + + QString taskUID = queryTask->taskUID(); + if (queryTask->numberOfRetry() < d->MaximumNumberOfRetry && !queryTask->isStopped()) + { + ctkDICOMQueryTask* newQueryTask = new ctkDICOMQueryTask(); + newQueryTask->setServer(queryTask->server()); + newQueryTask->setFilters(d->Filters); + newQueryTask->setQueryLevel(queryTask->queryLevel()); + newQueryTask->setStudyInstanceUID(queryTask->studyInstanceUID()); + newQueryTask->setSeriesInstanceUID(queryTask->seriesInstanceUID()); + newQueryTask->setNumberOfRetry(queryTask->numberOfRetry() + 1); + newQueryTask->setTaskUID(taskUID); + newQueryTask->setAutoDelete(false); + + QTimer::singleShot(d->RetryDelay, this, [=] () {onTaskRetry(newQueryTask); }); + } + else if (!queryTask->isStopped()) + { + emit this->progressTaskDetail(nullptr); + logger.error(d->loggerQueryReport(queryTask, "failed")); + emit this->taskFailed("Query"); + } + + this->deleteTask(taskUID); + } + + ctkDICOMRetrieveTask* retrieveTask = qobject_cast(task); + if (retrieveTask) + { + logger.debug(d->loggerRetrieveReport(retrieveTask, "canceled")); + emit this->taskCanceled("Retrieve"); + + QString taskUID = retrieveTask->taskUID(); + if (retrieveTask->numberOfRetry() < d->MaximumNumberOfRetry && !retrieveTask->isStopped()) + { + ctkDICOMRetrieveTask* newRetrieveTask = new ctkDICOMRetrieveTask(); + newRetrieveTask->setServer(retrieveTask->server()); + newRetrieveTask->setRetrieveLevel(retrieveTask->retrieveLevel()); + newRetrieveTask->setStudyInstanceUID(retrieveTask->studyInstanceUID()); + newRetrieveTask->setSeriesInstanceUID(retrieveTask->seriesInstanceUID()); + newRetrieveTask->setSOPInstanceUID(retrieveTask->sopInstanceUID()); + newRetrieveTask->setNumberOfRetry(retrieveTask->numberOfRetry() + 1); + newRetrieveTask->setTaskUID(taskUID); + newRetrieveTask->setAutoDelete(false); + + QTimer::singleShot(d->RetryDelay, this, [=] () {onTaskRetry(newRetrieveTask); }); + } + else if (!retrieveTask->isStopped()) + { + logger.error(d->loggerRetrieveReport(retrieveTask, "failed")); + emit this->taskFailed("Retrieve"); + } + + this->deleteTask(taskUID); + } + + ctkDICOMStorageListenerTask* listenerTask = qobject_cast(this->sender()); + if (listenerTask) + { + logger.debug(d->loggerListenerReport(listenerTask, "canceled")); + emit this->taskCanceled("Listener"); + + QString taskUID = listenerTask->taskUID(); + if (listenerTask->numberOfRetry() < d->MaximumNumberOfRetry && !listenerTask->isStopped()) + { + ctkDICOMStorageListenerTask* newListenerTask = new ctkDICOMStorageListenerTask(); + newListenerTask->setAETitle(listenerTask->AETitle()); + newListenerTask->setPort(listenerTask->port()); + newListenerTask->setConnectionTimeout(listenerTask->connectionTimeout()); + newListenerTask->setNumberOfRetry(listenerTask->numberOfRetry() + 1); + newListenerTask->setTaskUID(taskUID); + newListenerTask->setAutoDelete(false); + + QTimer::singleShot(d->RetryDelay, this, [=] () {onTaskRetry(newListenerTask); }); + } + else if (!listenerTask->isStopped()) + { + logger.error(d->loggerListenerReport(listenerTask, "failed")); + emit this->taskFailed("Listener"); + } + + this->deleteTask(taskUID); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::onTaskRetry(ctkAbstractTask *task, + QThread::Priority priority) +{ + Q_D(ctkDICOMTaskPool); + + logger.debug("ctkDICOMTaskPool: retry task." + "TaskUID: " + task->taskUID()); + + QObject::connect(task, SIGNAL(started()), this, SLOT(onTaskStarted()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(finished()), this, SLOT(onTaskFinished()), Qt::QueuedConnection); + QObject::connect(task, SIGNAL(canceled()), this, SLOT(onTaskCanceled()), Qt::QueuedConnection); + + ctkDICOMRetrieveTask* retrieveTask = qobject_cast(task); + if (retrieveTask && retrieveTask->retrieveLevel() == ctkDICOMRetrieveTask::DICOMLevel::Series) + { + QObject::connect(retrieveTask->retriever(), SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), + this, SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), Qt::QueuedConnection); + } + ctkDICOMStorageListenerTask* listenerTask = qobject_cast(task); + if (listenerTask) + { + QObject::connect(listenerTask->listener(), SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(onListenerProgressBarTaskDetail(ctkDICOMTaskResults*)), Qt::QueuedConnection); + } + + + QString newTaskUID = d->generateUniqueTaskUID(); + task->setTaskUID(newTaskUID); + d->Tasks.insert(newTaskUID, task); + + d->ThreadPool->start(task, priority); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::onListenerProgressBarTaskDetail(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMTaskPool); + if (!taskResults) + { + return; + } + + emit progressBarTaskDetail(taskResults); + + ctkDICOMStorageListenerTask* listenerTask = + qobject_cast(this->getTaskByUID(taskResults->taskUID())); + if (!listenerTask || listenerTask->isStopped()) + { + return; + } + + ctkDICOMStorageListener* listener = listenerTask->listener(); + if (!listener) + { + return; + } + + bool isThumbnail = d->isAThumbnailUIDs(taskResults->studyInstanceUID(), + taskResults->seriesInstanceUID(), + taskResults->sopInstanceUID()); + + if (isThumbnail && !listenerTask->isStopped()) + { + taskResults->setTypeOfTask(ctkDICOMTaskResults::StoreSOPInstance); + d->Indexer->insertTaskResults(taskResults); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskPool::onInsertListenerTaskResults(ctkDICOMStorageListenerTask* listenerTask) +{ + Q_D(ctkDICOMTaskPool); + if (!listenerTask || + listenerTask->taskResultsList().count() == 0 || + listenerTask->isStopped()) + { + return; + } + + d->Indexer->insertTaskResults(listenerTask->taskResultsList()); +} diff --git a/Libs/DICOM/Core/ctkDICOMTaskPool.h b/Libs/DICOM/Core/ctkDICOMTaskPool.h new file mode 100644 index 0000000000..5bddcce743 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMTaskPool.h @@ -0,0 +1,232 @@ +/*========================================================================= + + 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 __ctkDICOMTaskPool_h +#define __ctkDICOMTaskPool_h + +// Qt includes +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMDatabase.h" + +#include + +class ctkAbstractTask; +class ctkDICOMIndexer; +class ctkDICOMTaskPoolPrivate; +class ctkDICOMServer; +class ctkDICOMStorageListenerTask; +class ctkDICOMTaskResults; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMTaskPool : public ctkAbstractTaskPool +{ + 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 ctkAbstractTaskPool Superclass; + explicit ctkDICOMTaskPool(QObject* parent = 0); + virtual ~ctkDICOMTaskPool(); + + /// Query Patients applying filters on all servers. + /// The method spans a ctkDICOMQueryTask for each server. + Q_INVOKABLE void queryPatients(QThread::Priority priority = QThread::LowPriority); + + /// Query Studies applying filters on all servers. + /// The method spans a ctkDICOMQueryTask 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 ctkDICOMQueryTask 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 ctkDICOMQueryTask 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 ctkDICOMRetrieveTask for each server. + Q_INVOKABLE void retrieveStudy(const QString& patientID, + const QString& studyInstanceUID, + QThread::Priority priority = QThread::LowPriority); + + /// Retrieve Series. + /// The method spans a ctkDICOMRetrieveTask 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 ctkDICOMRetrieveTask for each server. + Q_INVOKABLE void retrieveSOPInstance(const QString& patientID, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString &SOPInstanceUID, + bool trackThumbnailUID = false, + QThread::Priority priority = QThread::LowPriority); + + + /// Start a storage listener + /// The method spans a ctkDICOMStorageListenerTask. + Q_INVOKABLE void startListener(const int port, + const QString &AETitle, + QThread::Priority priority = QThread::LowPriority); + + /// 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 char* connectionName); + Q_INVOKABLE void addServer(ctkDICOMServer* server); + Q_INVOKABLE void removeServer(const char* connectionName); + Q_INVOKABLE void removeNthServer(int id); + Q_INVOKABLE void removeAllServers(); + Q_INVOKABLE QString getServerNameFromIndex(int id); + Q_INVOKABLE int getServerIndexFromName(const char* connectionName); + + /// Tasks managment + Q_INVOKABLE void waitForFinish(); + Q_INVOKABLE int totalTasks(); + Q_INVOKABLE int numberOfPersistentTasks(); + Q_INVOKABLE void stopAllTasks(bool stopPersistentTasks = false); + Q_INVOKABLE void stopAllTasksNotStarted(); + Q_INVOKABLE void deleteTask(QString taskUID); + Q_INVOKABLE void deleteTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE void deleteSeriesTaskResults(ctkDICOMTaskResults *taskResults); + Q_INVOKABLE ctkAbstractTask* getTaskByUID(QString taskUID); + Q_INVOKABLE void deleteAllTasks(); + Q_INVOKABLE void deleteStoppedTasks(); + Q_INVOKABLE void stopTasks(const QString& studyInstanceUID, + const QString& seriesInstanceUID = "", + const QString& sopInstanceUID = ""); + Q_INVOKABLE void lowerPriorityToAllTasks(); + Q_INVOKABLE void raiseRetrieveFramesTasksPriorityForSeries(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + QThread::Priority priority = QThread::HighPriority); + + /// Maximum number of concurrent QThreads spawned by the threadPool in the task pool + /// NOTE: there is another QThread spawned by the ctkDICOMIndexer which is not counted in this variable. + /// default: 20 + int maximumThreadCount() const; + void setMaximumThreadCount(const int& maximumThreadCount); + /// Maximum number of retries that the task pool will try on each failed task + /// default: 3 + int maximumNumberOfRetry() const; + void setMaximumNumberOfRetry(const int& maximumNumberOfRetry); + /// Retry delay in millisec + /// default: 100 msec + int retryDelay() const; + void setRetryDelay(const int& retryDelay); + /// maximum number of responses allowed in one query + /// when query is at Patient level. Default is 25. + void setMaximumPatientsQuery(const int maximumPatientsQuery); + int maximumPatientsQuery(); + + /// Return the listener Task. + Q_INVOKABLE ctkDICOMStorageListenerTask* listenerTask() const; + Q_INVOKABLE bool isStorageListenerActive() const; + + /// Return the Indexer. + Q_INVOKABLE ctkDICOMIndexer* indexer() const; + /// Return Indexer as a shared pointer + /// (not Python-wrappable). + QSharedPointer indexerShared() const; + + /// Return the threadPool. + Q_INVOKABLE QThreadPool* threadPool() const; + /// Return threadPool as a shared pointer + /// (not Python-wrappable). + QSharedPointer threadPoolShared() const; + +Q_SIGNALS: + void progressTaskDetail(ctkDICOMTaskResults*); + void progressBarTaskDetail(ctkDICOMTaskResults*); + void taskStarted(QString taskType); + void taskFinished(QString taskType); + void taskCanceled(QString taskType); + void taskFailed(QString taskType); + +public Q_SLOTS: + void onTaskStarted(); + void onTaskFinished(); + void onTaskCanceled(); + void onTaskRetry(ctkAbstractTask* task, + QThread::Priority priority = QThread::LowPriority); + void onListenerProgressBarTaskDetail(ctkDICOMTaskResults*); + void onInsertListenerTaskResults(ctkDICOMStorageListenerTask* listenerTask); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMTaskPool); + Q_DISABLE_COPY(ctkDICOMTaskPool); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMTaskPool_p.h b/Libs/DICOM/Core/ctkDICOMTaskPool_p.h new file mode 100644 index 0000000000..2586aca224 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMTaskPool_p.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 __ctkDICOMQueryTaskPrivate_h +#define __ctkDICOMQueryTaskPrivate_h + +// ctkDICOMCore includes +#include "ctkAbstractTask.h" + +// ctkDICOMCore includes +#include "ctkDICOMIndexer.h" +#include "ctkDICOMTaskPool.h" + +class ctkDICOMQueryTask; +class ctkDICOMRetrieveTask; +class ctkDICOMStorageListenerTask; + +struct ThumbnailUID +{ + QString studyInstanceUID; + QString seriesInstanceUID; + QString SOPInstanceUID; +} ; + +//------------------------------------------------------------------------------ +class ctkDICOMTaskPoolPrivate : public QObject +{ + Q_OBJECT + + Q_DECLARE_PUBLIC( ctkDICOMTaskPool ); + +protected: + ctkDICOMTaskPool* const q_ptr; + +public: + ctkDICOMTaskPoolPrivate(ctkDICOMTaskPool& obj); + ~ctkDICOMTaskPoolPrivate(); + + QString generateUniqueTaskUID(); + QString loggerQueryReport(ctkDICOMQueryTask*, const QString&); + QString loggerRetrieveReport(ctkDICOMRetrieveTask*, const QString&); + QString loggerListenerReport(ctkDICOMStorageListenerTask*, const QString&); + ctkDICOMServer* getServerFromProxyServersByConnectionName(const QString&); + bool isAThumbnailUIDs(const QString &studyInstanceUID, + const QString &seriesInstanceUID, + const QString &SOPInstanceUID); + void init(); + + QSharedPointer DicomDatabase; + QSharedPointer ThreadPool; + QSharedPointer Indexer; + QList> Servers; + QMap Tasks; + QList ThumbnailUIDs; + QMap Filters; + + int RetryDelay; + int MaximumNumberOfRetry; + int MaximumPatientsQuery; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMTaskResults.cpp b/Libs/DICOM/Core/ctkDICOMTaskResults.cpp new file mode 100644 index 0000000000..3db4054baf --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMTaskResults.cpp @@ -0,0 +1,349 @@ +/*========================================================================= + + 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 "ctkDICOMItem.h" +#include "ctkDICOMTaskResults.h" +#include "ctkLogger.h" + +// DCMTK includes +#include + +static ctkLogger logger("org.commontk.dicom.DICOMTaskResults"); + + +//------------------------------------------------------------------------------ +class ctkDICOMTaskResultsPrivate: public QObject +{ + Q_DECLARE_PUBLIC(ctkDICOMTaskResults); + +protected: + ctkDICOMTaskResults* const q_ptr; + +public: + ctkDICOMTaskResultsPrivate(ctkDICOMTaskResults& obj); + ~ctkDICOMTaskResultsPrivate(); + + QString FilePath; + bool CopyFile; + bool OverwriteExistingDataset; + + ctkDICOMTaskResults::TaskType TypeOfTask; + QString TaskUID; + QString PatientID; + QString StudyInstanceUID; + QString SeriesInstanceUID; + QString SOPInstanceUID; + QString ConnectionName; + QMap Datasets; +}; + +//------------------------------------------------------------------------------ +// ctkDICOMTaskResultsPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMTaskResultsPrivate::ctkDICOMTaskResultsPrivate(ctkDICOMTaskResults& obj) + : q_ptr(&obj) +{ + this->TypeOfTask = ctkDICOMTaskResults::TaskType::FileIndexing; + this->TaskUID = ""; + this->PatientID = ""; + this->StudyInstanceUID = ""; + this->SeriesInstanceUID = ""; + this->SOPInstanceUID = ""; + this->ConnectionName = ""; + this->CopyFile = false; + this->OverwriteExistingDataset = false; + this->FilePath = ""; +} + +//------------------------------------------------------------------------------ +ctkDICOMTaskResultsPrivate::~ctkDICOMTaskResultsPrivate() +{ + qDeleteAll(this->Datasets); + this->Datasets.clear(); +} + +//------------------------------------------------------------------------------ +// ctkDICOMTaskResults methods + +//------------------------------------------------------------------------------ +ctkDICOMTaskResults::ctkDICOMTaskResults(QObject* parent) + : QObject(parent), + d_ptr(new ctkDICOMTaskResultsPrivate(*this)) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMTaskResults::~ctkDICOMTaskResults() +{ +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskResults::setFilePath(const QString &filePath) +{ + Q_D(ctkDICOMTaskResults); + d->FilePath = filePath; + + if (d->FilePath.contains("server://") || d->FilePath == "") + { + return; + } + + ctkDICOMItem *dataset = 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 ctkDICOMTaskResults::filePath() const +{ + Q_D(const ctkDICOMTaskResults); + return d->FilePath; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskResults::setCopyFile(const bool ©File) +{ + Q_D(ctkDICOMTaskResults); + d->CopyFile = copyFile; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMTaskResults::copyFile() const +{ + Q_D(const ctkDICOMTaskResults); + return d->CopyFile; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskResults::setOverwriteExistingDataset(const bool &overwriteExistingDataset) +{ + Q_D(ctkDICOMTaskResults); + d->OverwriteExistingDataset = overwriteExistingDataset; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMTaskResults::overwriteExistingDataset() const +{ + Q_D(const ctkDICOMTaskResults); + return d->OverwriteExistingDataset; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskResults::setTypeOfTask(const ctkDICOMTaskResults::TaskType &typeOfTask) +{ + Q_D(ctkDICOMTaskResults); + d->TypeOfTask = typeOfTask; +} + +//------------------------------------------------------------------------------ +ctkDICOMTaskResults::TaskType ctkDICOMTaskResults::typeOfTask() const +{ + Q_D(const ctkDICOMTaskResults); + return d->TypeOfTask; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskResults::setTaskUID(const QString &taskUID) +{ + Q_D(ctkDICOMTaskResults); + d->TaskUID = taskUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskResults::taskUID() const +{ + Q_D(const ctkDICOMTaskResults); + return d->TaskUID; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskResults::setPatientID(const QString& patientID) +{ + Q_D(ctkDICOMTaskResults); + d->PatientID = patientID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskResults::patientID() const +{ + Q_D(const ctkDICOMTaskResults); + return d->PatientID; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskResults::setStudyInstanceUID(const QString& studyInstanceUID) +{ + Q_D(ctkDICOMTaskResults); + d->StudyInstanceUID = studyInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskResults::studyInstanceUID() const +{ + Q_D(const ctkDICOMTaskResults); + return d->StudyInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskResults::setSeriesInstanceUID(const QString& seriesInstanceUID) +{ + Q_D(ctkDICOMTaskResults); + d->SeriesInstanceUID = seriesInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskResults::seriesInstanceUID() const +{ + Q_D(const ctkDICOMTaskResults); + return d->SeriesInstanceUID; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTaskResults::setSOPInstanceUID(const QString& sopInstanceUID) +{ + Q_D(ctkDICOMTaskResults); + d->SOPInstanceUID = sopInstanceUID; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskResults::sopInstanceUID() const +{ + Q_D(const ctkDICOMTaskResults); + return d->SOPInstanceUID; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskResults::setConnectionName(const QString &connectionName) +{ + Q_D(ctkDICOMTaskResults); + d->ConnectionName = connectionName; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMTaskResults::connectionName() const +{ + Q_D(const ctkDICOMTaskResults); + return d->ConnectionName; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskResults::setDataset(DcmItem *dcmItem, bool takeOwnership) +{ + Q_D(ctkDICOMTaskResults); + if (!dcmItem) + { + return; + } + + ctkDICOMItem *dataset = new ctkDICOMItem; + dataset->InitializeFromItem(dcmItem, takeOwnership); + + OFString SOPInstanceUID; + dcmItem->findAndGetOFString(DCM_SOPInstanceUID, SOPInstanceUID); + d->Datasets.insert(QString(SOPInstanceUID.c_str()), dataset); +} + +//------------------------------------------------------------------------------ +ctkDICOMItem *ctkDICOMTaskResults::dataset() const +{ + Q_D(const ctkDICOMTaskResults); + if (d->Datasets.count() == 0) + { + return nullptr; + } + return d->Datasets.first(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskResults::setDatasets(QMap dcmItems, bool takeOwnership) +{ + Q_D(ctkDICOMTaskResults); + for(QString key : dcmItems.keys()) + { + DcmItem *dcmItem = dcmItems.value(key); + if (!dcmItem) + { + continue; + } + + ctkDICOMItem *dataset = new ctkDICOMItem; + dataset->InitializeFromItem(dcmItem, takeOwnership); + + d->Datasets.insert(key, dataset); + } +} + +//------------------------------------------------------------------------------ +QMap ctkDICOMTaskResults::datasets() const +{ + Q_D(const ctkDICOMTaskResults); + return d->Datasets; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTaskResults::deepCopy(ctkDICOMTaskResults *node) +{ + if (!node) + { + return; + } + + this->setFilePath(node->filePath()); + this->setCopyFile(node->copyFile()); + this->setOverwriteExistingDataset(node->overwriteExistingDataset()); + this->setTypeOfTask(node->typeOfTask()); + this->setTaskUID(node->filePath()); + this->setPatientID(node->patientID()); + this->setStudyInstanceUID(node->studyInstanceUID()); + this->setSeriesInstanceUID(node->seriesInstanceUID()); + this->setSOPInstanceUID(node->sopInstanceUID()); + this->setConnectionName(node->connectionName()); + + QMap nodeDatasets = node->datasets(); + QMap dcmItems; + for(QString key : nodeDatasets.keys()) + { + ctkDICOMItem* nodeDataset = nodeDatasets.value(key); + if (!nodeDataset) + { + continue; + } + + DcmItem nodedcmItem = nodeDataset->GetDcmItem(); + DcmItem* dcmItem = new DcmItem(); + dcmItem->copyFrom(nodedcmItem); + OFString SOPInstanceUID; + dcmItem->findAndGetOFString(DCM_SOPInstanceUID, SOPInstanceUID); + + dcmItems.insert(SOPInstanceUID.c_str(), dcmItem); + } + + this->setDatasets(dcmItems); +} diff --git a/Libs/DICOM/Core/ctkDICOMTaskResults.h b/Libs/DICOM/Core/ctkDICOMTaskResults.h new file mode 100644 index 0000000000..b236605ce9 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMTaskResults.h @@ -0,0 +1,129 @@ +/*========================================================================= + + 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 __ctkDICOMTaskResults_h +#define __ctkDICOMTaskResults_h + + +// Qt includes +#include +#include + +#include "ctkDICOMCoreExport.h" +#include "ctkDICOMItem.h" + +class ctkDICOMTaskResultsPrivate; +class DcmDataset; + +/// \ingroup DICOM_Core +class CTK_DICOM_CORE_EXPORT ctkDICOMTaskResults : public QObject +{ + Q_OBJECT + Q_ENUMS(TaskType) + 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(TaskType typeOfTask READ typeOfTask WRITE setTypeOfTask); + Q_PROPERTY(QString taskUID READ taskUID WRITE setTaskUID); + 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 ctkDICOMTaskResults(QObject* parent = 0); + virtual ~ctkDICOMTaskResults(); + + /// File Path + void setFilePath(const QString& filePath); + QString filePath() const; + + /// Copy File + void setCopyFile(const bool& copyFile); + bool copyFile() const; + + /// Overwrite existing dataset + void setOverwriteExistingDataset(const bool& overwriteExistingDataset); + bool overwriteExistingDataset() const; + + /// Task type + enum TaskType { + FileIndexing = 0, + QueryPatients, + QueryStudies, + QuerySeries, + QueryInstances, + RetrieveStudy, + RetrieveSeries, + RetrieveSOPInstance, + StoreStudy, + StoreSeries, + StoreSOPInstance + }; + void setTypeOfTask(const TaskType& typeOfTask); + TaskType typeOfTask() const; + + /// Task UID + void setTaskUID(const QString& taskUID); + QString taskUID() 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; + Q_INVOKABLE void setDatasets(QMap dcmItems, bool takeOwnership = true); + Q_INVOKABLE QMap datasets() const; + + /// Copy object + Q_INVOKABLE void deepCopy(ctkDICOMTaskResults* node); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMTaskResults); + Q_DISABLE_COPY(ctkDICOMTaskResults); +}; + + +#endif diff --git a/Libs/DICOM/Widgets/CMakeLists.txt b/Libs/DICOM/Widgets/CMakeLists.txt index 7c55927199..283b312d27 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 Qt5::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..acc2b3574c --- /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..4e4b745c25 --- /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..846383523a --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/add.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..560d174b9b --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..c386420774 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/dns.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..4e5d743153 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/load.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..e172f878a6 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..c611282e40 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/query.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..9cc945f49f --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/save.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..8c2108c746 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/text_document.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..5b6554fd6a --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..1dd5a16998 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui @@ -0,0 +1,365 @@ + + + ctkDICOMPatientItemWidget + + + + 0 + 0 + 1041 + 400 + + + + Patient Item + + + + + + + 0 + 0 + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 12 + + + + + + + Patient Information + + + false + + + false + + + 14 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + + + 100 + 0 + + + + + 11 + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + 0 + + + ID + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 10 + + + + + + + + 100 + 0 + + + + + 11 + + + + Sex + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 10 + + + + + + + + 11 + + + + + + + Qt::AlignCenter + + + 10 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 100 + 0 + + + + + 11 + + + + Birth Date + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 10 + + + + + + + + 11 + + + + + + + Qt::AlignCenter + + + 10 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 11 + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + 0 + + + + + + Qt::AlignCenter + + + 10 + + + 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 + 181 + + + + background-color: rgb(255, 255, 255); + + + + + + + + + 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..b0c5fd2f72 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMSeriesItemWidget.ui @@ -0,0 +1,101 @@ + + + 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..82d75eaa8f --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMServerNodeWidget2.ui @@ -0,0 +1,407 @@ + + + ctkDICOMServerNodeWidget2 + + + + 0 + 0 + 1679 + 271 + + + + Server List + + + + 1 + + + 1 + + + 1 + + + 1 + + + 5 + + + + + Servers + + + Qt::AlignCenter + + + + + + + + 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 + + + + + + + + + 1 + 0 + + + + Storage + + + false + + + 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 + + + + + + + + + + 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..b8db3456fb --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui @@ -0,0 +1,225 @@ + + + ctkDICOMStudyItemWidget + + + + 0 + 0 + 378 + 318 + + + + Study Item + + + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 20 + 20 + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 12 + + + + + + + Study ID 1234 --- Date + + + 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:12pt; 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;"><br /></p></body></html> + + + + + + + + 0 + 10 + + + + + + + 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..f405420237 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui @@ -0,0 +1,965 @@ + + + ctkDICOMVisualBrowserWidget + + + Qt::WindowModal + + + + 0 + 0 + 1201 + 678 + + + + Select data to load + + + + :/Icons/patient.svg:/Icons/patient.svg + + + + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + 11 + + + + + + + Patients Search + + + false + + + + 0 + + + 2 + + + 0 + + + 2 + + + 2 + + + + + 3 + + + + + 3 + + + + + 3 + + + + + + 0 + 0 + + + + + 11 + + + + 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 + + + + + 11 + + + + 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 + + + + + 11 + + + + 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 + + + + + 11 + + + + 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 + + + + + 11 + + + + Modality + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + Filter by modality + + + false + + + + + + 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 + + + + + 11 + + + + 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 + + + + + + + + + 11 + + + + + + + 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 + + + 1 + + + + 32 + 32 + + + + 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 + + + + + + + + + + + 11 + + + + + + + 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/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..53c6744824 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.contains("server://")) { 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..c5b0478db2 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp @@ -0,0 +1,810 @@ +/*========================================================================= + + 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 "ctkDICOMTaskPool.h" +#include "ctkDICOMTaskResults.h" + +// ctkDICOMWidgets includes +#include "ctkDICOMSeriesItemWidget.h" +#include "ctkDICOMStudyItemWidget.h" +#include "ctkDICOMPatientItemWidget.h" +#include "ui_ctkDICOMPatientItemWidget.h" + +static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMPatientItemWidget"); + +//---------------------------------------------------------------------------- +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 TaskPool; + + int NumberOfStudiesPerPatient; + int NumberOfSeriesPerRow; + int MinimumThumbnailSize; + + QString PatientItem; + QString PatientID; + + QString FilteringStudyDescription; + ctkDICOMPatientItemWidget::DateType FilteringDate; + + QString FilteringSeriesDescription; + QStringList FilteringModalities; + + QList StudyItemWidgetsList; + + QWidget* VisualDICOMBrowser; +}; + +//---------------------------------------------------------------------------- +// ctkDICOMPatientItemWidgetPrivate methods + +//---------------------------------------------------------------------------- +ctkDICOMPatientItemWidgetPrivate::ctkDICOMPatientItemWidgetPrivate(ctkDICOMPatientItemWidget& obj) + : q_ptr(&obj) +{ + this->NumberOfStudiesPerPatient = 2; + this->NumberOfSeriesPerRow = 6; + this->MinimumThumbnailSize = 300; + + this->DicomDatabase = nullptr; + this->TaskPool = nullptr; +} + +//---------------------------------------------------------------------------- +ctkDICOMPatientItemWidgetPrivate::~ctkDICOMPatientItemWidgetPrivate() +{ + QLayout *StudiesListWidgetLayout = this->StudiesListWidget->layout(); + this->clearLayout(StudiesListWidgetLayout); +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidgetPrivate::init(QWidget* parentWidget) +{ + Q_Q(ctkDICOMPatientItemWidget); + this->setupUi(q); + + this->VisualDICOMBrowser = parentWidget; + + 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, 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->PatientIDValueLabel->setText(""); + this->PatientSexValueLabel->setText(""); + this->PatientBirthDateValueLabel->setText(""); + return; + } + else + { + 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); + + // 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, 10, QSizePolicy::Fixed, QSizePolicy::Fixed); + 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::setNumberOfSeriesPerRow(int numberOfSeriesPerRow) +{ + Q_D(ctkDICOMPatientItemWidget); + d->NumberOfSeriesPerRow = numberOfSeriesPerRow; +} + +//------------------------------------------------------------------------------ +int ctkDICOMPatientItemWidget::numberOfSeriesPerRow() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->NumberOfSeriesPerRow; +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::setMinimumThumbnailSize(int minimumThumbnailSize) +{ + Q_D(ctkDICOMPatientItemWidget); + d->MinimumThumbnailSize = minimumThumbnailSize; +} + +//---------------------------------------------------------------------------- +int ctkDICOMPatientItemWidget::minimumThumbnailSize() const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->MinimumThumbnailSize; +} + +//---------------------------------------------------------------------------- +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 +} + +//---------------------------------------------------------------------------- +ctkDICOMTaskPool* ctkDICOMPatientItemWidget::taskPool()const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->TaskPool.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMPatientItemWidget::taskPoolShared()const +{ + Q_D(const ctkDICOMPatientItemWidget); + return d->TaskPool; +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::setTaskPool(ctkDICOMTaskPool& taskPool) +{ + Q_D(ctkDICOMPatientItemWidget); + if (d->TaskPool) + { + QObject::disconnect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } + + d->TaskPool = QSharedPointer(&taskPool, skipDelete); + + if (d->TaskPool) + { + QObject::connect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidget::setTaskPool(QSharedPointer taskPool) +{ + Q_D(ctkDICOMPatientItemWidget); + if (d->TaskPool) + { + QObject::disconnect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } + + d->TaskPool = taskPool; + + if (d->TaskPool) + { + QObject::connect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } +} + +//---------------------------------------------------------------------------- +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); + 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->setNumberOfSeriesPerRow(d->NumberOfSeriesPerRow); + if (this->parentWidget()) + { + studyItemWidget->setThumbnailSize(std::max(int(this->parentWidget()->width() / d->NumberOfSeriesPerRow), d->MinimumThumbnailSize) * 0.94); + } + studyItemWidget->setFilteringSeriesDescription(d->FilteringSeriesDescription); + studyItemWidget->setFilteringModalities(d->FilteringModalities); + studyItemWidget->setDicomDatabase(d->DicomDatabase); + studyItemWidget->setTaskPool(d->TaskPool); + // 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, SLOT(onLoad())); + this->connect(studyItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + d->VisualDICOMBrowser, SLOT(showStudyContextMenu(const QPoint&))); + this->connect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemClicked(QTableWidgetItem *)), + this, SLOT(onSeriesItemClicked())); + this->connect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemSelectionChanged()), + this, SLOT(raiseRetrieveFramesTasksPriority())); + + 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, SLOT(showStudyContextMenu(const QPoint&))); + this->disconnect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemClicked(QTableWidgetItem *)), + this, SLOT(onSeriesItemClicked())); + this->disconnect(studyItemWidget->seriesListTableWidget(), SIGNAL(itemSelectionChanged()), + this, SLOT(raiseRetrieveFramesTasksPriority())); + 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(); + QStringList studiesList = d->DicomDatabase->studiesForPatient(d->PatientItem); + if (studiesList.count() && d->TaskPool && d->TaskPool->getNumberOfQueryRetrieveServers() > 0) + { + d->TaskPool->queryStudies(d->PatientID, QThread::NormalPriority); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::updateGUIFromTaskPool(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMPatientItemWidget); + + if (!taskResults) + { + d->createStudies(); + } + + if (!taskResults || + taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::QueryStudies || + taskResults->patientID() != d->PatientID) + { + return; + } + + d->createStudies(); + d->TaskPool->deleteTask(taskResults->taskUID()); +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::raiseRetrieveFramesTasksPriority() +{ + Q_D(ctkDICOMPatientItemWidget); + + if (!d->TaskPool || d->TaskPool->getNumberOfQueryRetrieveServers() == 0) + { + logger.error("raiseRetrieveFramesTasksPriority failed, no task pool has been set. \n"); + return; + } + + QList selectedSeriesWidgets; + foreach (ctkDICOMStudyItemWidget *studyItemWidget, d->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)); + + selectedSeriesWidgets.append(seriesItemWidget); + } + } + + if (selectedSeriesWidgets.count() == 0) + { + return; + } + + d->TaskPool->lowerPriorityToAllTasks(); + foreach (ctkDICOMSeriesItemWidget* seriesWidget, selectedSeriesWidgets) + { + if (!seriesWidget || !seriesWidget->isCloud()) + { + continue; + } + + d->TaskPool->raiseRetrieveFramesTasksPriorityForSeries( + seriesWidget->studyInstanceUID(), seriesWidget->seriesInstanceUID() + ); + } +} + +//------------------------------------------------------------------------------ +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..6457c44212 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h @@ -0,0 +1,158 @@ +/*========================================================================= + + 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 +class ctkDICOMPatientItemWidgetPrivate; + +class ctkDICOMDatabase; +class ctkDICOMTaskPool; +class ctkDICOMStudyItemWidget; +class ctkDICOMTaskResults; + +/// \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(int numberOfSeriesPerRow READ numberOfSeriesPerRow WRITE setNumberOfSeriesPerRow); + Q_PROPERTY(int minimumThumbnailSize READ minimumThumbnailSize WRITE setMinimumThumbnailSize); + +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; + + /// Number of series displayed per row + /// 6 by default + void setNumberOfSeriesPerRow(int numberOfSeriesPerRow); + int numberOfSeriesPerRow() const; + + /// Minimum thumbnail size in pixel + /// 300 by default + void setMinimumThumbnailSize(int minimumThumbnailSize); + int minimumThumbnailSize() const; + + /// Return the task pool. + Q_INVOKABLE ctkDICOMTaskPool* taskPool() const; + /// Return the task pool as a shared pointer + /// (not Python-wrappable). + QSharedPointer taskPoolShared() const; + /// Set the task pool. + Q_INVOKABLE void setTaskPool(ctkDICOMTaskPool& taskPool); + /// Set the task pool as a shared pointer + /// (not Python-wrappable). + void setTaskPool(QSharedPointer taskPool); + + /// 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 updateGUIFromTaskPool(ctkDICOMTaskResults*); + void onSeriesItemClicked(); + void raiseRetrieveFramesTasksPriority(); + +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 b57205c643..e91f6ae5a2 100644 --- a/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp @@ -71,7 +71,7 @@ class ctkDICOMQueryRetrieveWidgetPrivate: public Ui_ctkDICOMQueryRetrieveWidget QProgressDialog* ProgressDialog; QString CurrentServer; - bool UseProgressDialog; + bool UseProgressDialog; }; //---------------------------------------------------------------------------- @@ -228,11 +228,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 +351,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 +379,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 +429,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/ctkDICOMSeriesItemWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp new file mode 100644 index 0000000000..67b70c6e56 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp @@ -0,0 +1,786 @@ +/*========================================================================= + + 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 + +// CTK includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMDatabase.h" +#include "ctkDICOMTaskPool.h" +#include "ctkDICOMTaskResults.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(ctkDICOMTaskResults *taskResults = nullptr); + void drawModalityThumbnail(); + void drawThumbnail(const QString& file, int numberOfFrames); + void drawTextWithShadow(QPainter *painter, + const QRect &r, + int flags, + const QString &text); + void updateThumbnailProgressBar(); + + QSharedPointer DicomDatabase; + QSharedPointer TaskPool; + + QString PatientID; + QString SeriesItem; + QString StudyInstanceUID; + QString SeriesInstanceUID; + QString CentralFrameSOPInstanceUID; + QString SeriesNumber; + QString Modality; + + bool IsCloud; + bool IsLoaded; + bool IsVisible; + int ThumbnailSize; + int NumberOfDownloads; +}; + +//---------------------------------------------------------------------------- +// ctkDICOMSeriesItemWidgetPrivate methods + +//---------------------------------------------------------------------------- +ctkDICOMSeriesItemWidgetPrivate::ctkDICOMSeriesItemWidgetPrivate(ctkDICOMSeriesItemWidget& obj) + : q_ptr(&obj) +{ + this->IsCloud = false; + this->IsLoaded = false; + this->IsVisible = false; + this->ThumbnailSize = 300; + this->NumberOfDownloads = 0; + + this->DicomDatabase = nullptr; + this->TaskPool = nullptr; +} + +//---------------------------------------------------------------------------- +ctkDICOMSeriesItemWidgetPrivate::~ctkDICOMSeriesItemWidgetPrivate() +{ +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::init() +{ + Q_Q(ctkDICOMSeriesItemWidget); + this->setupUi(q); + + this->SeriesThumbnail->setTransformationMode(Qt::TransformationMode::SmoothTransformation); +} + +//---------------------------------------------------------------------------- +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(ctkDICOMTaskResults *taskResults) +{ + if (!this->DicomDatabase) + { + logger.error("importFiles failed, no DICOM Database has been set. \n"); + return; + } + + ctkDICOMTaskResults::TaskType typeOfTask = ctkDICOMTaskResults::TaskType::FileIndexing; + QString taskSopInstanceUID; + if (taskResults) + { + taskSopInstanceUID = taskResults->sopInstanceUID(); + typeOfTask = taskResults->typeOfTask(); + } + + QStringList instancesList = this->DicomDatabase->instancesForSeries(this->SeriesInstanceUID); + int numberOfFrames = instancesList.count(); + if (numberOfFrames == 0) + { + this->drawModalityThumbnail(); + return; + } + + QStringList filesList = this->DicomDatabase->filesForSeries(this->SeriesInstanceUID); + int numberOfInstancesOnServer = filesList.filter("server://").count(); + if (!this->IsCloud && numberOfFrames > 0 && numberOfInstancesOnServer > 0) + { + this->IsCloud = true; + this->SeriesThumbnail->operationProgressBar()->show(); + } + else if (this->IsCloud && numberOfFrames > 0 && numberOfInstancesOnServer == 0) + { + 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.contains("server://") && numberOfInstancesOnServer < numberOfFrames) + { + foreach(QString newFile, filesList) + { + if (newFile.contains("server://")) + { + continue; + } + + file = newFile; + this->CentralFrameSOPInstanceUID = this->DicomDatabase->instanceForFile(file); + break; + } + } + } + else + { + file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); + } + + if (this->TaskPool && this->TaskPool->getNumberOfQueryRetrieveServers() > 0) + { + // Get file for thumbnail + if (this->IsCloud && (typeOfTask == ctkDICOMTaskResults::TaskType::FileIndexing || + typeOfTask == ctkDICOMTaskResults::TaskType::QueryInstances)) + { + this->TaskPool->retrieveSOPInstance(this->PatientID, + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->CentralFrameSOPInstanceUID, + true, + QThread::NormalPriority); + return; + } + if (this->IsCloud && typeOfTask == ctkDICOMTaskResults::TaskType::RetrieveSOPInstance) + { + // Get all the others frames + if (numberOfFrames > 1) + { + foreach(QString file, filesList) + { + if (!file.contains("server://")) + { + continue; + } + + this->TaskPool->retrieveSeries(this->PatientID, + this->StudyInstanceUID, + this->SeriesInstanceUID); + break; + } + } + } + } + + file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); + if ((taskSopInstanceUID.isEmpty() || taskSopInstanceUID == this->CentralFrameSOPInstanceUID) && + !file.contains("server://")) + { + this->drawThumbnail(file, numberOfFrames); + } + + if (numberOfFrames == 1) + { + this->updateThumbnailProgressBar(); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::drawModalityThumbnail() +{ + if (!this->DicomDatabase) + { + logger.error("drawThumbnail failed, no DICOM Database has been set. \n"); + return; + } + + int margin = 10; + int fontSize = 40; + + QPixmap resultPixmap(this->ThumbnailSize, this->ThumbnailSize); + resultPixmap.fill(Qt::transparent); + ctkDICOMThumbnailGenerator thumbnailGenerator; + thumbnailGenerator.setWidth(this->ThumbnailSize); + thumbnailGenerator.setHeight(this->ThumbnailSize); + + QImage thumbnailImage; + QPainter painter; + + thumbnailGenerator.generateBlankThumbnail(thumbnailImage, Qt::white); + resultPixmap = QPixmap::fromImage(thumbnailImage); + if (painter.begin(&resultPixmap)) + { + painter.setRenderHint(QPainter::Antialiasing); + QRect rect = resultPixmap.rect(); + painter.setFont(QFont("Arial", fontSize, QFont::Bold)); + this->drawTextWithShadow(&painter, rect.adjusted(margin, margin, margin, margin), 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 = 10; + int fontSize = 12; + if (!this->SeriesThumbnail->text().isEmpty()) + { + margin = 5; + fontSize = 14; + } + + QPixmap resultPixmap(this->ThumbnailSize, this->ThumbnailSize); + resultPixmap.fill(Qt::transparent); + ctkDICOMThumbnailGenerator thumbnailGenerator; + thumbnailGenerator.setWidth(this->ThumbnailSize); + thumbnailGenerator.setHeight(this->ThumbnailSize); + QImage thumbnailImage; + QPainter painter; + if (!thumbnailGenerator.generateThumbnail(file, thumbnailImage)) + { + thumbnailGenerator.generateBlankThumbnail(thumbnailImage, Qt::white); + resultPixmap = QPixmap::fromImage(thumbnailImage); + if (painter.begin(&resultPixmap)) + { + painter.setRenderHint(QPainter::Antialiasing); + QSvgRenderer renderer(QString(":Icons/text_document.svg")); + renderer.render(&painter); + painter.end(); + } + } + else + { + if (painter.begin(&resultPixmap)) + { + painter.setRenderHint(QPainter::Antialiasing); + QRect rect = resultPixmap.rect(); + painter.setFont(QFont("Arial", fontSize, QFont::Bold)); + int x = int((rect.width() / 2) - (thumbnailImage.rect().width() / 2)); + int y = int((rect.height() / 2) - (thumbnailImage.rect().height() / 2)); + painter.drawPixmap(x, y, QPixmap::fromImage(thumbnailImage)); + QString topLeft = ctkDICOMSeriesItemWidget::tr("Series: %1\n%2").arg(this->SeriesNumber).arg(this->Modality); + this->drawTextWithShadow(&painter, rect.adjusted(margin, margin, margin, margin), Qt::AlignTop | Qt::AlignLeft, topLeft); + QString bottomLeft = ctkDICOMSeriesItemWidget::tr("N.frames: %1").arg(numberOfFrames); + this->drawTextWithShadow(&painter, rect.adjusted(margin, -margin, margin, -margin), Qt::AlignBottom | Qt::AlignLeft, bottomLeft); + QString rows = this->DicomDatabase->instanceValue(this->CentralFrameSOPInstanceUID, "0028,0010"); + QString columns = this->DicomDatabase->instanceValue(this->CentralFrameSOPInstanceUID, "0028,0011"); + QString bottomRight = rows + "x" + columns; + this->drawTextWithShadow(&painter, rect.adjusted(-margin, -margin, -margin, -margin), Qt::AlignBottom | Qt::AlignRight, bottomRight); + 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() - 48 - margin, topRight.y() + margin, 48, 48); + renderer.render(&painter, bounds); + painter.end(); + } + } + + this->SeriesThumbnail->setPixmap(resultPixmap); +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::drawTextWithShadow(QPainter *painter, + const QRect &r, + int flags, const + QString &text) +{ + painter->setPen(Qt::darkGray); + painter->drawText(r.adjusted(1, 1, 1, 1), flags, text); + painter->setPen(QColor(Qt::gray)); + painter->drawText(r.adjusted(2, 2, 2, 2), flags, text); + painter->setPen(QPen(QColor(41, 121, 255))); + painter->drawText(r, flags, text); +} + +//---------------------------------------------------------------------------- +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) + { + // change icons + QStringList instancesList = this->DicomDatabase->instancesForSeries(this->SeriesInstanceUID); + int numberOfFrames = instancesList.count(); + QString file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); + this->drawThumbnail(file, numberOfFrames); + } + else if (progress == 100) + { + this->createThumbnail(); + } +} + +//---------------------------------------------------------------------------- +// 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); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMSeriesItemWidget::seriesDescription() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->SeriesThumbnail->text(); +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::isCloud() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->IsCloud; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::IsLoaded() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->IsLoaded; +} + +//---------------------------------------------------------------------------- +bool ctkDICOMSeriesItemWidget::IsVisible() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->IsVisible; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setThumbnailSize(int thumbnailSize) +{ + Q_D(ctkDICOMSeriesItemWidget); + d->ThumbnailSize = thumbnailSize; +} + +//------------------------------------------------------------------------------ +int ctkDICOMSeriesItemWidget::thumbnailSize() const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->ThumbnailSize; +} + +//---------------------------------------------------------------------------- +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 +} + +//---------------------------------------------------------------------------- +ctkDICOMTaskPool* ctkDICOMSeriesItemWidget::taskPool()const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->TaskPool.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMSeriesItemWidget::taskPoolShared()const +{ + Q_D(const ctkDICOMSeriesItemWidget); + return d->TaskPool; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setTaskPool(ctkDICOMTaskPool& taskPool) +{ + Q_D(ctkDICOMSeriesItemWidget); + if (d->TaskPool) + { + QObject::disconnect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } + + d->TaskPool = QSharedPointer(&taskPool, skipDelete); + + if (d->TaskPool) + { + QObject::connect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::setTaskPool(QSharedPointer taskPool) +{ + Q_D(ctkDICOMSeriesItemWidget); + if (d->TaskPool) + { + QObject::disconnect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + QObject::disconnect(d->TaskPool.data(), SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateSeriesProgressBar(ctkDICOMTaskResults*))); + } + + d->TaskPool = taskPool; + + if (d->TaskPool) + { + QObject::connect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + QObject::connect(d->TaskPool.data(), SIGNAL(progressBarTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateSeriesProgressBar(ctkDICOMTaskResults*))); + } +} + +//---------------------------------------------------------------------------- +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; + } + + d->createThumbnail(); + QStringList instancesList = d->DicomDatabase->instancesForSeries(d->SeriesInstanceUID); + if (instancesList.count() == 0 && d->TaskPool && d->TaskPool->getNumberOfQueryRetrieveServers() > 0) + { + d->TaskPool->queryInstances(d->PatientID, + d->StudyInstanceUID, + d->SeriesInstanceUID, + QThread::NormalPriority); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::updateGUIFromTaskPool(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMSeriesItemWidget); + + if (!taskResults || + (taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::QueryInstances && + taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::RetrieveSOPInstance && + taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::RetrieveSeries && + taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::StoreSOPInstance && + taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::StoreSeries) || + taskResults->patientID() != d->PatientID || + taskResults->studyInstanceUID() != d->StudyInstanceUID || + taskResults->seriesInstanceUID() != d->SeriesInstanceUID) + { + return; + } + + if (taskResults->typeOfTask() == ctkDICOMTaskResults::TaskType::RetrieveSeries) + { + d->TaskPool->deleteTask(taskResults->taskUID()); + return; + } + + if (taskResults->typeOfTask() == ctkDICOMTaskResults::TaskType::StoreSeries && + taskResults->sopInstanceUID() != d->CentralFrameSOPInstanceUID) + { + d->TaskPool->deleteSeriesTaskResults(taskResults); + return; + } + + d->createThumbnail(taskResults); + if (taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::StoreSOPInstance) + { + d->TaskPool->deleteTask(taskResults->taskUID()); + } + else + { + d->TaskPool->deleteTaskResults(taskResults); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::updateSeriesProgressBar(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMSeriesItemWidget); + + if (!taskResults || + (taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::RetrieveSeries && + taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::StoreSOPInstance && + taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::StoreSeries) || + taskResults->studyInstanceUID() != d->StudyInstanceUID || + taskResults->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..195b75ed59 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h @@ -0,0 +1,133 @@ +/*========================================================================= + + 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 + +#include "ctkDICOMWidgetsExport.h" + +// Qt includes +#include + +class ctkDICOMSeriesItemWidgetPrivate; +class ctkDICOMDatabase; +class ctkDICOMTaskPool; +class ctkDICOMTaskResults; + +/// \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(QString isCloud READ isCloud); + Q_PROPERTY(int thumbnailSize READ thumbnailSize WRITE setThumbnailSize); + +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; + + /// Series lives in the server + bool isCloud() const; + + /// Series has been loaded by the parent widget + bool IsLoaded() const; + + /// Series is visible in the parent widget + bool IsVisible() const; + + /// Series Thumbnail size + /// 300 px by default + void setThumbnailSize(int thumbnailSize); + int thumbnailSize() const; + + /// Return the task pool. + Q_INVOKABLE ctkDICOMTaskPool* taskPool() const; + /// Return the task pool as a shared pointer + /// (not Python-wrappable). + QSharedPointer taskPoolShared() const; + /// Set the task pool. + Q_INVOKABLE void setTaskPool(ctkDICOMTaskPool& taskPool); + /// Set the task pool as a shared pointer + /// (not Python-wrappable). + void setTaskPool(QSharedPointer taskPool); + + /// 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 updateGUIFromTaskPool(ctkDICOMTaskResults*); + void updateSeriesProgressBar(ctkDICOMTaskResults*); + +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..8778c77ebd --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp @@ -0,0 +1,1252 @@ +/*========================================================================= + + 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 + +// CTK includes +#include +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMEcho.h" +#include "ctkDICOMServer.h" +#include "ctkDICOMTaskPool.h" + +// 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, 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 disconnectTaskPool(); + void connectTaskPool(); + /// 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); + ctkDICOMServer* createServerFromServerNode(const QMap& node); + void updateProxyComboBoxes(const QString &connectionName, int rowCount) const; + + bool SettingsModified; + QSharedPointer TaskPool; + QPushButton *SaveButton; + QPushButton *RestoreButton; +}; + +//---------------------------------------------------------------------------- +ctkDICOMServerNodeWidget2Private::ctkDICOMServerNodeWidget2Private(ctkDICOMServerNodeWidget2& obj) + : q_ptr(&obj) +{ + this->SettingsModified = false; + this->TaskPool = 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(addServerNode())); + QObject::connect(this->TestButton, SIGNAL(clicked()), + q, SLOT(testCurrentServerNode())); + QObject::connect(this->RemoveButton, SIGNAL(clicked()), + q, SLOT(removeCurrentServerNode())); + this->SaveButton = this->buttonBox->button(QDialogButtonBox::StandardButton::Save); + this->SaveButton->setStyleSheet("text-align:left;"); + this->SaveButton->setText(QObject::tr("Apply changes")); + this->RestoreButton = this->buttonBox->button(QDialogButtonBox::StandardButton::Discard); + this->SaveButton->setStyleSheet("text-align:left;"); + this->RestoreButton->setText(QObject::tr("Discard changes")); + QObject::connect(this->RestoreButton, SIGNAL(clicked()), + q, SLOT(readSettings())); + QObject::connect(this->SaveButton, SIGNAL(clicked()), + q, SLOT(saveSettings())); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2Private::disconnectTaskPool() +{ + Q_Q(ctkDICOMServerNodeWidget2); + if (!this->TaskPool) + { + return; + } + + ctkDICOMServerNodeWidget2::disconnect(this->TaskPool.data(), SIGNAL(taskStarted(QString)), + q, SLOT(updateGUIState())); + ctkDICOMServerNodeWidget2::disconnect(this->TaskPool.data(), SIGNAL(taskFinished(QString)), + q, SLOT(updateGUIState())); + ctkDICOMServerNodeWidget2::disconnect(this->TaskPool.data(), SIGNAL(taskCanceled(QString)), + q, SLOT(updateGUIState())); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2Private::connectTaskPool() +{ + Q_Q(ctkDICOMServerNodeWidget2); + if (!this->TaskPool) + { + return; + } + + ctkDICOMServerNodeWidget2::connect(this->TaskPool.data(), SIGNAL(taskStarted(QString)), + q, SLOT(updateGUIState())); + ctkDICOMServerNodeWidget2::connect(this->TaskPool.data(), SIGNAL(taskFinished(QString)), + q, SLOT(updateGUIState())); + ctkDICOMServerNodeWidget2::connect(this->TaskPool.data(), SIGNAL(taskCanceled(QString)), + 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; + const 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; + const 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; + } + const 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; + const 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; + const 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; + } + + const 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; +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMServerNodeWidget2Private::createServerFromServerNode(const QMap &node) +{ + ctkDICOMServer* server = 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::addServerNode() +{ + Q_D(ctkDICOMServerNodeWidget2); + const int rowCount = d->NodeTable->rowCount(); + d->NodeTable->setRowCount(rowCount + 1); + + QTableWidgetItem *newItem = new QTableWidgetItem(QString::number(rowCount)); + 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; + QComboBox *protocolComboBox = new QComboBox(); + protocolComboBox->addItem("CGET"); + protocolComboBox->addItem("CMOVE"); + // To Do: protocolComboBox->addItem("WADO"); + connect(protocolComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(onSettingsModified())); + d->NodeTable->setItem(rowCount, ProtocolColumn, newItem); + d->NodeTable->setCellWidget(rowCount, ProtocolColumn, protocolComboBox); + + d->NodeTable->setCurrentCell(rowCount, NameColumn); + + this->onSettingsModified(); + + return rowCount; +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::removeCurrentServerNode() +{ + 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::testCurrentServerNode() +{ + 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->TaskPool && d->TaskPool->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->TaskPool) + { + return; + } + + QSettings settings; + const int rowCount = d->NodeTable->rowCount(); + + settings.remove("DICOM/ServerNodes"); + this->removeAllServers(); + this->stopAllTasks(); + + 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; + } + + ctkDICOMServer* server = d->createServerFromServerNode(node); + d->TaskPool->addServer(server); + } + + for (int ii = 0; ii < rowCount; ++ii) + { + QMap node = d->serverNodeParameters(ii); + QString serverName = node["Name"].toString(); + if (!proxyServers.contains(serverName)) + { + continue; + } + + ctkDICOMServer* 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", QString::number(this->storageListenerEnabled())); + settings.setValue("DICOM/StorageAETitle", this->storageAETitle()); + settings.setValue("DICOM/StoragePort", this->storagePort()); + settings.sync(); + + d->SettingsModified = false; + + if (d->StorageEnabledCheckBox->isChecked() && !d->TaskPool->isStorageListenerActive()) + { + d->TaskPool->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("11112"); + 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()); + + const 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(const 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 +} + +//---------------------------------------------------------------------------- +ctkDICOMTaskPool* ctkDICOMServerNodeWidget2::taskPool()const +{ + Q_D(const ctkDICOMServerNodeWidget2); + return d->TaskPool.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMServerNodeWidget2::taskPoolShared()const +{ + Q_D(const ctkDICOMServerNodeWidget2); + return d->TaskPool; +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::setTaskPool(ctkDICOMTaskPool& taskPool) +{ + Q_D(ctkDICOMServerNodeWidget2); + d->disconnectTaskPool(); + d->TaskPool = QSharedPointer(&taskPool, skipDelete); + d->connectTaskPool(); + this->saveSettings(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::setTaskPool(QSharedPointer taskPool) +{ + Q_D(ctkDICOMServerNodeWidget2); + d->disconnectTaskPool(); + d->TaskPool = taskPool; + d->connectTaskPool(); + this->saveSettings(); +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2::getNumberOfServers() +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->TaskPool) + { + logger.error("getNumberOfServers failed, no task pool has been set. \n"); + return -1; + } + + return d->TaskPool->getNumberOfServers(); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMServerNodeWidget2::getNthServer(int id) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->TaskPool) + { + logger.error("getNthServer failed, no task pool has been set. \n"); + return nullptr; + } + + return d->TaskPool->getNthServer(id); +} + +//---------------------------------------------------------------------------- +ctkDICOMServer* ctkDICOMServerNodeWidget2::getServer(const char *connectionName) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->TaskPool) + { + logger.error("getServer failed, no task pool has been set. \n"); + return nullptr; + } + + return d->TaskPool->getServer(connectionName); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::addServer(ctkDICOMServer* server) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->TaskPool) + { + logger.error("addServer failed, no task pool has been set. \n"); + return; + } + + d->addServerNode(server); + this->saveSettings(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::removeServer(const char *connectionName) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->TaskPool) + { + 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->TaskPool) + { + 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->TaskPool) + { + logger.error("removeAllServers failed, no task pool has been set. \n"); + return; + } + + d->TaskPool->removeAllServers(); +} + +//---------------------------------------------------------------------------- +QString ctkDICOMServerNodeWidget2::getServerNameFromIndex(int id) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->TaskPool) + { + logger.error("getServerNameFromIndex failed, no task pool has been set. \n"); + return ""; + } + + return d->TaskPool->getServerNameFromIndex(id); +} + +//---------------------------------------------------------------------------- +int ctkDICOMServerNodeWidget2::getServerIndexFromName(const char *connectionName) +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->TaskPool) + { + logger.error("getServerIndexFromName failed, no task pool has been set. \n"); + return -1; + } + + return d->TaskPool->getServerIndexFromName(connectionName); +} + +//---------------------------------------------------------------------------- +void ctkDICOMServerNodeWidget2::stopAllTasks() +{ + Q_D(ctkDICOMServerNodeWidget2); + if (!d->TaskPool) + { + return; + } + + QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); + d->TaskPool->stopAllTasks(true); + QApplication::restoreOverrideCursor(); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h new file mode 100644 index 0000000000..f2f94551bc --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.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 __ctkDICOMServerNodeWidget2_h +#define __ctkDICOMServerNodeWidget2_h + +// Qt includes +#include +#include +#include +#include + +#include "ctkDICOMWidgetsExport.h" + +class QTableWidgetItem; +class ctkAbstractTask; +class ctkDICOMServer; +class ctkDICOMServerNodeWidget2Private; +class ctkDICOMTaskPool; + +/// \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(const int storagePort); + int storagePort() const; + + /// Return the task pool. + Q_INVOKABLE ctkDICOMTaskPool* taskPool() const; + /// Return the task pool as a shared pointer + /// (not Python-wrappable). + QSharedPointer taskPoolShared() const; + /// Set the task pool. + Q_INVOKABLE void setTaskPool(ctkDICOMTaskPool& taskPool); + /// Set the task pool as a shared pointer + /// (not Python-wrappable). + void setTaskPool(QSharedPointer taskPool); + + /// Servers + Q_INVOKABLE int getNumberOfServers(); + Q_INVOKABLE ctkDICOMServer* getNthServer(int id); + Q_INVOKABLE ctkDICOMServer* getServer(const char* connectionName); + Q_INVOKABLE void addServer(ctkDICOMServer* server); + Q_INVOKABLE void removeServer(const char* connectionName); + Q_INVOKABLE void removeNthServer(int id); + Q_INVOKABLE void removeAllServers(); + Q_INVOKABLE QString getServerNameFromIndex(int id); + Q_INVOKABLE int getServerIndexFromName(const char* connectionName); + Q_INVOKABLE void stopAllTasks(); + +public Q_SLOTS: + /// Add an empty server node and make it current + /// Return the row index added into the table + int addServerNode(); + /// Remove the current row (different from the checked rows) + void removeCurrentServerNode(); + /// Test the current row (different from the checked rows) + void testCurrentServerNode(); + + 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..12c197e3cf --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp @@ -0,0 +1,685 @@ +/*========================================================================= + + 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 + +// CTK includes +#include + +// ctkDICOMCore includes +#include "ctkDICOMDatabase.h" +#include "ctkDICOMTaskPool.h" +#include "ctkDICOMTaskResults.h" + +// ctkDICOMWidgets includes +#include "ctkDICOMSeriesItemWidget.h" +#include "ctkDICOMStudyItemWidget.h" +#include "ui_ctkDICOMStudyItemWidget.h" + +#include + +static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMStudyItemWidget"); + +//---------------------------------------------------------------------------- +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(); + void addEmptySeriesItemWidget(const int& rowIndex, + const int& columnIndex); + bool isSeriesItemAlreadyAdded(const QString& seriesItem); + + QString FilteringSeriesDescription; + QStringList FilteringModalities; + + QSharedPointer DicomDatabase; + QSharedPointer TaskPool; + + int ThumbnailSize; + QString PatientID; + QString StudyInstanceUID; + QString StudyItem; + + QWidget* VisualDICOMBrowser; +}; + +//---------------------------------------------------------------------------- +// ctkDICOMStudyItemWidgetPrivate methods + +//---------------------------------------------------------------------------- +ctkDICOMStudyItemWidgetPrivate::ctkDICOMStudyItemWidgetPrivate(ctkDICOMStudyItemWidget& obj) + : q_ptr(&obj) +{ + this->ThumbnailSize = 300; + + this->DicomDatabase = nullptr; + this->TaskPool = 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, SLOT(showSeriesContextMenu(const QPoint&))); + } + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidgetPrivate::init(QWidget* parentWidget) +{ + Q_Q(ctkDICOMStudyItemWidget); + this->setupUi(q); + + this->VisualDICOMBrowser = parentWidget; + + 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 i = 0; i < this->SeriesListTableWidget->columnCount(); ++i) + { + this->SeriesListTableWidget->setColumnWidth(i, this->ThumbnailSize); + } +} + +//------------------------------------------------------------------------------ +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->ThumbnailSize) + { + iHeight = this->ThumbnailSize; + } + iHeight += 25; + this->SeriesListTableWidget->setMinimumHeight(iHeight); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidgetPrivate::addEmptySeriesItemWidget(const int& rowIndex, + const int& columnIndex) +{ + QTableWidgetItem *tableItem = new QTableWidgetItem; + tableItem->setFlags(Qt::NoItemFlags); + tableItem->setSizeHint(QSize(this->ThumbnailSize, this->ThumbnailSize)); + + 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(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setNumberOfSeriesPerRow(int numberOfSeriesPerRow) +{ + Q_D(ctkDICOMStudyItemWidget); + d->SeriesListTableWidget->setColumnCount(numberOfSeriesPerRow); + d->updateColumnsWidths(); +} + +//------------------------------------------------------------------------------ +int ctkDICOMStudyItemWidget::numberOfSeriesPerRow() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->SeriesListTableWidget->columnCount(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setThumbnailSize(int thumbnailSize) +{ + Q_D(ctkDICOMStudyItemWidget); + d->ThumbnailSize = thumbnailSize; + d->updateColumnsWidths(); +} + +//------------------------------------------------------------------------------ +int ctkDICOMStudyItemWidget::thumbnailSize() const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->ThumbnailSize; +} + +//------------------------------------------------------------------------------ +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(); +} + +//---------------------------------------------------------------------------- +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 +} + +//---------------------------------------------------------------------------- +ctkDICOMTaskPool* ctkDICOMStudyItemWidget::taskPool()const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->TaskPool.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMStudyItemWidget::taskPoolShared()const +{ + Q_D(const ctkDICOMStudyItemWidget); + return d->TaskPool; +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setTaskPool(ctkDICOMTaskPool& taskPool) +{ + Q_D(ctkDICOMStudyItemWidget); + if (d->TaskPool) + { + QObject::disconnect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } + + d->TaskPool = QSharedPointer(&taskPool, skipDelete); + + if (d->TaskPool) + { + QObject::connect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMStudyItemWidget::setTaskPool(QSharedPointer taskPool) +{ + Q_D(ctkDICOMStudyItemWidget); + if (d->TaskPool) + { + QObject::disconnect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } + + d->TaskPool = taskPool; + + if (d->TaskPool) + { + QObject::connect(d->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + this, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + } +} + +//---------------------------------------------------------------------------- +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; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::addSeriesItemWidget(const 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->setThumbnailSize(d->ThumbnailSize); + seriesItemWidget->setDicomDatabase(d->DicomDatabase); + seriesItemWidget->setTaskPool(d->TaskPool); + seriesItemWidget->generateInstances(); + seriesItemWidget->setContextMenuPolicy(Qt::CustomContextMenu); + + this->connect(seriesItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + d->VisualDICOMBrowser, SLOT(showSeriesContextMenu(const QPoint&))); + + QTableWidgetItem *tableItem = new QTableWidgetItem; + tableItem->setSizeHint(QSize(d->ThumbnailSize, d->ThumbnailSize)); + + 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->ThumbnailSize + 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, 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(); + QStringList seriesList = d->DicomDatabase->seriesForStudy(d->StudyInstanceUID); + if (seriesList.count() == 0 && d->TaskPool && d->TaskPool->getNumberOfQueryRetrieveServers() > 0) + { + d->TaskPool->querySeries(d->PatientID, + d->StudyInstanceUID, + QThread::NormalPriority); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::updateGUIFromTaskPool(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMStudyItemWidget); + + if (!taskResults) + { + d->createSeries(); + } + + if (!taskResults || + taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::QuerySeries || + taskResults->patientID() != d->PatientID || + taskResults->studyInstanceUID() != d->StudyInstanceUID) + { + return; + } + + d->createSeries(); + d->TaskPool->deleteTask(taskResults->taskUID()); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::onStudySelectionClicked(bool toggled) +{ + Q_D(ctkDICOMStudyItemWidget); + + this->setSelection(toggled); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h new file mode 100644 index 0000000000..8ce5a68e0b --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.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 __ctkDICOMStudyItemWidget_h +#define __ctkDICOMStudyItemWidget_h + +#include "ctkDICOMWidgetsExport.h" + +// Qt includes +#include + +class ctkCollapsibleGroupBox; +class ctkDICOMStudyItemWidgetPrivate; +class ctkDICOMDatabase; +class ctkDICOMTaskPool; +class ctkDICOMTaskResults; + +class QTableWidget; + +/// \ingroup DICOM_Widgets +class CTK_DICOM_WIDGETS_EXPORT ctkDICOMStudyItemWidget : public QWidget +{ + Q_OBJECT; + 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 WRITE setNumberOfSeriesPerRow); + Q_PROPERTY(int thumbnailSize READ thumbnailSize WRITE setThumbnailSize); + +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 + /// 6 by default + void setNumberOfSeriesPerRow(int numberOfSeriesPerRow); + int numberOfSeriesPerRow() const; + + /// Series Thumbnail size + /// 300 px by default + void setThumbnailSize(int thumbnailSize); + int thumbnailSize() 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 task pool. + Q_INVOKABLE ctkDICOMTaskPool* taskPool() const; + /// Return the task pool as a shared pointer + /// (not Python-wrappable). + QSharedPointer taskPoolShared() const; + /// Set the task pool. + Q_INVOKABLE void setTaskPool(ctkDICOMTaskPool& taskPool); + /// Set the task pool as a shared pointer + /// (not Python-wrappable). + void setTaskPool(QSharedPointer taskPool); + + /// 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(); + + /// Add/Remove Series item widget + Q_INVOKABLE void addSeriesItemWidget(const 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 updateGUIFromTaskPool(ctkDICOMTaskResults*); + 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 a99ee51705..711aa99a81 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, Qt::GlobalColor 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..2ea7be7024 100644 --- a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h +++ b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h @@ -56,7 +56,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, Qt::GlobalColor 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..90a5537af0 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp @@ -0,0 +1,2869 @@ +/*========================================================================= + + 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 + +// CTK includes +#include +#include +#include +#include +#include + +// ctkDICOMCore includes +#include "ctkDICOMDatabase.h" +#include "ctkDICOMIndexer.h" +#include "ctkDICOMTaskPool.h" +#include "ctkDICOMServer.h" +#include "ctkDICOMTaskResults.h" +#include "ctkUtils.h" + +// ctkDICOMWidgets includes +#include "ctkDICOMObjectListWidget.h" +#include "ctkDICOMVisualBrowserWidget.h" +#include "ctkDICOMStudyItemWidget.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 disconnectTaskPool(); + void connectTaskPool(); + void importDirectory(QString directory, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); + void importFiles(const QStringList& files, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); + void importOldSettings(); + void updateModalityCheckableComboBox(); + void createPatients(); + void updateFiltersWarnings(); + 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 QString& filterName, + const QString& filterValue); + QStringList filterStudyList(const QStringList& studyList, + const QString& filterName, + const QString& filterValue); + QStringList filterSeriesList(const QStringList& seriesList, + const QString& filterName, + const QString& filterValue); + QStringList filterSeriesList(const QStringList& seriesList, + const QString& filterName, + const QStringList& filterValues); + + // 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; + bool IsImportFolder; + 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 TaskPool; + + QString FilteringPatientID; + QString FilteringPatientName; + + QString FilteringStudyDescription; + ctkDICOMPatientItemWidget::DateType FilteringDate; + + QString FilteringSeriesDescription; + QStringList PreviousFilteringModalities; + QStringList FilteringModalities; + + int NumberOfStudiesPerPatient; + int NumberOfSeriesPerRow; + int MinimumThumbnailSize; + bool SendActionVisible; + bool IsGUIUpdating; + + ctkDICOMServerNodeWidget2* ServerNodeWidget; +}; + +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->TaskPool = QSharedPointer (new ctkDICOMTaskPool); + this->DicomDatabase = QSharedPointer (new ctkDICOMDatabase); + this->TaskPool->setDicomDatabase(this->DicomDatabase); + this->MetadataDialog = QSharedPointer (new ctkDICOMMetadataDialog()); + this->MetadataDialog->setObjectName("DICOMMetadata"); + this->MetadataDialog->setWindowTitle(ctkDICOMVisualBrowserWidget::tr("DICOM File Metadata")); + + this->NumberOfStudiesPerPatient = 2; + this->NumberOfSeriesPerRow = 6; + this->MinimumThumbnailSize = 300; + this->SendActionVisible = false; + + 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->IsImportFolder = false; + this->ImportDialog = nullptr; + + this->FilteringDate = ctkDICOMPatientItemWidget::DateType::Any; + this->IsGUIUpdating = false; + + this->ServerNodeWidget = new ctkDICOMServerNodeWidget2(); + this->ServerNodeWidget->setTaskPool(this->TaskPool); + this->connectTaskPool(); +} + +//---------------------------------------------------------------------------- +ctkDICOMVisualBrowserWidgetPrivate::~ctkDICOMVisualBrowserWidgetPrivate() +{ + this->removeAllPatientItemWidgets(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::init() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + this->setupUi(q); + + 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(onQueryPatient())); + + this->ServersSettingsCollapsibleGroupBox->layout()->addWidget(this->ServerNodeWidget); + + QSize iconSize(28, 28); + this->PatientsTabWidget->setIconSize(iconSize); + this->PatientsTabWidget->clear(); + + // setup patients menu on a tool button on the tab bar + QTabBar* tabWidget = this->PatientsTabWidget->tabBar(); + tabWidget->setExpanding(true); + + this->patientsTabMenuToolButton = new QToolButton(); + this->patientsTabMenuToolButton->setObjectName("patientsTabMenuToolButton"); + this->patientsTabMenuToolButton->setIconSize(iconSize); + this->patientsTabMenuToolButton->setFixedHeight(38); + 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::TopRightCorner); + + 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"), ctkDICOMVisualBrowserWidget::ImportDirectoryAddLink); + importDirectoryModeComboBox->addItem(ctkDICOMVisualBrowserWidget::tr("Copy"), 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(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::disconnectTaskPool() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (!this->TaskPool) + { + return; + } + + ctkDICOMVisualBrowserWidget::disconnect(this->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + q, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + ctkDICOMVisualBrowserWidget::disconnect(this->TaskPool.data(), SIGNAL(taskFailed(QString)), + q, SLOT(onTaskFailed(QString))); + ctkDICOMVisualBrowserWidget::disconnect(this->TaskPool->indexer(), SIGNAL(progress(int)), q, SLOT(onIndexingProgress(int))); + ctkDICOMVisualBrowserWidget::disconnect(this->TaskPool->indexer(), SIGNAL(progressStep(QString)),q, SLOT(onIndexingProgressStep(QString))); + ctkDICOMVisualBrowserWidget::disconnect(this->TaskPool->indexer(), SIGNAL(progressDetail(QString)), q, SLOT(onIndexingProgressDetail(QString))); + ctkDICOMVisualBrowserWidget::disconnect(this->TaskPool->indexer(), SIGNAL(indexingComplete(int,int,int,int)), q, SLOT(onIndexingComplete(int,int,int,int))); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::connectTaskPool() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (!this->TaskPool) + { + return; + } + + ctkDICOMVisualBrowserWidget::connect(this->TaskPool.data(), SIGNAL(progressTaskDetail(ctkDICOMTaskResults*)), + q, SLOT(updateGUIFromTaskPool(ctkDICOMTaskResults*))); + ctkDICOMVisualBrowserWidget::connect(this->TaskPool.data(), SIGNAL(taskFailed(QString)), + q, SLOT(onTaskFailed(QString))); + ctkDICOMVisualBrowserWidget::connect(this->TaskPool->indexer(), SIGNAL(progress(int)), q, SLOT(onIndexingProgress(int))); + ctkDICOMVisualBrowserWidget::connect(this->TaskPool->indexer(), SIGNAL(progressStep(QString)),q, SLOT(onIndexingProgressStep(QString))); + ctkDICOMVisualBrowserWidget::connect(this->TaskPool->indexer(), SIGNAL(progressDetail(QString)), q, SLOT(onIndexingProgressDetail(QString))); + ctkDICOMVisualBrowserWidget::connect(this->TaskPool->indexer(), 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->TaskPool || !this->TaskPool->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->TaskPool->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->TaskPool || !this->TaskPool->indexer()) + { + logger.error("importFiles failed, no task pool has been set. \n"); + return; + } + + // Start background indexing + this->TaskPool->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::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); + 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() +{ + 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 + QString background = "QWidget { background-color: white }"; + this->FilteringPatientIDSearchBox->setStyleSheet(this->FilteringPatientIDSearchBox->styleSheet() + background); + this->FilteringPatientNameSearchBox->setStyleSheet(this->FilteringPatientNameSearchBox->styleSheet() + background); + this->FilteringDateComboBox->setStyleSheet(this->FilteringDateComboBox->styleSheet() + background); + this->FilteringStudyDescriptionSearchBox->setStyleSheet(this->FilteringStudyDescriptionSearchBox->styleSheet() + background); + this->FilteringSeriesDescriptionSearchBox->setStyleSheet(this->FilteringSeriesDescriptionSearchBox->styleSheet() + background); + this->FilteringModalityCheckableComboBox->setStyleSheet(this->FilteringModalityCheckableComboBox->styleSheet() + background); + + background = "QWidget { background-color: yellow }"; + QStringList patientList = this->DicomDatabase->patients(); + if (patientList.count() == 0) + { + this->FilteringPatientIDSearchBox->setStyleSheet(this->FilteringPatientIDSearchBox->styleSheet() + background); + this->FilteringPatientNameSearchBox->setStyleSheet(this->FilteringPatientNameSearchBox->styleSheet() + background); + return; + } + + QStringList filteredPatientListByName = + this->filterPatientList(patientList, "PatientsName", this->FilteringPatientName); + QStringList filteredPatientListByID = + this->filterPatientList(patientList, "PatientID", this->FilteringPatientID); + + if (filteredPatientListByName.count() == 0) + { + this->FilteringPatientNameSearchBox->setStyleSheet(this->FilteringPatientNameSearchBox->styleSheet() + background); + } + + if (filteredPatientListByID.count() == 0) + { + this->FilteringPatientIDSearchBox->setStyleSheet(this->FilteringPatientIDSearchBox->styleSheet() + background); + } + + QStringList filteredPatientList = filteredPatientListByName + filteredPatientListByID; + if (filteredPatientList.count() == 0) + { + return; + } + + QStringList studiesList; + foreach (QString patientItem, filteredPatientList) + { + studiesList.append(this->DicomDatabase->studiesForPatient(patientItem)); + } + + QStringList filteredStudyListByDate = + this->filterStudyList(studiesList, "StudyDate", + QString::number(ctkDICOMPatientItemWidget::getNDaysFromFilteringDate(this->FilteringDate))); + QStringList filteredStudyListByDescription = + this->filterStudyList(studiesList, "StudyDescription", this->FilteringStudyDescription); + + if (filteredStudyListByDate.count() == 0) + { + this->FilteringDateComboBox->setStyleSheet(this->FilteringDateComboBox->styleSheet() + background); + } + + if (filteredStudyListByDescription.count() == 0) + { + this->FilteringStudyDescriptionSearchBox->setStyleSheet(this->FilteringStudyDescriptionSearchBox->styleSheet() + background); + } + + QStringList filteredStudyList = filteredStudyListByDate + filteredStudyListByDescription; + if (filteredStudyList.count() == 0) + { + return; + } + + QStringList seriesList; + foreach (QString studyItem, filteredStudyList) + { + QString studyInstanceUID = this->DicomDatabase->fieldForStudy("StudyInstanceUID", studyItem); + seriesList.append(this->DicomDatabase->seriesForStudy(studyInstanceUID)); + } + + QStringList filteredSeriesListByModality = + this->filterSeriesList(seriesList, "Modality", this->FilteringModalities); + QStringList filteredSeriesListByDescription = + this->filterSeriesList(seriesList, "SeriesDescription", this->FilteringSeriesDescription); + + if (filteredSeriesListByModality.count() == 0) + { + this->FilteringModalityCheckableComboBox->setStyleSheet(this->FilteringModalityCheckableComboBox->styleSheet() + background); + } + + if (filteredSeriesListByDescription.count() == 0) + { + this->FilteringSeriesDescriptionSearchBox->setStyleSheet(this->FilteringSeriesDescriptionSearchBox->styleSheet() + background); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::retrieveSeries() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (!this->TaskPool) + { + logger.error("retrieveSeries failed, no task pool has been set. \n"); + return; + } + + ctkDICOMPatientItemWidget* currentPatientItemWidget = + qobject_cast(this->PatientsTabWidget->currentWidget()); + if (!currentPatientItemWidget) + { + return; + } + + 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) + { + return; + } + + QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); + this->TaskPool->stopAllTasksNotStarted(); + // Stop running tasks, but do not stop the tasks associated to the selected ones + + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, seriesWidgetsList) + { + if (selectedSeriesWidgetsList.indexOf(seriesItemWidget) == -1) + { + this->TaskPool->stopTasks(seriesItemWidget->studyInstanceUID(), + seriesItemWidget->seriesInstanceUID()); + } + } + this->TaskPool->waitForFinish(); + + q->onStop(); + + QStringList seriesInstanceUIDs; + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, selectedSeriesWidgetsList) + { + seriesInstanceUIDs.append(seriesItemWidget->seriesInstanceUID()); + } + + q->emit seriesRetrieved(seriesInstanceUIDs); + 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 QString& filterName, + const QString& filterValue) +{ + QStringList filteredPatientList; + if (!this->DicomDatabase) + { + logger.error("filterPatientList failed, no DICOM Database has been set. \n"); + return filteredPatientList; + } + + foreach (QString patientItem, patientList) + { + QString filter = this->DicomDatabase->fieldForPatient(filterName, patientItem); + if (!filter.contains(filterValue, Qt::CaseInsensitive)) + { + continue; + } + + filteredPatientList.append(patientItem); + } + + return filteredPatientList; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::filterStudyList(const QStringList& studyList, + const QString &filterName, + const QString &filterValue) +{ + QStringList filteredStudyList; + if (!this->DicomDatabase) + { + logger.error("filterStudyList failed, no DICOM Database has been set. \n"); + return filteredStudyList; + } + + foreach (QString studyItem, studyList) + { + QString filter = this->DicomDatabase->fieldForStudy(filterName, studyItem); + if (filterName == "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) + { + continue; + } + } + } + else if (!filter.contains(filterValue)) + { + continue; + } + + filteredStudyList.append(studyItem); + } + + return filteredStudyList; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::filterSeriesList(const QStringList& seriesList, + const QString &filterName, + const QString &filterValue) +{ + QStringList filteredSeriesList; + if (!this->DicomDatabase) + { + logger.error("filterSeriesList failed, no DICOM Database has been set. \n"); + return filteredSeriesList; + } + + foreach (QString seriesItem, seriesList) + { + QString filter = this->DicomDatabase->fieldForSeries(filterName, seriesItem); + if (!filter.contains(filterValue)) + { + continue; + } + + filteredSeriesList.append(seriesItem); + } + + return filteredSeriesList; +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMVisualBrowserWidgetPrivate::filterSeriesList(const QStringList &seriesList, + const QString &filterName, + const QStringList &filterValues) +{ + QStringList filteredSeriesList; + if (!this->DicomDatabase) + { + logger.error("filterSeriesList failed, no DICOM Database has been set. \n"); + return filteredSeriesList; + } + + foreach (QString seriesItem, seriesList) + { + QString filter = this->DicomDatabase->fieldForSeries(filterName, seriesItem); + if (!filterValues.contains("Any") && !filterValues.contains(filter)) + { + continue; + } + + filteredSeriesList.append(seriesItem); + } + + return filteredSeriesList; +} + +//---------------------------------------------------------------------------- +// 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 +} + +//---------------------------------------------------------------------------- +ctkDICOMTaskPool* ctkDICOMVisualBrowserWidget::taskPool()const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->TaskPool.data(); +} + +//---------------------------------------------------------------------------- +QSharedPointer ctkDICOMVisualBrowserWidget::taskPoolShared()const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->TaskPool; +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::setTaskPool(ctkDICOMTaskPool& taskPool) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->disconnectTaskPool(); + d->TaskPool = QSharedPointer(&taskPool, skipDelete); + d->ServerNodeWidget->setTaskPool(d->TaskPool); + d->connectTaskPool(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidget::setTaskPool(QSharedPointer taskPool) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->disconnectTaskPool(); + d->TaskPool = taskPool; + d->ServerNodeWidget->setTaskPool(d->TaskPool); + d->connectTaskPool(); +} + +//---------------------------------------------------------------------------- +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(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 char *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 char *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 char *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::setNumberOfSeriesPerRow(int numberOfSeriesPerRow) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->NumberOfSeriesPerRow = numberOfSeriesPerRow; +} + +//------------------------------------------------------------------------------ +int ctkDICOMVisualBrowserWidget::numberOfSeriesPerRow() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->NumberOfSeriesPerRow; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setMinimumThumbnailSize(int minimumThumbnailSize) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->MinimumThumbnailSize = minimumThumbnailSize; +} + +//------------------------------------------------------------------------------ +int ctkDICOMVisualBrowserWidget::minimumThumbnailSize() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->MinimumThumbnailSize; +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::setSendActionVisible(bool visible) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->SendActionVisible = visible; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMVisualBrowserWidget::isSendActionVisible() const +{ + Q_D(const ctkDICOMVisualBrowserWidget); + return d->SendActionVisible; +} + +//---------------------------------------------------------------------------- +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->setMinimumThumbnailSize(d->MinimumThumbnailSize); + patientItemWidget->setNumberOfSeriesPerRow(d->NumberOfSeriesPerRow); + patientItemWidget->setNumberOfStudiesPerPatient(d->NumberOfStudiesPerPatient); + patientItemWidget->setDicomDatabase(d->DicomDatabase); + patientItemWidget->setTaskPool(d->TaskPool); + 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()); + 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(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() + || (!QDir(absDirectory).isEmpty() && !QFile(databaseFileName).exists())) + { + logger.warn(tr("Database folder does not contain ctkDICOM.sql file: ") + absDirectory + "\n"); + success = false; + } + + if (success) + { + bool databaseOpenSuccess = false; + try + { + d->DicomDatabase->openDatabase(databaseFileName); + databaseOpenSuccess = d->DicomDatabase->isOpen(); + } + catch (std::exception e) + { + databaseOpenSuccess = false; + } + if (!databaseOpenSuccess || d->DicomDatabase->schemaVersionLoaded().isEmpty()) + { + logger.warn(tr("Database error: %1 \n").arg(d->DicomDatabase->lastError())); + d->DicomDatabase->closeDatabase(); + 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(); + success = false; + } + } + + // 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(); + } + + // 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(QStringList directories, ImportDirectoryMode mode) +{ + Q_D(ctkDICOMVisualBrowserWidget); + foreach (const QString& directory, directories) + { + d->importDirectory(directory, mode); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::importDirectory(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->TaskPool || !d->TaskPool->indexer()) + { + logger.error("waitForImportFinished failed, no task pool has been set. \n"); + return; + } + d->TaskPool->indexer()->waitForImportFinished(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onImportDirectory(QString directory, ImportDirectoryMode mode) +{ + this->importDirectory(directory, mode); +} + +//------------------------------------------------------------------------------ +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); + if (!d->IsImportFolder) + { + return; + } + + 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->IsImportFolder = false; + + d->createPatients(); +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMVisualBrowserWidget::fileListForCurrentSelection(ctkDICOMModel::IndexType level, + 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, + QList selectedWidgets) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (!d->DicomDatabase) + { + logger.error("removeSelectedItems failed, no DICOM database has been set. \n"); + return; + } + + // Stop any fetching task. + this->onStop(); + + 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); + } + } + } + + 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::onQueryPatient() +{ + 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 && + d->DicomDatabase->patients().count() == 0 && + filtersEmpty) + { + QString background = "QWidget { background-color: yellow }"; + d->FilteringPatientIDSearchBox->setStyleSheet(d->FilteringPatientIDSearchBox->styleSheet() + background); + d->FilteringPatientNameSearchBox->setStyleSheet(d->FilteringPatientNameSearchBox->styleSheet() + background); + d->FilteringDateComboBox->setStyleSheet(d->FilteringDateComboBox->styleSheet() + background); + d->FilteringStudyDescriptionSearchBox->setStyleSheet(d->FilteringStudyDescriptionSearchBox->styleSheet() + background); + d->FilteringSeriesDescriptionSearchBox->setStyleSheet(d->FilteringSeriesDescriptionSearchBox->styleSheet() + background); + d->FilteringModalityCheckableComboBox->setStyleSheet(d->FilteringModalityCheckableComboBox->styleSheet() + background); + + 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->TaskPool && d->TaskPool->getNumberOfQueryRetrieveServers() == 0)) + { + d->updateFiltersWarnings(); + } + else if (d->TaskPool && d->TaskPool->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->TaskPool->setFilters(parameters); + d->TaskPool->queryPatients(QThread::NormalPriority); + + d->QueryPatientPushButton->setIcon(QIcon(":/Icons/wait.svg")); + d->ProgressFrame->show(); + d->ProgressDetailLineEdit->hide(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::updateGUIFromTaskPool(ctkDICOMTaskResults *taskResults) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->updateFiltersWarnings(); + d->ProgressFrame->hide(); + d->QueryPatientPushButton->setIcon(QIcon(":/Icons/query.svg")); + if (!taskResults || taskResults->typeOfTask() != ctkDICOMTaskResults::TaskType::QueryPatients) + { + return; + } + + if (taskResults->datasets().count() == 0) + { + d->WarningPushButton->setText(tr("The query provided no results. Please refine your filters.")); + d->WarningPushButton->show(); + } + + d->createPatients(); + d->TaskPool->deleteTask(taskResults->taskUID()); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onTaskFailed(QString taskType) +{ + Q_D(ctkDICOMVisualBrowserWidget); + if (taskType == "Query") + { + d->updateFiltersWarnings(); + d->ProgressFrame->hide(); + d->QueryPatientPushButton->setIcon(QIcon(":/Icons/query.svg")); + } + + d->WarningPushButton->setText(tr("%1 task failed to fetch the data." + "\nFor more information please open the error report console. \n").arg(taskType)); + 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"); + QAction *deleteAction = new QAction(deleteString, patientMenu); + patientMenu->addAction(deleteAction); + + 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") : + tr("Delete %1 studies").arg(numberOfSelectedStudies); + QAction *deleteAction = new QAction(deleteString, studyMenu); + studyMenu->addAction(deleteAction); + + 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") : + tr("Delete %1 series").arg(numberOfSelectedSeries); + QAction *deleteAction = new QAction(deleteString, seriesMenu); + seriesMenu->addAction(deleteAction); + + 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 = d->patientsTabMenuToolButton->geometry().bottomLeft(); + globalPos.setY(globalPos.y() + this->geometry().top() + d->patientsTabMenuToolButton->height() * 3); + globalPos.setX(globalPos.x() + this->geometry().left()); + + 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"); + QAction *deleteAction = new QAction(deleteString, patientMenu); + deleteAction->setIcon(QIcon(":Icons/delete.svg")); + patientMenu->addAction(deleteAction); + + 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, + 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::DirectoryOnly); + 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(QString dirPath, 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; + } + } + + int fileNumber = 0; + 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)) + { + //: %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(this); + copyErrorMessageBox.setText(errorString); + copyErrorMessageBox.setIcon(QMessageBox::Warning); + copyErrorMessageBox.exec(); + return; + } + if (QFile::exists(destinationFileName)) + { + //: %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) + { + //: %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++; + } + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onImportDirectoriesSelected(QStringList directories) +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->IsImportFolder = true; + 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->TaskPool && d->TaskPool->totalTasks() > d->TaskPool->numberOfPersistentTasks()) + { + 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->TaskPool) + { + return; + } + + QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); + d->TaskPool->stopAllTasks(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(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..63f921a327 --- /dev/null +++ b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h @@ -0,0 +1,365 @@ +/*========================================================================= + + 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 + +#include "ctkDICOMWidgetsExport.h" + +// Qt includes +#include + +// CTK includes +#include +#include "ctkDICOMModel.h" +#include "ctkErrorLogLevel.h" + +class ctkCollapsibleGroupBox; +class ctkDICOMVisualBrowserWidgetPrivate; +class ctkDICOMDatabase; +class ctkFileDialog; +class ctkDICOMServer; +class ctkDICOMServerNodeWidget2; +class ctkDICOMTaskResults; + +/// \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 executed by a task pool manager in separate threads with +/// a priority queue. +/// +/// 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(int numberOfSeriesPerRow READ numberOfSeriesPerRow WRITE setNumberOfSeriesPerRow); + Q_PROPERTY(int minimumThumbnailSize READ minimumThumbnailSize WRITE setMinimumThumbnailSize); + Q_PROPERTY(bool sendActionVisible READ isSendActionVisible WRITE setSendActionVisible) + 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 ctkDICOMTaskPool* taskPool() const; + /// Return the task pool as a shared pointer + /// (not Python-wrappable). + QSharedPointer taskPoolShared() const; + /// Set the task pool. + Q_INVOKABLE void setTaskPool(ctkDICOMTaskPool& taskPool); + /// Set the task pool as a shared pointer + /// (not Python-wrappable). + void setTaskPool(QSharedPointer taskPool); + + /// 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(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 char* connectionName); + Q_INVOKABLE void addServer(ctkDICOMServer* server); + Q_INVOKABLE void removeServer(const char* connectionName); + Q_INVOKABLE void removeNthServer(int id); + Q_INVOKABLE void removeAllServers(); + Q_INVOKABLE QString getServerNameFromIndex(int id); + Q_INVOKABLE int getServerIndexFromName(const char* 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. + 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; + + /// Number of series displayed per row + /// 6 by default + void setNumberOfSeriesPerRow(int numberOfSeriesPerRow); + int numberOfSeriesPerRow() const; + + /// Minimum thumbnail size in pixel + /// 300 by default + void setMinimumThumbnailSize(int minimumThumbnailSize); + int minimumThumbnailSize() const; + + /// Set if send action on right click context menu is available + /// false by default + void setSendActionVisible(bool visible); + bool isSendActionVisible() 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(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(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(); + + /// \deprecated importDirectory() should be used + void onImportDirectory(QString directory, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode = ImportDirectoryAddLink); + + /// 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); + + void onFilteringPatientIDChanged(); + void onFilteringPatientNameChanged(); + void onFilteringStudyDescriptionChanged(); + void onFilteringSeriesDescriptionChanged(); + void onFilteringModalityCheckableComboBoxChanged(); + void onFilteringDateComboBoxChanged(int); + void onQueryPatient(); + void updateGUIFromTaskPool(ctkDICOMTaskResults*); + void onTaskFailed(QString); + 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(QStringList uids); + + /// Get file list for right click selection + QStringList fileListForCurrentSelection(ctkDICOMModel::IndexType level, 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, QList selectedWidgets = QList()); + /// Export the items associated with the selected widget + void exportSelectedItems(ctkDICOMModel::IndexType level, QList selectedWidgets); + /// Export the series associated with the selected UIDs + void exportSeries(QString dirPath, 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(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..abb2bf3810 100644 --- a/Libs/Widgets/Resources/UI/ctkThumbnailLabel.ui +++ b/Libs/Widgets/Resources/UI/ctkThumbnailLabel.ui @@ -6,29 +6,120 @@ 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 diff --git a/Libs/Widgets/ctkCheckableHeaderView.cpp b/Libs/Widgets/ctkCheckableHeaderView.cpp index 02ab7f2fb0..5ee6a5613f 100644 --- a/Libs/Widgets/ctkCheckableHeaderView.cpp +++ b/Libs/Widgets/ctkCheckableHeaderView.cpp @@ -398,14 +398,14 @@ void ctkCheckableHeaderView::initStyleSectionOption(QStyleOptionHeader *option, } QVariant variant = this->model()->headerData(section, this->orientation(), - Qt::DecorationRole); + Qt::DecorationRole); option->icon = qvariant_cast(variant); if (option->icon.isNull()) { option->icon = qvariant_cast(variant); } QVariant foregroundBrush = this->model()->headerData(section, this->orientation(), - Qt::ForegroundRole); + Qt::ForegroundRole); if (foregroundBrush.canConvert()) { option->palette.setBrush(QPalette::ButtonText, qvariant_cast(foregroundBrush)); @@ -413,7 +413,7 @@ void ctkCheckableHeaderView::initStyleSectionOption(QStyleOptionHeader *option, //QPointF oldBO = painter->brushOrigin(); QVariant backgroundBrush = this->model()->headerData(section, this->orientation(), - Qt::BackgroundRole); + Qt::BackgroundRole); if (backgroundBrush.canConvert()) { option->palette.setBrush(QPalette::Button, qvariant_cast(backgroundBrush)); diff --git a/Libs/Widgets/ctkThumbnailLabel.cpp b/Libs/Widgets/ctkThumbnailLabel.cpp index 71879d211a..cce96b9369 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->TextLabel->isVisible()) + { + if (this->TextPosition & Qt::AlignTop) + { + size.setHeight(size.height() - this->TextLabel->height()); + } + else if (this->TextPosition & Qt::AlignBottom) + { + size.setHeight(size.height() - this->TextLabel->height()); + } + else if (this->TextPosition & Qt::AlignLeft) + { + size.setWidth(size.width() - this->TextLabel->width()); + } + else if (this->TextPosition & Qt::AlignRight) + { + size.setWidth(size.width() - this->TextLabel->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,6 +147,38 @@ ctkThumbnailLabel::~ctkThumbnailLabel() { } +//---------------------------------------------------------------------------- +QLabel* ctkThumbnailLabel::textLabel() +{ + Q_D(ctkThumbnailLabel); + + return d->TextLabel; +} + +//---------------------------------------------------------------------------- +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) { @@ -160,7 +221,7 @@ void ctkThumbnailLabel::setTextPosition(const Qt::Alignment& position) { row = 0; } - else if (position &Qt::AlignBottom) + else if (position & Qt::AlignBottom) { row = 2; } @@ -214,6 +275,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 { diff --git a/Libs/Widgets/ctkThumbnailLabel.h b/Libs/Widgets/ctkThumbnailLabel.h index 3b7a20ade9..fd9aa3db0d 100644 --- a/Libs/Widgets/ctkThumbnailLabel.h +++ b/Libs/Widgets/ctkThumbnailLabel.h @@ -29,6 +29,10 @@ 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 +53,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 +70,11 @@ class CTK_WIDGETS_EXPORT ctkThumbnailLabel : public QWidget explicit ctkThumbnailLabel(QWidget* parent=0); virtual ~ctkThumbnailLabel(); + Q_INVOKABLE QLabel* textLabel(); + Q_INVOKABLE QFrame* pixmapFrame(); + Q_INVOKABLE QLabel* pixmapLabel(); + Q_INVOKABLE QProgressBar* operationProgressBar(); + void setText(const QString& text); QString text()const; @@ -73,6 +84,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);