From 7f2f24a051fd58118e9d6ddb464eaf085ec05853 Mon Sep 17 00:00:00 2001 From: Andras Lasso Date: Fri, 11 Oct 2019 14:25:56 -0400 Subject: [PATCH] ENH: DICOM browser rework Huge performance and usability improvement of DICOM browser. DICOM indexer: - Implemented background indexing (in worker thread): indexing may take several minutes and it is not desirable to block the entire application for this long time - Reduced indexing time by a factor of 3x (previously, inefficient database inserts, unnecessary queries, and table view updates slowed down the insertions) DICOM browser: - Removed popups from DICOM browser to not interfere with user workflow (questions, status updates, etc. are display in the browser layout) - Do not block the GUI while importing data sets, provide continuous, detailed update about indexing progress - Add separate displayed fields preset for DICOM query-retrieve database (this database is built without having access to the files, so less displayed fields can be computed) - Condensed table headers in horizontal mode (one row can hold all the search/filter fields instead of using one for each) - Set default sorting of columns (based on last insertion time, study date, and series number) - Added option to show DICOM metadata (right-click menu) - Added option to export datasets to file system (right-click menu) DICOM database: - Remove orphan empty directories after deleting files from the database - Emit signals when the database is opened or closed - Added display option for specifying column that is used for sorting by default - Improved displayed field update speed by about a factor of 100x (batch execution is very slow on sqlite and individual queries in a transaction should be used instead) DICOM query/retrieve: - Fixed crash when query/retrieve was used from Python (where database cannot be passed to the Q/R widget as a shared pointer) --- Applications/ctkDICOM/ctkDICOMMain.cpp | 6 +- Applications/ctkDICOM2/ctkDICOM2Main.cpp | 33 +- .../ctkDICOMHost/ctkDICOMHostMain.cpp | 6 +- .../ctkDICOMIndexer/ctkDICOMIndexerMain.cpp | 8 +- .../ctkDICOMQueryRetrieveMain.cpp | 6 +- Libs/DICOM/Core/Resources/ctkDICOMCore.qrc | 1 + Libs/DICOM/Core/Resources/dicom-qr-schema.sql | 149 +++ Libs/DICOM/Core/Resources/dicom-schema.sql | 12 +- Libs/DICOM/Core/ctkDICOMDatabase.cpp | 1151 +++++++++++------ Libs/DICOM/Core/ctkDICOMDatabase.h | 47 +- .../Core/ctkDICOMDisplayedFieldGenerator.cpp | 279 ++-- .../Core/ctkDICOMDisplayedFieldGenerator.h | 3 +- Libs/DICOM/Core/ctkDICOMIndexer.cpp | 538 ++++++-- Libs/DICOM/Core/ctkDICOMIndexer.h | 111 +- Libs/DICOM/Core/ctkDICOMIndexer_p.h | 206 ++- Libs/DICOM/Core/ctkDICOMRetrieve.cpp | 8 +- .../Widgets/Resources/UI/ctkDICOMBrowser.ui | 224 +++- .../Resources/UI/ctkDICOMObjectListWidget.ui | 8 +- .../UI/ctkDICOMQueryRetrieveWidget.ui | 2 +- .../Resources/UI/ctkDICOMTableManager.ui | 134 +- .../Widgets/Resources/UI/ctkDICOMTableView.ui | 115 +- .../Testing/Cpp/ctkDICOMBrowserTest.cpp | 14 +- Libs/DICOM/Widgets/ctkDICOMAppWidget.cpp | 18 +- Libs/DICOM/Widgets/ctkDICOMBrowser.cpp | 1111 ++++++++++------ Libs/DICOM/Widgets/ctkDICOMBrowser.h | 108 +- .../Widgets/ctkDICOMQueryRetrieveWidget.cpp | 37 +- Libs/DICOM/Widgets/ctkDICOMTableManager.cpp | 107 +- Libs/DICOM/Widgets/ctkDICOMTableManager.h | 19 +- Libs/DICOM/Widgets/ctkDICOMTableView.cpp | 190 ++- Libs/DICOM/Widgets/ctkDICOMTableView.h | 56 +- .../ctkDICOMWidgetsPythonQtDecorators.h | 9 +- 31 files changed, 3284 insertions(+), 1432 deletions(-) create mode 100644 Libs/DICOM/Core/Resources/dicom-qr-schema.sql diff --git a/Applications/ctkDICOM/ctkDICOMMain.cpp b/Applications/ctkDICOM/ctkDICOMMain.cpp index bd97a13bb7..71780e91a9 100644 --- a/Applications/ctkDICOM/ctkDICOMMain.cpp +++ b/Applications/ctkDICOM/ctkDICOMMain.cpp @@ -57,17 +57,17 @@ int main(int argc, char** argv) if (argc > 1) { QString directory(argv[1]); - settings.setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), directory); + settings.setValue("DatabaseDirectory", directory); settings.sync(); } - if ( settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "") == "" ) + if ( settings.value("DatabaseDirectory", "") == "" ) { databaseDirectory = QString("./ctkDICOM-Database"); std::cerr << "No DatabaseDirectory on command line or in settings. Using \"" << databaseDirectory.toLatin1().data() << "\".\n"; } else { - databaseDirectory = settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "").toString(); + databaseDirectory = settings.value("DatabaseDirectory", "").toString(); } QDir qdir(databaseDirectory); diff --git a/Applications/ctkDICOM2/ctkDICOM2Main.cpp b/Applications/ctkDICOM2/ctkDICOM2Main.cpp index ed11dd6ca1..6d9fa36630 100644 --- a/Applications/ctkDICOM2/ctkDICOM2Main.cpp +++ b/Applications/ctkDICOM2/ctkDICOM2Main.cpp @@ -49,39 +49,14 @@ int main(int argc, char** argv) // set up Qt resource files QResource::registerResource("./Resources/ctkDICOM.qrc"); - QSettings settings; - QString databaseDirectory; - + ctkDICOMBrowser DICOMApp; + DICOMApp.setDatabaseDirectorySettingsKey("DatabaseDirectory"); // set up the database if (argc > 1) { - QString directory(argv[1]); - settings.setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), directory); - settings.sync(); - } - - if ( settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "") == "" ) - { - databaseDirectory = QString("./ctkDICOM-Database"); - std::cerr << "No DatabaseDirectory on command line or in settings. Using \"" << databaseDirectory.toLatin1().data() << "\".\n"; - } else - { - databaseDirectory = settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "").toString(); + DICOMApp.setDatabaseDirectory(argv[1]); } - - QDir qdir(databaseDirectory); - if ( !qdir.exists(databaseDirectory) ) - { - if ( !qdir.mkpath(databaseDirectory) ) - { - std::cerr << "Could not create database directory \"" << databaseDirectory.toLatin1().data() << "\".\n"; - return EXIT_FAILURE; - } - } - - ctkDICOMBrowser DICOMApp; - - DICOMApp.setDatabaseDirectory(databaseDirectory); + DICOMApp.show(); return app.exec(); diff --git a/Applications/ctkDICOMHost/ctkDICOMHostMain.cpp b/Applications/ctkDICOMHost/ctkDICOMHostMain.cpp index 08a5e00178..b697f67644 100644 --- a/Applications/ctkDICOMHost/ctkDICOMHostMain.cpp +++ b/Applications/ctkDICOMHost/ctkDICOMHostMain.cpp @@ -129,17 +129,17 @@ int main(int argc, char** argv) if (argc > 1) { QString directory(argv[1]); - settings.setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), directory); + settings.setValue("DatabaseDirectory", directory); settings.sync(); } - if ( settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "") == "" ) + if ( settings.value("DatabaseDirectory", "") == "" ) { databaseDirectory = QString("./ctkDICOM-Database"); std::cerr << "No DatabaseDirectory on command line or in settings. Using \"" << databaseDirectory.toLatin1().data() << "\".\n"; } else { - databaseDirectory = settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "").toString(); + databaseDirectory = settings.value("DatabaseDirectory", "").toString(); } QDir qdir(databaseDirectory); diff --git a/Applications/ctkDICOMIndexer/ctkDICOMIndexerMain.cpp b/Applications/ctkDICOMIndexer/ctkDICOMIndexerMain.cpp index b4733ac19e..5fb5dab839 100644 --- a/Applications/ctkDICOMIndexer/ctkDICOMIndexerMain.cpp +++ b/Applications/ctkDICOMIndexer/ctkDICOMIndexerMain.cpp @@ -61,9 +61,9 @@ int main(int argc, char** argv) QCoreApplication app(argc, argv); QTextStream out(stdout); - ctkDICOMIndexer idx; ctkDICOMDatabase myCTK; - + ctkDICOMIndexer idx; + idx.setDatabase(&myCTK); try { @@ -73,11 +73,11 @@ int main(int argc, char** argv) myCTK.openDatabase( argv[2] ); if (argc > 4) { - idx.addDirectory(myCTK,argv[3],argv[4]); + idx.addDirectory(argv[3],argv[4]); } else { - idx.addDirectory(myCTK,argv[3]); + idx.addDirectory(argv[3]); } } } diff --git a/Applications/ctkDICOMQueryRetrieve/ctkDICOMQueryRetrieveMain.cpp b/Applications/ctkDICOMQueryRetrieve/ctkDICOMQueryRetrieveMain.cpp index 0841d916a9..77719c6e05 100644 --- a/Applications/ctkDICOMQueryRetrieve/ctkDICOMQueryRetrieveMain.cpp +++ b/Applications/ctkDICOMQueryRetrieve/ctkDICOMQueryRetrieveMain.cpp @@ -57,17 +57,17 @@ int main(int argc, char** argv) if (argc > 1) { QString directory(argv[1]); - settings.setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), directory); + settings.setValue("DatabaseDirectory", directory); settings.sync(); } - if ( settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "") == "" ) + if ( settings.value("DatabaseDirectory", "") == "" ) { databaseDirectory = QString("./ctkDICOM-Database"); std::cerr << "No DatabaseDirectory on command line or in settings. Using \"" << databaseDirectory.toLatin1().data() << "\".\n"; } else { - databaseDirectory = settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "").toString(); + databaseDirectory = settings.value("DatabaseDirectory", "").toString(); } QDir qdir(databaseDirectory); diff --git a/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc b/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc index 4c8b77bc5d..e9bd48459e 100644 --- a/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc +++ b/Libs/DICOM/Core/Resources/ctkDICOMCore.qrc @@ -1,6 +1,7 @@ dicom-schema.sql + dicom-qr-schema.sql diff --git a/Libs/DICOM/Core/Resources/dicom-qr-schema.sql b/Libs/DICOM/Core/Resources/dicom-qr-schema.sql new file mode 100644 index 0000000000..6bc96a0f39 --- /dev/null +++ b/Libs/DICOM/Core/Resources/dicom-qr-schema.sql @@ -0,0 +1,149 @@ +-- +-- A simple SQLITE3 database schema for temporary storage of query responses +-- +-- Note: the semicolon at the end is necessary for the simple parser to separate +-- the statements since the SQlite driver does not handle multiple +-- commands per QSqlQuery::exec call! +-- Note: be sure to update ctkDICOMDatabase and SchemaInfo Version +-- whenever you make a change to this schema +-- ; + +DROP TABLE IF EXISTS 'SchemaInfo' ; +DROP TABLE IF EXISTS 'Images' ; +DROP TABLE IF EXISTS 'Patients' ; +DROP TABLE IF EXISTS 'Series' ; +DROP TABLE IF EXISTS 'Studies' ; +DROP TABLE IF EXISTS 'ColumnDisplayProperties' ; +DROP TABLE IF EXISTS 'Directories' ; + +DROP INDEX IF EXISTS 'ImagesFilenameIndex' ; +DROP INDEX IF EXISTS 'ImagesSeriesIndex' ; +DROP INDEX IF EXISTS 'SeriesStudyIndex' ; +DROP INDEX IF EXISTS 'StudiesPatientIndex' ; + +CREATE TABLE 'SchemaInfo' ( 'Version' VARCHAR(1024) NOT NULL ); +INSERT INTO 'SchemaInfo' VALUES('0.6.2'); + +CREATE TABLE 'Images' ( + 'SOPInstanceUID' VARCHAR(64) NOT NULL, + 'Filename' VARCHAR(1024) NOT NULL , + 'SeriesInstanceUID' VARCHAR(64) NOT NULL , + 'InsertTimestamp' VARCHAR(20) NOT NULL , + 'DisplayedFieldsUpdatedTimestamp' DATETIME NULL , + PRIMARY KEY ('SOPInstanceUID') ); +CREATE TABLE 'Patients' ( + 'UID' INTEGER PRIMARY KEY AUTOINCREMENT, + 'PatientsName' VARCHAR(255) NULL , + 'PatientID' VARCHAR(255) NULL , + 'PatientsBirthDate' DATE NULL , + 'PatientsBirthTime' TIME NULL , + 'PatientsSex' VARCHAR(1) NULL , + 'PatientsAge' VARCHAR(10) NULL , + 'PatientsComments' VARCHAR(255) NULL , + 'InsertTimestamp' VARCHAR(20) NOT NULL , + 'DisplayedPatientsName' VARCHAR(255) NULL , + 'DisplayedNumberOfStudies' INT NULL , + 'DisplayedFieldsUpdatedTimestamp' DATETIME NULL ); +CREATE TABLE 'Studies' ( + 'StudyInstanceUID' VARCHAR(64) NOT NULL , + 'PatientsUID' INT NOT NULL , + 'StudyID' VARCHAR(255) NULL , + 'StudyDate' DATE NULL , + 'StudyTime' VARCHAR(20) NULL , + 'StudyDescription' VARCHAR(255) NULL , + 'AccessionNumber' VARCHAR(255) NULL , + 'ModalitiesInStudy' VARCHAR(255) NULL , + 'InstitutionName' VARCHAR(255) NULL , + 'ReferringPhysician' VARCHAR(255) NULL , + 'PerformingPhysiciansName' VARCHAR(255) NULL , + 'InsertTimestamp' VARCHAR(20) NOT NULL , + 'DisplayedNumberOfSeries' INT NULL , + 'DisplayedFieldsUpdatedTimestamp' DATETIME NULL , + PRIMARY KEY ('StudyInstanceUID') ); +CREATE TABLE 'Series' ( + 'SeriesInstanceUID' VARCHAR(64) NOT NULL , + 'StudyInstanceUID' VARCHAR(64) NOT NULL , + 'SeriesNumber' INT NULL , + 'SeriesDate' DATE NULL , + 'SeriesTime' VARCHAR(20) NULL , + 'SeriesDescription' VARCHAR(255) NULL , + 'Modality' VARCHAR(20) NULL , + 'BodyPartExamined' VARCHAR(255) NULL , + 'FrameOfReferenceUID' VARCHAR(64) NULL , + 'AcquisitionNumber' INT NULL , + 'ContrastAgent' VARCHAR(255) NULL , + 'ScanningSequence' VARCHAR(45) NULL , + 'EchoNumber' INT NULL , + 'TemporalPosition' INT NULL , + 'InsertTimestamp' VARCHAR(20) NOT NULL , + 'DisplayedCount' INT NULL , + 'DisplayedSize' VARCHAR(20) NULL , + 'DisplayedNumberOfFrames' INT NULL , + 'DisplayedFieldsUpdatedTimestamp' DATETIME NULL , + PRIMARY KEY ('SeriesInstanceUID') ); + +CREATE UNIQUE INDEX IF NOT EXISTS 'ImagesFilenameIndex' ON 'Images' ('Filename'); +CREATE INDEX IF NOT EXISTS 'ImagesSeriesIndex' ON 'Images' ('SeriesInstanceUID'); +CREATE INDEX IF NOT EXISTS 'SeriesStudyIndex' ON 'Series' ('StudyInstanceUID'); +CREATE INDEX IF NOT EXISTS 'StudiesPatientIndex' ON 'Studies' ('PatientsUID'); + +CREATE TABLE 'Directories' ( + 'Dirname' VARCHAR(1024) , + PRIMARY KEY ('Dirname') ); + +CREATE TABLE 'ColumnDisplayProperties' ( + 'TableName' VARCHAR(64) NOT NULL, + 'FieldName' VARCHAR(64) NOT NULL , + 'DisplayedName' VARCHAR(255) NULL , + 'Visibility' INT NULL DEFAULT 1 , + 'Weight' INT NULL , + 'Format' VARCHAR(255) NULL , + PRIMARY KEY ('TableName', 'FieldName') ); + +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'UID', '', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsName', 'Patient name', 1, 0, '{"sort": "ascending", "resizeMode":"stretch"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientID', 'Patient ID', 1, 1, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsBirthDate', 'Birth date', 1, 2, '{"resizeMode":"resizeToContents"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsBirthTime', 'Birth time', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsSex', 'Sex', 1, 3, '{"resizeMode":"resizeToContents"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsAge', 'Age', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsComments', 'Comments', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'InsertTimestamp', 'Date added', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'DisplayedPatientsName', 'Patient name', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'DisplayedNumberOfStudies', 'Studies', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'DisplayedFieldsUpdatedTimestamp', '', 0, 0, ''); + +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyInstanceUID', '', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'PatientsUID', '', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyID', 'Study ID', 1, 2, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyDate', 'Study date', 1, 1, '{"resizeMode":"resizeToContents", "sort": "ascending"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyTime', 'Study time', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyDescription', 'Study description', 1, 3, '{"resizeMode":"stretch"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'AccessionNumber', 'Accession #', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'ModalitiesInStudy', 'Modalities', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'InstitutionName', 'Institution', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'ReferringPhysician', 'Referring physician', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'PerformingPhysiciansName', 'Performing physician', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'InsertTimestamp', 'Date added', 0, 5, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'DisplayedNumberOfSeries', 'Series', 0, 4, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'DisplayedFieldsUpdatedTimestamp', '', 0, 0, ''); + +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesInstanceUID', '', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'StudyInstanceUID', '', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesNumber', 'Series #', 1, 0, '{"resizeMode":"resizeToContents", "sort": "ascending"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesDate', 'Series date', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesTime', 'Series time', 1, 3, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesDescription', 'Series description', 1, 2, '{"resizeMode":"stretch"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'Modality', 'Modality', 1, 1, '{"resizeMode":"resizeToContents"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'BodyPartExamined', 'Body part', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'FrameOfReferenceUID', '', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'AcquisitionNumber', 'Acquisition #', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'ContrastAgent', 'Contrast agent', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'ScanningSequence', 'Scanning sequence', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'EchoNumber', 'Echo #', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'TemporalPosition', 'Temporal position', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'InsertTimestamp', 'Date added', 0, 6, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedSize', 'Size', 0, 4, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedCount', 'Count', 0, 5, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedNumberOfFrames', 'Number of frames', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedFieldsUpdatedTimestamp', '', 0, 0, ''); diff --git a/Libs/DICOM/Core/Resources/dicom-schema.sql b/Libs/DICOM/Core/Resources/dicom-schema.sql index 8101022d58..ae33608cc3 100644 --- a/Libs/DICOM/Core/Resources/dicom-schema.sql +++ b/Libs/DICOM/Core/Resources/dicom-schema.sql @@ -108,15 +108,15 @@ INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsBirthTime', INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsSex', 'Sex', 1, 4, '{"resizeMode":"resizeToContents"}'); INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsAge', 'Age', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsComments', 'Comments', 0, 0, ''); -INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'InsertTimestamp', 'Date added', 1, 6, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'InsertTimestamp', 'Date added', 1, 6, '{"sort": "descending"}'); INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'DisplayedPatientsName', 'Patient name', 1, 1, '{"resizeMode":"stretch"}'); INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'DisplayedNumberOfStudies', 'Studies', 1, 5, '{"resizeMode":"resizeToContents"}'); INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'DisplayedFieldsUpdatedTimestamp', '', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyInstanceUID', '', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'PatientsUID', '', 0, 0, ''); -INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyID', 'Study ID', 1, 1, ''); -INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyDate', 'Study date', 1, 2, '{"resizeMode":"resizeToContents"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyID', 'Study ID', 1, 2, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyDate', 'Study date', 1, 1, '{"resizeMode":"resizeToContents", "sort": "ascending"}'); INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyTime', 'Study time', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyDescription', 'Study description', 1, 3, '{"resizeMode":"stretch"}'); INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'AccessionNumber', 'Accession #', 0, 0, ''); @@ -130,7 +130,7 @@ INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'DisplayedFieldsUpdated INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesInstanceUID', '', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'StudyInstanceUID', '', 0, 0, ''); -INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesNumber', 'Series #', 1, 1, '{"resizeMode":"resizeToContents"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesNumber', 'Series #', 1, 1, '{"resizeMode":"resizeToContents", "sort": "ascending"}'); INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesDate', 'Series date', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesTime', 'Series time', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'SeriesDescription', 'Series description', 1, 2, '{"resizeMode":"stretch"}'); @@ -143,7 +143,7 @@ INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'ScanningSequence', INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'EchoNumber', 'Echo #', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'TemporalPosition', 'Temporal position', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'InsertTimestamp', 'Date added', 1, 6, ''); -INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedCount', 'Count', 1, 4, '{"resizeMode":"resizeToContents"}'); -INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedSize', 'Size', 1, 5, '{"resizeMode":"resizeToContents"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedSize', 'Size', 1, 4, '{"resizeMode":"resizeToContents"}'); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedCount', 'Count', 1, 5, '{"resizeMode":"resizeToContents"}'); INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedNumberOfFrames', 'Number of frames', 0, 0, ''); INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedFieldsUpdatedTimestamp', '', 0, 0, ''); diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index 314487c912..40cf22c0d0 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -90,13 +90,27 @@ class ctkDICOMDatabasePrivate bool loggedExecBatch(QSqlQuery& query); bool LoggedExecVerbose; - /// Group several inserts into a single transaction - void beginTransaction(); - void endTransaction(); + bool removeImage(const QString& sopInstanceUID); + + /// Store copy of the dataset in database folder. + /// If the original file is available then that will be inserted. If not then a file is created from the dataset object. + bool storeDatasetFile(const ctkDICOMItem& dataset, const QString& originalFilePath, + const QString& studyInstanceUID, const QString& seriesInstanceUID, const QString& sopInstanceUID, QString& storedFilePath); + + /// Returns false in case of an error + bool indexingStatusForFile(const QString& filePath, const QString& sopInstanceUID, bool& datasetInDatabase, bool& datasetUpToDate, QString& databaseFilename); + + /// Retrieve thumbnail from file and store in database folder. + bool storeThumbnailFile(const QString& originalFilePath, + const QString& studyInstanceUID, const QString& seriesInstanceUID, const QString& sopInstanceUID); + + /// Get basic UIDs for a data set, return true if the data set has all the required tags + bool uidsForDataSet(const ctkDICOMItem& dataset, QString& patientsName, QString& patientID, QString& studyInstanceUID, QString& seriesInstanceUID); + bool uidsForDataSet(QString& patientsName, QString& patientID, QString& studyInstanceUID); /// 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& ctkDataset, const QString& filePath, bool storeFile = true, bool generateThumbnail = true); + void insert ( const ctkDICOMItem& dataset, const QString& filePath, bool storeFile = true, bool generateThumbnail = true); /// Copy the complete list of files to an extra table QStringList allFilesInDatabase(); @@ -105,13 +119,14 @@ class ctkDICOMDatabasePrivate /// \return Success flag bool applyDisplayedFieldsChanges( QMap > &displayedFieldsMapSeries, QMap > &displayedFieldsMapStudy, - QVector > &displayedFieldsVectorPatient ); + QMap > &displayedFieldsMapPatient ); - /// Find patient by both patient ID and patient name and return its index and insert it in the given fields map - /// \param displayedFieldsVectorPatient Vector of patient field maps (name, value pairs) to which the found patient + /// Find patient by composite patient ID and return its index and insert it in the given fields map + /// \param displayedFieldsMapPatient Map of patient field maps (name, value pairs) to which the found patient /// is inserted on success. Also contains the generated patient index - /// \return Generated patient index that is an incremental UID for temporal and internal use (so that there is a single identifier for patients) - int getDisplayPatientFieldsIndex(QString patientsName, QString patientID, QVector > &displayedFieldsVectorPatient); + /// \return The composite patient ID if successfully gound, empty string otherwise + QString getDisplayPatientFieldsKey(const QString& patientID, const QString& patientsName, const QString& patientsBirthDate, + QMap >& displayedFieldsMapPatient); /// Find study by instance UID and insert it in the given fields map /// \param displayedFieldsMapStudy Map of study field maps (name, value pairs) to which the found study has been inserted on success @@ -135,7 +150,9 @@ class ctkDICOMDatabasePrivate void setNumberOfSeriesToStudyDisplayedFields(QMap > &displayedFieldsMapStudy); /// Calculate number of studies for each patient in the displayed fields container /// \param displayedFieldsVectorPatient (Internal_ID -> (DisplayField -> Value) ) - void setNumberOfStudiesToPatientDisplayedFields(QVector > &displayedFieldsVectorPatient); + void setNumberOfStudiesToPatientDisplayedFields(QMap >& displayedFieldsMapPatient); + + int rowCount(const QString& tableName); /// Name of the database file (i.e. for SQLITE the sqlite file) QString DatabaseFileName; @@ -147,14 +164,19 @@ class ctkDICOMDatabasePrivate ctkDICOMDisplayedFieldGenerator DisplayedFieldGenerator; - /// these are for optimizing the import of image sequences - /// since most information are identical for all slices - QString LastPatientID; - QString LastPatientsName; - QString LastPatientsBirthDate; - QString LastStudyInstanceUID; - QString LastSeriesInstanceUID; - int LastPatientUID; + /// These are for optimizing the import of image sequences + /// since most information are identical for all slices. + /// It would be very expensive to check in the database + /// presence of all these records on each slice insertion, + /// therefore we cache recently added entries in memory. + QMap InsertedPatientsCompositeIDCache; // map from composite patient ID to database ID + QSet InsertedStudyUIDsCache; + QSet InsertedSeriesUIDsCache; + + /// There is no unique patient ID. We use this composite ID in InsertedPatientsCompositeIDCache. + /// It is not a problem that is somewhat more strict than the criteria that is used to decide if a study should be insert + /// under the same patient. + QString compositePatientID(const QString& patientID, const QString& patientsName, const QString& patientsBirthDate); /// resets the variables to new inserts won't be fooled by leftover values void resetLastInsertedValues(); @@ -168,11 +190,13 @@ class ctkDICOMDatabasePrivate QString TagCacheDatabaseFilename; QStringList TagsToPrecache; bool openTagCacheDatabase(); - void precacheTags( const QString sopInstanceUID ); + void precacheTags(const ctkDICOMItem& dataset, const QString sopInstanceUID); - int insertPatient(const ctkDICOMItem& ctkDataset); - void insertStudy(const ctkDICOMItem& ctkDataset, int dbPatientID); - void insertSeries( const ctkDICOMItem& ctkDataset, QString studyInstanceUID); + // Return true if a new item is inserted + bool insertPatientStudySeries(const ctkDICOMItem& dataset, const QString& patientID, const QString& patientsName); + bool insertPatient(const ctkDICOMItem& dataset, int& databasePatientID); + bool insertStudy(const ctkDICOMItem& dataset, int dbPatientID); + bool insertSeries( const ctkDICOMItem& dataset, QString studyInstanceUID); }; //------------------------------------------------------------------------------ @@ -190,12 +214,9 @@ ctkDICOMDatabasePrivate::ctkDICOMDatabasePrivate(ctkDICOMDatabase& o): q_ptr(&o) //------------------------------------------------------------------------------ void ctkDICOMDatabasePrivate::resetLastInsertedValues() { - this->LastPatientID = QString(""); - this->LastPatientsName = QString(""); - this->LastPatientsBirthDate = QString(""); - this->LastStudyInstanceUID = QString(""); - this->LastSeriesInstanceUID = QString(""); - this->LastPatientUID = -1; + this->InsertedPatientsCompositeIDCache.clear(); + this->InsertedStudyUIDsCache.clear(); + this->InsertedSeriesUIDsCache.clear(); } //------------------------------------------------------------------------------ @@ -225,6 +246,25 @@ ctkDICOMDatabasePrivate::~ctkDICOMDatabasePrivate() { } +//------------------------------------------------------------------------------ +int ctkDICOMDatabasePrivate::rowCount(const QString& tableName) +{ + QSqlQuery numberOfItemsQuery(this->Database); + numberOfItemsQuery.prepare(QString("SELECT COUNT(*) FROM %1;").arg(tableName)); + int numberOfItems = 0; + if (numberOfItemsQuery.exec()) + { + numberOfItemsQuery.first(); + numberOfItems = numberOfItemsQuery.value(0).toInt(); + } + else + { + logger.error("SQLITE ERROR: " + numberOfItemsQuery.lastError().driverText()); + } + return numberOfItems; +} + + //------------------------------------------------------------------------------ bool ctkDICOMDatabasePrivate::loggedExec(QSqlQuery& query) { @@ -280,22 +320,6 @@ bool ctkDICOMDatabasePrivate::loggedExecBatch(QSqlQuery& query) return (success); } -//------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::beginTransaction() -{ - QSqlQuery transaction( this->Database ); - transaction.prepare( "BEGIN TRANSACTION" ); - transaction.exec(); -} - -//------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::endTransaction() -{ - QSqlQuery transaction( this->Database ); - transaction.prepare( "END TRANSACTION" ); - transaction.exec(); -} - //------------------------------------------------------------------------------ QStringList ctkDICOMDatabasePrivate::allFilesInDatabase() { @@ -367,64 +391,81 @@ QStringList ctkDICOMDatabasePrivate::filenames(QString table) } //------------------------------------------------------------------------------ -int ctkDICOMDatabasePrivate::insertPatient(const ctkDICOMItem& ctkDataset) +QString ctkDICOMDatabasePrivate::compositePatientID(const QString& patientID, const QString& patientsName, const QString& patientsBirthDate) { - int dbPatientID; + return QString("%1~%2~%3").arg(patientID).arg(patientsBirthDate).arg(patientsName); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMDatabasePrivate::insertPatient(const ctkDICOMItem& dataset, int& dbPatientID) +{ + dbPatientID = -1; // Check if patient is already present in the db - // TODO: maybe add birthdate check for extra safety - QString patientID(ctkDataset.GetElementAsString(DCM_PatientID) ); - QString patientsName(ctkDataset.GetElementAsString(DCM_PatientName) ); - QString patientsBirthDate(ctkDataset.GetElementAsString(DCM_PatientBirthDate) ); + + 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)); QSqlQuery checkPatientExistsQuery(this->Database); - checkPatientExistsQuery.prepare( "SELECT * FROM Patients WHERE PatientID = ? AND PatientsName = ?" ); - checkPatientExistsQuery.bindValue( 0, patientID ); - checkPatientExistsQuery.bindValue( 1, patientsName ); + checkPatientExistsQuery.prepare("SELECT * FROM Patients WHERE PatientID = ? AND PatientsName = ?"); + checkPatientExistsQuery.bindValue(0, patientID); + checkPatientExistsQuery.bindValue(1, patientsName); loggedExec(checkPatientExistsQuery); + QString compositeID = this->compositePatientID(patientID, patientsName, patientsBirthDate); if (checkPatientExistsQuery.next()) { // we found him dbPatientID = checkPatientExistsQuery.value(checkPatientExistsQuery.record().indexOf("UID")).toInt(); 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; + this->InsertedPatientsCompositeIDCache[compositeID] = dbPatientID; + return false; } else { // Insert it - QString patientsBirthTime(ctkDataset.GetElementAsString(DCM_PatientBirthTime) ); - QString patientsSex(ctkDataset.GetElementAsString(DCM_PatientSex) ); - QString patientsAge(ctkDataset.GetElementAsString(DCM_PatientAge) ); - QString patientComments(ctkDataset.GetElementAsString(DCM_PatientComments) ); + QString patientsBirthTime(dataset.GetElementAsString(DCM_PatientBirthTime)); + QString patientsSex(dataset.GetElementAsString(DCM_PatientSex)); + QString patientsAge(dataset.GetElementAsString(DCM_PatientAge)); + QString patientComments(dataset.GetElementAsString(DCM_PatientComments)); QSqlQuery insertPatientStatement(this->Database); - insertPatientStatement.prepare ( "INSERT INTO Patients " + insertPatientStatement.prepare("INSERT INTO Patients " "( 'UID', 'PatientsName', 'PatientID', 'PatientsBirthDate', 'PatientsBirthTime', 'PatientsSex', 'PatientsAge', 'PatientsComments', " - "'InsertTimestamp', 'DisplayedPatientsName', 'DisplayedNumberOfStudies', 'DisplayedFieldsUpdatedTimestamp' ) " - "VALUES ( NULL, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL )" ); - insertPatientStatement.bindValue( 0, patientsName ); - insertPatientStatement.bindValue( 1, patientID ); - insertPatientStatement.bindValue( 2, QDate::fromString ( patientsBirthDate, "yyyyMMdd" ) ); - insertPatientStatement.bindValue( 3, patientsBirthTime ); - insertPatientStatement.bindValue( 4, patientsSex ); + "'InsertTimestamp', 'DisplayedPatientsName', 'DisplayedNumberOfStudies', 'DisplayedFieldsUpdatedTimestamp' ) " + "VALUES ( NULL, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL )"); + insertPatientStatement.bindValue(0, patientsName); + insertPatientStatement.bindValue(1, patientID); + insertPatientStatement.bindValue(2, QDate::fromString(patientsBirthDate, "yyyyMMdd")); + insertPatientStatement.bindValue(3, patientsBirthTime); + insertPatientStatement.bindValue(4, patientsSex); // TODO: shift patient's age to study, // since this is not a patient level attribute in images // insertPatientStatement.bindValue( 5, patientsAge ); - insertPatientStatement.bindValue( 6, patientComments ); - insertPatientStatement.bindValue( 7, QDateTime::currentDateTime() ); + insertPatientStatement.bindValue(6, patientComments); + insertPatientStatement.bindValue(7, QDateTime::currentDateTime()); loggedExec(insertPatientStatement); dbPatientID = insertPatientStatement.lastInsertId().toInt(); - logger.debug( "New patient inserted: " + QString().setNum ( dbPatientID ) ); - qDebug() << "New patient inserted as : " << dbPatientID; + this->InsertedPatientsCompositeIDCache[compositeID] = dbPatientID; + logger.debug("New patient inserted: database item ID = " + QString().setNum(dbPatientID)); + return true; } - - return dbPatientID; } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& ctkDataset, int dbPatientID) +bool ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& dataset, int dbPatientID) { - QString studyInstanceUID(ctkDataset.GetElementAsString(DCM_StudyInstanceUID) ); + QString studyInstanceUID(dataset.GetElementAsString(DCM_StudyInstanceUID) ); QSqlQuery checkStudyExistsQuery(this->Database); checkStudyExistsQuery.prepare( "SELECT * FROM Studies WHERE StudyInstanceUID = ?" ); checkStudyExistsQuery.bindValue( 0, studyInstanceUID ); @@ -433,15 +474,15 @@ void ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& ctkDataset, int db { qDebug() << "Need to insert new study: " << studyInstanceUID; - QString studyID(ctkDataset.GetElementAsString(DCM_StudyID) ); - QString studyDate(ctkDataset.GetElementAsString(DCM_StudyDate) ); - QString studyTime(ctkDataset.GetElementAsString(DCM_StudyTime) ); - QString accessionNumber(ctkDataset.GetElementAsString(DCM_AccessionNumber) ); - QString modalitiesInStudy(ctkDataset.GetElementAsString(DCM_ModalitiesInStudy) ); - QString institutionName(ctkDataset.GetElementAsString(DCM_InstitutionName) ); - QString performingPhysiciansName(ctkDataset.GetElementAsString(DCM_PerformingPhysicianName) ); - QString referringPhysician(ctkDataset.GetElementAsString(DCM_ReferringPhysicianName) ); - QString studyDescription(ctkDataset.GetElementAsString(DCM_StudyDescription) ); + QString studyID(dataset.GetElementAsString(DCM_StudyID) ); + QString studyDate(dataset.GetElementAsString(DCM_StudyDate) ); + QString studyTime(dataset.GetElementAsString(DCM_StudyTime) ); + QString accessionNumber(dataset.GetElementAsString(DCM_AccessionNumber) ); + QString modalitiesInStudy(dataset.GetElementAsString(DCM_ModalitiesInStudy) ); + QString institutionName(dataset.GetElementAsString(DCM_InstitutionName) ); + QString performingPhysiciansName(dataset.GetElementAsString(DCM_PerformingPhysicianName) ); + QString referringPhysician(dataset.GetElementAsString(DCM_ReferringPhysicianName) ); + QString studyDescription(dataset.GetElementAsString(DCM_StudyDescription) ); QSqlQuery insertStudyStatement(this->Database); insertStudyStatement.prepare( "INSERT INTO Studies " @@ -466,19 +507,23 @@ void ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& ctkDataset, int db } else { - this->LastStudyInstanceUID = studyInstanceUID; + this->InsertedStudyUIDsCache.insert(studyInstanceUID); } + + return true; } else { qDebug() << "Used existing study: " << studyInstanceUID; + this->InsertedStudyUIDsCache.insert(studyInstanceUID); + return false; } } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::insertSeries(const ctkDICOMItem& ctkDataset, QString studyInstanceUID) +bool ctkDICOMDatabasePrivate::insertSeries(const ctkDICOMItem& dataset, QString studyInstanceUID) { - QString seriesInstanceUID(ctkDataset.GetElementAsString(DCM_SeriesInstanceUID) ); + QString seriesInstanceUID(dataset.GetElementAsString(DCM_SeriesInstanceUID) ); QSqlQuery checkSeriesExistsQuery(this->Database); checkSeriesExistsQuery.prepare( "SELECT * FROM Series WHERE SeriesInstanceUID = ?" ); checkSeriesExistsQuery.bindValue( 0, seriesInstanceUID ); @@ -491,18 +536,18 @@ void ctkDICOMDatabasePrivate::insertSeries(const ctkDICOMItem& ctkDataset, QStri { qDebug() << "Need to insert new series: " << seriesInstanceUID; - QString seriesDate(ctkDataset.GetElementAsString(DCM_SeriesDate) ); - QString seriesTime(ctkDataset.GetElementAsString(DCM_SeriesTime) ); - QString seriesDescription(ctkDataset.GetElementAsString(DCM_SeriesDescription) ); - QString modality(ctkDataset.GetElementAsString(DCM_Modality) ); - QString bodyPartExamined(ctkDataset.GetElementAsString(DCM_BodyPartExamined) ); - QString frameOfReferenceUID(ctkDataset.GetElementAsString(DCM_FrameOfReferenceUID) ); - QString contrastAgent(ctkDataset.GetElementAsString(DCM_ContrastBolusAgent) ); - QString scanningSequence(ctkDataset.GetElementAsString(DCM_ScanningSequence) ); - long seriesNumber(ctkDataset.GetElementAsInteger(DCM_SeriesNumber) ); - long acquisitionNumber(ctkDataset.GetElementAsInteger(DCM_AcquisitionNumber) ); - long echoNumber(ctkDataset.GetElementAsInteger(DCM_EchoNumbers) ); - long temporalPosition(ctkDataset.GetElementAsInteger(DCM_TemporalPositionIdentifier) ); + QString seriesDate(dataset.GetElementAsString(DCM_SeriesDate) ); + QString seriesTime(dataset.GetElementAsString(DCM_SeriesTime) ); + QString seriesDescription(dataset.GetElementAsString(DCM_SeriesDescription) ); + QString modality(dataset.GetElementAsString(DCM_Modality) ); + QString bodyPartExamined(dataset.GetElementAsString(DCM_BodyPartExamined) ); + QString frameOfReferenceUID(dataset.GetElementAsString(DCM_FrameOfReferenceUID) ); + QString contrastAgent(dataset.GetElementAsString(DCM_ContrastBolusAgent) ); + QString scanningSequence(dataset.GetElementAsString(DCM_ScanningSequence) ); + long seriesNumber(dataset.GetElementAsInteger(DCM_SeriesNumber) ); + long acquisitionNumber(dataset.GetElementAsInteger(DCM_AcquisitionNumber) ); + long echoNumber(dataset.GetElementAsInteger(DCM_EchoNumbers) ); + long temporalPosition(dataset.GetElementAsInteger(DCM_TemporalPositionIdentifier) ); QSqlQuery insertSeriesStatement(this->Database); insertSeriesStatement.prepare( "INSERT INTO Series " @@ -529,16 +574,19 @@ void ctkDICOMDatabasePrivate::insertSeries(const ctkDICOMItem& ctkDataset, QStri logger.error( "Error executing statement: " + insertSeriesStatement.lastQuery() + " Error: " + insertSeriesStatement.lastError().text() ); - this->LastSeriesInstanceUID = ""; } else { - this->LastSeriesInstanceUID = seriesInstanceUID; + this->InsertedSeriesUIDsCache.insert(seriesInstanceUID); } + + return true; } else { qDebug() << "Used existing series: " << seriesInstanceUID; + this->InsertedSeriesUIDsCache.insert(seriesInstanceUID); + return false; } } @@ -569,15 +617,10 @@ bool ctkDICOMDatabasePrivate::openTagCacheDatabase() } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::precacheTags( const QString sopInstanceUID ) +void ctkDICOMDatabasePrivate::precacheTags(const ctkDICOMItem& dataset, const QString sopInstanceUID) { Q_Q(ctkDICOMDatabase); - ctkDICOMItem dataset; - QString fileName = q->fileForInstance(sopInstanceUID); - dataset.InitializeFromFile(fileName); - - QStringList sopInstanceUIDs, tags, values; foreach (const QString &tag, this->TagsToPrecache) { @@ -590,276 +633,472 @@ void ctkDICOMDatabasePrivate::precacheTags( const QString sopInstanceUID ) values << value; } - QSqlQuery transaction( this->TagCacheDatabase ); - transaction.prepare( "BEGIN TRANSACTION" ); - transaction.exec(); - + this->TagCacheDatabase.transaction(); q->cacheTags(sopInstanceUIDs, tags, values); + this->TagCacheDatabase.commit(); +} - transaction = QSqlQuery( this->TagCacheDatabase ); - transaction.prepare( "END TRANSACTION" ); - transaction.exec(); +//------------------------------------------------------------------------------ +bool ctkDICOMDatabasePrivate::removeImage(const QString& sopInstanceUID) +{ + Q_Q(ctkDICOMDatabase); + QSqlQuery deleteFile(Database); + deleteFile.prepare("DELETE FROM Images WHERE SOPInstanceUID == :sopInstanceUID"); + deleteFile.bindValue(":sopInstanceUID", sopInstanceUID); + bool success = deleteFile.exec(); + if (!success) + { + logger.error("SQLITE ERROR deleting old image row: " + deleteFile.lastError().driverText()); + } + return success; } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& ctkDataset, const QString& filePath, bool storeFile, bool generateThumbnail) +bool ctkDICOMDatabasePrivate::storeDatasetFile(const ctkDICOMItem& dataset, const QString& originalFilePath, + const QString& studyInstanceUID, const QString& seriesInstanceUID, const QString& sopInstanceUID, + QString& storedFilePath) { Q_Q(ctkDICOMDatabase); - // this is the method that all other insert signatures end up calling - // after they have pre-parsed their arguments + if (sopInstanceUID.isEmpty()) + { + return false; + } - // Check to see if the file has already been loaded - // TODO: - // It could make sense to actually remove the dataset and re-add it. This needs the remove - // method we still have to write. + QString destinationDirectoryName = q->databaseDirectory() + "/dicom/"; + QDir destinationDir(destinationDirectoryName); + storedFilePath = destinationDirectoryName + + studyInstanceUID + "/" + + seriesInstanceUID + "/" + + sopInstanceUID; - QString sopInstanceUID ( ctkDataset.GetElementAsString(DCM_SOPInstanceUID) ); + destinationDir.mkpath(studyInstanceUID + "/" + + seriesInstanceUID); - QSqlQuery fileExistsQuery ( Database ); - fileExistsQuery.prepare("SELECT InsertTimestamp,Filename FROM Images WHERE SOPInstanceUID == :sopInstanceUID"); - fileExistsQuery.bindValue(":sopInstanceUID",sopInstanceUID); + if (originalFilePath.isEmpty()) { - bool success = fileExistsQuery.exec(); - if (!success) + if (this->LoggedExecVerbose) { - logger.error("SQLITE ERROR: " + fileExistsQuery.lastError().driverText()); - return; + logger.debug("Saving file: " + storedFilePath); } - bool found = fileExistsQuery.next(); + if (!dataset.SaveToFile(storedFilePath)) + { + logger.error("Error saving file: " + storedFilePath); + return false; + } + } + else + { + // we're inserting an existing file + QFile currentFile(originalFilePath); + currentFile.copy(storedFilePath); if (this->LoggedExecVerbose) { - qDebug() << "inserting filePath: " << filePath; + logger.debug("Copy file from: " + originalFilePath + " to: " + storedFilePath); } - if (!found) + } + + return true; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMDatabasePrivate::indexingStatusForFile(const QString& filePath, const QString& sopInstanceUID, + bool& datasetInDatabase, bool& datasetUpToDate, QString& databaseFilename) +{ + Q_Q(ctkDICOMDatabase); + datasetInDatabase = false; + datasetUpToDate = false; + + QSqlQuery fileExistsQuery(Database); + fileExistsQuery.prepare("SELECT InsertTimestamp,Filename FROM Images WHERE SOPInstanceUID == :sopInstanceUID"); + fileExistsQuery.bindValue(":sopInstanceUID", sopInstanceUID); + bool success = fileExistsQuery.exec(); + if (!success) + { + logger.error("SQLITE ERROR: " + fileExistsQuery.lastError().driverText()); + return false; + } + bool foundSOPInstanceUID = fileExistsQuery.next(); + if (!foundSOPInstanceUID) + { + // this data set is not in the database yet + return true; + } + + datasetInDatabase = true; + + // The SOP instance UID exists in the database. In theory, new SOP instance UID must be generated if + // a file is modified, but some software may not respect this, so check if the file was modified. + databaseFilename = fileExistsQuery.value(1).toString(); + QDateTime fileLastModified(QFileInfo(databaseFilename).lastModified()); + QDateTime databaseInsertTimestamp(QDateTime::fromString(fileExistsQuery.value(0).toString(), Qt::ISODate)); + // Compare QFileInfo objects instead of path strings to ensure equivalent file names + // (such as same file name in uppercase/lowercase on Windows) are considered as equal. + if (QFileInfo(databaseFilename) == QFileInfo(filePath) && fileLastModified < databaseInsertTimestamp) + { + // this file is already added and database is up-to-date + datasetUpToDate = true; + } + + return true; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMDatabasePrivate::insertPatientStudySeries(const ctkDICOMItem& dataset, + const QString& patientID, const QString& patientsName) +{ + Q_Q(ctkDICOMDatabase); + bool databaseWasChanged = false; + + // Insert new patient if needed + // Generate composite patient ID + QString patientsBirthDate(dataset.GetElementAsString(DCM_PatientBirthDate)); + QString compositePatientId = this->compositePatientID(patientID, patientsName, patientsBirthDate); + // The dbPatientID is a unique number within the database, generated by the sqlite autoincrement. + // The patientID is the (non-unique) DICOM patient id. + QMap::iterator dbPatientIDit = this->InsertedPatientsCompositeIDCache.find(compositePatientId); + int dbPatientID = -1; + if (dbPatientIDit != this->InsertedPatientsCompositeIDCache.end()) + { + // already in database + int dbPatientID = *dbPatientIDit; + } + else + { + if (this->LoggedExecVerbose) { - if (this->LoggedExecVerbose) - { - qDebug() << "database filename for " << sopInstanceUID << " is empty - we should insert on top of it"; - } + qDebug() << "Insert new patient if not already in database: " << patientID << " " << patientsName; } - else + 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; + } + + // Insert new study if needed + QString studyInstanceUID(dataset.GetElementAsString(DCM_StudyInstanceUID)); + if (!this->InsertedStudyUIDsCache.contains(studyInstanceUID)) + { + if (this->insertStudy(dataset, dbPatientID)) { - QString databaseFilename(fileExistsQuery.value(1).toString()); - QDateTime fileLastModified(QFileInfo(databaseFilename).lastModified()); - QDateTime databaseInsertTimestamp(QDateTime::fromString(fileExistsQuery.value(0).toString(),Qt::ISODate)); + qDebug() << "Study Added"; + databaseWasChanged = true; + // let users of this class track when things happen + emit q->studyAdded(studyInstanceUID); + } + } - if ( databaseFilename == filePath && fileLastModified < databaseInsertTimestamp ) - { - logger.debug ( "File " + databaseFilename + " already added" ); - return; - } - else - { - QSqlQuery deleteFile ( Database ); - deleteFile.prepare("DELETE FROM Images WHERE SOPInstanceUID == :sopInstanceUID"); - deleteFile.bindValue(":sopInstanceUID",sopInstanceUID); - bool success = deleteFile.exec(); - if (!success) - { - logger.error("SQLITE ERROR deleting old image row: " + deleteFile.lastError().driverText()); - return; - } - } + QString seriesInstanceUID(dataset.GetElementAsString(DCM_SeriesInstanceUID)); + if (!seriesInstanceUID.isEmpty() && !this->InsertedSeriesUIDsCache.contains(seriesInstanceUID)) + { + if (this->insertSeries(dataset, studyInstanceUID)) + { + qDebug() << "Series Added"; + databaseWasChanged = true; + emit q->seriesAdded(seriesInstanceUID); } } - //If the following fields can not be evaluated, cancel evaluation of the DICOM file - QString patientsName(ctkDataset.GetElementAsString(DCM_PatientName) ); - QString studyInstanceUID(ctkDataset.GetElementAsString(DCM_StudyInstanceUID) ); - QString seriesInstanceUID(ctkDataset.GetElementAsString(DCM_SeriesInstanceUID) ); - QString patientID(ctkDataset.GetElementAsString(DCM_PatientID) ); - if ( patientID.isEmpty() && !studyInstanceUID.isEmpty() ) - { // Use study instance uid as patient id if patient id is empty - can happen on anonymized datasets + return databaseWasChanged; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMDatabasePrivate::storeThumbnailFile(const QString& originalFilePath, + const QString& studyInstanceUID, const QString& seriesInstanceUID, const QString& sopInstanceUID) +{ + Q_Q(ctkDICOMDatabase); + if (!this->ThumbnailGenerator) + { + return false; + } + QString studySeriesDirectory = studyInstanceUID + "/" + seriesInstanceUID; + // Create thumbnail here + QString thumbnailPath = q->databaseDirectory() + + "/thumbs/" + studyInstanceUID + "/" + seriesInstanceUID + + "/" + sopInstanceUID + ".png"; + QFileInfo thumbnailInfo(thumbnailPath); + if (thumbnailInfo.exists() && (thumbnailInfo.lastModified() > QFileInfo(originalFilePath).lastModified())) + { + // thumbnail already exists and up-to-date + return true; + } + QDir(q->databaseDirectory() + "/thumbs/").mkpath(studySeriesDirectory); + DicomImage dcmImage(QDir::toNativeSeparators(originalFilePath).toLatin1()); + return this->ThumbnailGenerator->generateThumbnail(&dcmImage, thumbnailPath); +} + + +//------------------------------------------------------------------------------ +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); + studyInstanceUID = dataset.GetElementAsString(DCM_StudyInstanceUID); + seriesInstanceUID = dataset.GetElementAsString(DCM_SeriesInstanceUID); + return this->uidsForDataSet(patientsName, patientID, studyInstanceUID); +} + +//------------------------------------------------------------------------------ +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 // see: http://www.na-mic.org/Bug/view.php?id=2040 - logger.warn("Patient ID is empty, using studyInstanceUID as patient ID"); + logger.warn(QString("Patient ID is empty, using studyInstanceUID (%1) as patient ID").arg(studyInstanceUID)); patientID = studyInstanceUID; } - if ( patientsName.isEmpty() && !patientID.isEmpty() ) - { // Use patient id as name if name is empty - can happen on anonymized datasets + if (patientsName.isEmpty() && !patientID.isEmpty()) + { + // Use patient id as name if name is empty - can happen on anonymized datasets // see: http://www.na-mic.org/Bug/view.php?id=1643 patientsName = patientID; } - if ( patientsName.isEmpty() || studyInstanceUID.isEmpty() || patientID.isEmpty() ) + // We accept the dataset without seriesInstanceUID, as query/retrieve result may not contain it + if (patientsName.isEmpty() || studyInstanceUID.isEmpty() || patientID.isEmpty()) { - logger.error("Dataset is missing necessary information (patient name, study instance UID, or patient ID)!"); - return; + logger.error("Required information (patient name, patient ID, study instance UID) is missing from dataset"); + return false; } + // Valid data set + return true; +} - // store the file if the database is not in memory - // TODO: if we are called from insert(file) we - // have to do something else - // - QString filename = filePath; - if ( storeFile && !q->isInMemory() && !seriesInstanceUID.isEmpty() ) +//------------------------------------------------------------------------------ +void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& filePath, bool storeFile, bool generateThumbnail) +{ + Q_Q(ctkDICOMDatabase); + + // this is the method that all other insert signatures end up calling + // after they have pre-parsed their arguments + + QString sopInstanceUID(dataset.GetElementAsString(DCM_SOPInstanceUID)); + + // Check to see if the file has already been loaded + if (this->LoggedExecVerbose) { - // QString studySeriesDirectory = studyInstanceUID + "/" + seriesInstanceUID; - QString destinationDirectoryName = q->databaseDirectory() + "/dicom/"; - QDir destinationDir(destinationDirectoryName); - filename = destinationDirectoryName + - studyInstanceUID + "/" + - seriesInstanceUID + "/" + - sopInstanceUID; + qDebug() << "inserting filePath: " << filePath; + } - destinationDir.mkpath(studyInstanceUID + "/" + - seriesInstanceUID); + // 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) + { + logger.debug("File " + databaseFilename + " already added"); + return; + } + // File is updated, delete record and re-index + if (!this->removeImage(sopInstanceUID)) + { + logger.debug("File " + filePath + " cannot be added, failed to update existing values in the database"); + return; + } + } + + // Verify that minimum required fields are present + QString patientsName, patientID, studyInstanceUID, seriesInstanceUID; + if (!this->uidsForDataSet(dataset, patientsName, patientID, studyInstanceUID, seriesInstanceUID)) + { + // error occurred, message is already logged + return; + } - if (filePath.isEmpty()) + // Store a copy of the dataset + QString storedFilePath = filePath; + if (storeFile && !seriesInstanceUID.isEmpty() && !q->isInMemory()) + { + if (!this->storeDatasetFile(dataset, filePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID, storedFilePath)) { - if (this->LoggedExecVerbose) - { - logger.debug("Saving file: " + filename); - } + logger.error("Error saving file: " + filePath); + return; + } + } - if ( !ctkDataset.SaveToFile( filename) ) - { - logger.error("Error saving file: " + filename); - return; - } + bool databaseWasChanged = this->insertPatientStudySeries(dataset, patientID, patientsName); + + if (!storedFilePath.isEmpty() && !seriesInstanceUID.isEmpty()) + { + QSqlQuery checkImageExistsQuery(Database); + checkImageExistsQuery.prepare("SELECT * FROM Images WHERE Filename = ?"); + checkImageExistsQuery.addBindValue(storedFilePath); + checkImageExistsQuery.exec(); + if (this->LoggedExecVerbose) + { + qDebug() << "Maybe add Instance"; } - else + if (!checkImageExistsQuery.next()) { - // we're inserting an existing file - QFile currentFile( filePath ); - currentFile.copy(filename); + QSqlQuery insertImageStatement(Database); + insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )"); + insertImageStatement.addBindValue(sopInstanceUID); + insertImageStatement.addBindValue(storedFilePath); + insertImageStatement.addBindValue(seriesInstanceUID); + insertImageStatement.addBindValue(QDateTime::currentDateTime()); + insertImageStatement.exec(); + + // 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) { - logger.debug("Copy file from: " + filePath + " to: " + filename); + qDebug() << "Instance Added"; } + databaseWasChanged = true; + } + if (generateThumbnail) + { + this->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); } } + if (q->isInMemory() && databaseWasChanged) + { + emit q->databaseChanged(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::insert(const QList& indexingResults) +{ + Q_D(ctkDICOMDatabase); + bool databaseWasChanged = false; - //The dbPatientID is a unique number within the database, - //generated by the sqlite autoincrement - //The patientID is the (non-unique) DICOM patient id - int dbPatientID = LastPatientUID; + d->TagCacheDatabase.transaction(); + d->Database.transaction(); - if ( patientID != "" && patientsName != "" ) + foreach(const ctkDICOMDatabase::IndexingResult & indexingResult, indexingResults) { - //Speed up: Check if patient is the same as in last file; - // very probable, as all images belonging to a study have the same patient - QString patientsBirthDate(ctkDataset.GetElementAsString(DCM_PatientBirthDate) ); - if ( LastPatientID != patientID - || LastPatientsBirthDate != patientsBirthDate - || LastPatientsName != patientsName ) - { - if (this->LoggedExecVerbose) - { - qDebug() << "This looks like a different patient from last insert: " << patientID; - } - // Ok, something is different from last insert, let's insert him if he's not - // already in the db. + const ctkDICOMItem& dataset = *indexingResult.dataset.data(); + QString filePath = indexingResult.filePath; + bool generateThumbnail = false; + bool storeFile = indexingResult.copyFile; + //this->BackgroundIndexingDatabase->insert(indexingResult.filePath, *indexingResult.dataset.data(), indexingResult.storeFile, false); - dbPatientID = insertPatient( ctkDataset ); + // Check to see if the file has already been loaded + // TODO: + // It could make sense to actually remove the dataset and re-add it. This needs the remove + // method we still have to write. - // let users of this class track when things happen - emit q->patientAdded(dbPatientID, patientID, patientsName, patientsBirthDate); + QString sopInstanceUID(dataset.GetElementAsString(DCM_SOPInstanceUID)); - /// keep this for the next image - LastPatientUID = dbPatientID; - LastPatientID = patientID; - LastPatientsBirthDate = patientsBirthDate; - LastPatientsName = patientsName; + if (indexingResult.overwriteExistingDataset) + { + if (!d->removeImage(sopInstanceUID)) + { + logger.error("Failed to insert file into database (cannot update pre-existing item): " + filePath); + continue; + } } - if (this->LoggedExecVerbose) + // Verify that minimum required fields are present + QString patientsName, patientID, studyInstanceUID, seriesInstanceUID; + if (!d->uidsForDataSet(dataset, patientsName, patientID, studyInstanceUID, seriesInstanceUID)) { - qDebug() << "Going to insert this instance with dbPatientID: " << dbPatientID; + logger.error("Failed to insert file into database (required fields missing): " + filePath); + continue; } - // Patient is in now. Let's continue with the study - - if ( studyInstanceUID != "" && LastStudyInstanceUID != studyInstanceUID ) + // Store a copy of the dataset + QString storedFilePath = filePath; + if (storeFile && !seriesInstanceUID.isEmpty() && !this->isInMemory()) { - insertStudy(ctkDataset,dbPatientID); - - // let users of this class track when things happen - emit q->studyAdded(studyInstanceUID); - qDebug() << "Study Added"; + if (!d->storeDatasetFile(dataset, filePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID, storedFilePath)) + { + continue; + } } - - if ( seriesInstanceUID != "" && seriesInstanceUID != LastSeriesInstanceUID ) + if (d->insertPatientStudySeries(dataset, patientID, patientsName)) { - insertSeries(ctkDataset, studyInstanceUID); - - // let users of this class track when things happen - emit q->seriesAdded(seriesInstanceUID); - qDebug() << "Series Added"; + databaseWasChanged = true; } - // TODO: what to do with imported files - // - if ( !filename.isEmpty() && !seriesInstanceUID.isEmpty() ) + + if (!storedFilePath.isEmpty() && !seriesInstanceUID.isEmpty()) { - QSqlQuery checkImageExistsQuery (Database); - checkImageExistsQuery.prepare( "SELECT * FROM Images WHERE Filename = ?" ); - checkImageExistsQuery.addBindValue( filename ); - checkImageExistsQuery.exec(); - if (this->LoggedExecVerbose) + // 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) { - qDebug() << "Maybe add Instance"; - } - if (!checkImageExistsQuery.next()) - { - QSqlQuery insertImageStatement ( Database ); - insertImageStatement.prepare( "INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )" ); - insertImageStatement.addBindValue( sopInstanceUID ); - insertImageStatement.addBindValue( filename ); - insertImageStatement.addBindValue( seriesInstanceUID ); - insertImageStatement.addBindValue( QDateTime::currentDateTime() ); - insertImageStatement.exec(); - - // insert was needed, so cache any application-requested tags - this->precacheTags(sopInstanceUID); - - // let users of this class track when things happen - emit q->instanceAdded(sopInstanceUID); - if (this->LoggedExecVerbose) + unsigned short group, element; + this->tagToGroupElement(tag, group, element); + DcmTagKey tagKey(group, element); + QString value = dataset.GetAllElementValuesAsString(tagKey); + insertTags.bindValue(1, tag); + if (value.isEmpty()) + { + insertTags.bindValue(2, TagNotInInstance); + } + else { - qDebug() << "Instance Added"; + insertTags.bindValue(2, value); } + insertTags.exec(); } - } - if ( generateThumbnail && ThumbnailGenerator && !seriesInstanceUID.isEmpty() ) - { - QString studySeriesDirectory = studyInstanceUID + "/" + seriesInstanceUID; - //Create thumbnail here - QString thumbnailPath = q->databaseDirectory() + - "/thumbs/" + studyInstanceUID + "/" + seriesInstanceUID - + "/" + sopInstanceUID + ".png"; - QFileInfo thumbnailInfo(thumbnailPath); - if ( !(thumbnailInfo.exists() && (thumbnailInfo.lastModified() > QFileInfo(filename).lastModified())) ) + // Insert image files + QSqlQuery insertImageStatement(d->Database); + insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )"); + insertImageStatement.addBindValue(sopInstanceUID); + insertImageStatement.addBindValue(storedFilePath); + insertImageStatement.addBindValue(seriesInstanceUID); + insertImageStatement.addBindValue(QDateTime::currentDateTime()); + insertImageStatement.exec(); + emit instanceAdded(sopInstanceUID); + if (d->LoggedExecVerbose) { - QDir(q->databaseDirectory() + "/thumbs/").mkpath(studySeriesDirectory); - DicomImage dcmImage(QDir::toNativeSeparators(filename).toLatin1()); - ThumbnailGenerator->generateThumbnail(&dcmImage, thumbnailPath); + qDebug() << "Instance Added"; + databaseWasChanged = true; } - } - if (q->isInMemory()) - { - emit q->databaseChanged(); + if (generateThumbnail) + { + d->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); + } } } - else + + d->Database.commit(); + d->TagCacheDatabase.commit(); + + if (databaseWasChanged && this->isInMemory()) { - qDebug() << "No patient name or no patient id - not inserting!"; + emit this->databaseChanged(); } } + //------------------------------------------------------------------------------ -int ctkDICOMDatabasePrivate::getDisplayPatientFieldsIndex(QString patientsName, QString patientID, QVector > &displayedFieldsVectorPatient) +QString ctkDICOMDatabasePrivate::getDisplayPatientFieldsKey(const QString& patientID, + const QString& patientsName, const QString& patientsBirthDate, + QMap >& displayedFieldsMapPatient) { + QString compositeID = this->compositePatientID(patientID, patientsName, patientsBirthDate); + // Look for the patient in the displayed fields cache first - for (int patientIndex=0; patientIndex < displayedFieldsVectorPatient.size(); ++patientIndex) + if (displayedFieldsMapPatient.find(compositeID) != displayedFieldsMapPatient.end()) { - QMap currentPatient = displayedFieldsVectorPatient[patientIndex]; - if ( !currentPatient["PatientID"].compare(patientID) - && !currentPatient["PatientsName"].compare(patientsName) ) - { - return patientIndex; - } + return compositeID; } // Look for the patient in the display database @@ -870,7 +1109,7 @@ int ctkDICOMDatabasePrivate::getDisplayPatientFieldsIndex(QString patientsName, if (!displayPatientsQuery.exec()) { logger.error("SQLITE ERROR: " + displayPatientsQuery.lastError().driverText()); - return -1; + return QString(); } if (displayPatientsQuery.size() > 1) { @@ -884,15 +1123,12 @@ int ctkDICOMDatabasePrivate::getDisplayPatientFieldsIndex(QString patientsName, { patientFieldsMap.insert(patientRecord.fieldName(fieldIndex), patientRecord.value(fieldIndex).toString()); } - // The index of the patient represents its UID for this update of the display tables - int patientIndex = displayedFieldsVectorPatient.size(); - patientFieldsMap["PatientIndex"] = patientIndex; // Index as a single UID for internal temporal use - displayedFieldsVectorPatient.append(patientFieldsMap); - return patientIndex; + displayedFieldsMapPatient[compositeID] = patientFieldsMap; + return compositeID; } logger.error("Failed to find patient with PatientsName=" + patientsName + " and PatientID=" + patientID); - return -1; + return QString(); } //------------------------------------------------------------------------------ @@ -980,14 +1216,15 @@ QString ctkDICOMDatabasePrivate::getDisplaySeriesFieldsKey(QString seriesInstanc //------------------------------------------------------------------------------ bool ctkDICOMDatabasePrivate::applyDisplayedFieldsChanges( QMap > &displayedFieldsMapSeries, QMap > &displayedFieldsMapStudy, - QVector > &displayedFieldsVectorPatient ) + QMap > &displayedFieldsMapPatient) { - QMap patientIndexToPatientUidMap; + QMap patientCompositeIdToPatientUidMap; // Update patient fields - for (int patientIndex=0; patientIndex < displayedFieldsVectorPatient.size(); ++patientIndex) + + foreach(QString compositeID, displayedFieldsMapPatient.keys()) { - QMap currentPatient = displayedFieldsVectorPatient[patientIndex]; + QMap currentPatient = displayedFieldsMapPatient[compositeID]; QSqlQuery displayPatientsQuery(this->Database); displayPatientsQuery.prepare( "SELECT * FROM Patients WHERE PatientID=:patientID AND PatientsName=:patientsName ;" ); displayPatientsQuery.bindValue(":patientID", currentPatient["PatientID"]); @@ -1003,7 +1240,7 @@ bool ctkDICOMDatabasePrivate::applyDisplayedFieldsChanges( QMap boundValues; foreach (QString tagName, currentPatient.keys()) { - if (tagName == "PatientIndex") + if (tagName == "PatientCompositeID") { continue; // Do not write patient index that is only used internally and temporarily } @@ -1031,7 +1268,7 @@ bool ctkDICOMDatabasePrivate::applyDisplayedFieldsChanges( QMaploggedExec(updateDisplayedFieldsUpdatedTimestampStatement); - patientIndexToPatientUidMap[patientIndex] = patientUID; + patientCompositeIdToPatientUidMap[compositeID] = patientUID; } else { @@ -1058,10 +1295,10 @@ bool ctkDICOMDatabasePrivate::applyDisplayedFieldsChanges( QMap boundValues; foreach (QString tagName, currentStudy.keys()) { - if (!tagName.compare("PatientIndex")) + if (!tagName.compare("PatientCompositeID")) { displayStudiesFieldUpdateString.append( "PatientsUID = ? , " ); - boundValues << QString::number(patientIndexToPatientUidMap[currentStudy["PatientIndex"].toInt()]); + boundValues << QString::number(patientCompositeIdToPatientUidMap[currentStudy["PatientCompositeID"]]); } else { @@ -1147,9 +1384,8 @@ void ctkDICOMDatabasePrivate::setCountToSeriesDisplayedFields(QMapTagCacheDatabase); - countQuery.prepare("SELECT COUNT(*) FROM TagCache WHERE Tag = ? AND Value = ? ;"); - countQuery.addBindValue(ctkDICOMItem::TagKeyStripped(DCM_SeriesInstanceUID)); + QSqlQuery countQuery(this->Database); + countQuery.prepare("SELECT COUNT(*) FROM Images WHERE SeriesInstanceUID = ? ;"); countQuery.addBindValue(currentSeriesInstanceUid); if (!countQuery.exec()) { @@ -1190,11 +1426,11 @@ void ctkDICOMDatabasePrivate::setNumberOfSeriesToStudyDisplayedFields(QMap > &displayedFieldsVectorPatient) +void ctkDICOMDatabasePrivate::setNumberOfStudiesToPatientDisplayedFields(QMap >& displayedFieldsMapPatient) { - for (int patientIndex=0; patientIndex displayedFieldsForCurrentPatient = displayedFieldsVectorPatient[patientIndex]; + QMap displayedFieldsForCurrentPatient = displayedFieldsMapPatient[compositeID]; int patientUID = displayedFieldsForCurrentPatient["UID"].toInt(); QSqlQuery numberOfStudiesQuery(this->Database); numberOfStudiesQuery.prepare("SELECT COUNT(*) FROM Studies WHERE PatientsUID = ? ;"); @@ -1209,11 +1445,10 @@ void ctkDICOMDatabasePrivate::setNumberOfStudiesToPatientDisplayedFields(QVector int currentNumberOfStudies = numberOfStudiesQuery.value(0).toInt(); displayedFieldsForCurrentPatient["DisplayedNumberOfStudies"] = QString::number(currentNumberOfStudies); - displayedFieldsVectorPatient[patientIndex] = displayedFieldsForCurrentPatient; + displayedFieldsMapPatient[compositeID] = displayedFieldsForCurrentPatient; } } - //------------------------------------------------------------------------------ // ctkDICOMDatabase methods //------------------------------------------------------------------------------ @@ -1244,6 +1479,7 @@ ctkDICOMDatabase::~ctkDICOMDatabase() void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& connectionName ) { Q_D(ctkDICOMDatabase); + bool wasOpen = this->isOpen(); d->DatabaseFileName = databaseFile; QString verifiedConnectionName = connectionName; if (verifiedConnectionName.isEmpty()) @@ -1255,13 +1491,27 @@ void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& c if ( ! (d->Database.open()) ) { d->LastError = d->Database.lastError().text(); + if (wasOpen) + { + emit closed(); + } return; } + + // Disable synchronous writing to make modifications faster + QSqlQuery pragmaSyncQuery(d->Database); + pragmaSyncQuery.exec("PRAGMA synchronous = OFF"); + pragmaSyncQuery.finish(); + if ( d->Database.tables().empty() ) { if (!this->initializeDatabase()) { d->LastError = QString("Unable to initialize DICOM database!"); + if (wasOpen) + { + emit closed(); + } return; } } @@ -1273,11 +1523,6 @@ void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& c connect(watcher, SIGNAL(fileChanged(QString)),this, SIGNAL (databaseChanged()) ); } - // Disable synchronous writing to make modifications faster - QSqlQuery pragmaSyncQuery(d->Database); - pragmaSyncQuery.exec("PRAGMA synchronous = OFF"); - pragmaSyncQuery.finish(); - // Set up the tag cache for use later QFileInfo fileInfo(d->DatabaseFileName); d->TagCacheDatabaseFilename = QString( fileInfo.dir().path() + "/ctkDICOMTagCache.sql" ); @@ -1287,7 +1532,14 @@ void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& c this->initializeTagCache(); } - this->setTagsToPrecache(d->DisplayedFieldGenerator.getRequiredTags()); + // Add displayed field generator's required tags to the pre-cached list to make + // displayed field updates fast. + QStringList tags = this->tagsToPrecache(); + tags << d->DisplayedFieldGenerator.getRequiredTags(); + tags.removeDuplicates(); + this->setTagsToPrecache(tags); + + emit opened(); } //------------------------------------------------------------------------------ @@ -1342,6 +1594,8 @@ bool ctkDICOMDatabase::initializeDatabase(const char* sqlFileName/* = ":/dicom/d QSqlQuery dropSchemaInfo(d->Database); d->loggedExec( dropSchemaInfo, QString("DROP TABLE IF EXISTS 'SchemaInfo';") ); return d->executeScript(sqlFileName); + + emit databaseChanged(); } //------------------------------------------------------------------------------ @@ -1461,8 +1715,13 @@ bool ctkDICOMDatabase::updateSchema( void ctkDICOMDatabase::closeDatabase() { Q_D(ctkDICOMDatabase); + bool wasOpen = this->isOpen(); d->Database.close(); d->TagCacheDatabase.close(); + if (wasOpen) + { + emit closed(); + } } // @@ -1737,6 +1996,33 @@ QDateTime ctkDICOMDatabase::insertDateTimeForInstance(QString sopInstanceUID) return( result ); } +//------------------------------------------------------------------------------ +int ctkDICOMDatabase::patientsCount() +{ + Q_D(ctkDICOMDatabase); + return d->rowCount("Patients"); +} + +//------------------------------------------------------------------------------ +int ctkDICOMDatabase::studiesCount() +{ + Q_D(ctkDICOMDatabase); + return d->rowCount("Studies"); +} + +//------------------------------------------------------------------------------ +int ctkDICOMDatabase::seriesCount() +{ + Q_D(ctkDICOMDatabase); + return d->rowCount("Series"); +} + +//------------------------------------------------------------------------------ +int ctkDICOMDatabase::imagesCount() +{ + Q_D(ctkDICOMDatabase); + return d->rowCount("Images"); +} // // instance header methods @@ -1957,16 +2243,24 @@ void ctkDICOMDatabase::insert( DcmItem *item, bool storeFile, bool generateThumb { return; } - ctkDICOMItem ctkDataset; - ctkDataset.InitializeFromItem(item, false /* do not take ownership */); - this->insert(ctkDataset,storeFile,generateThumbnail); + ctkDICOMItem dataset; + dataset.InitializeFromItem(item, false /* do not take ownership */); + this->insert(dataset,storeFile,generateThumbnail); +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::insert( const ctkDICOMItem& dataset, bool storeFile, bool generateThumbnail) +{ + Q_D(ctkDICOMDatabase); + d->insert(dataset, QString(), storeFile, generateThumbnail); } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::insert( const ctkDICOMItem& ctkDataset, bool storeFile, bool generateThumbnail) +void ctkDICOMDatabase::insert(const QString& filePath, const ctkDICOMItem& dataset, + bool storeFile, bool generateThumbnail) { Q_D(ctkDICOMDatabase); - d->insert(ctkDataset, QString(), storeFile, generateThumbnail); + d->insert(dataset, filePath, storeFile, generateThumbnail); } //------------------------------------------------------------------------------ @@ -1988,12 +2282,12 @@ void ctkDICOMDatabase::insert( const QString& filePath, bool storeFile, bool gen logger.debug( "Processing " + filePath ); } - ctkDICOMItem ctkDataset; + ctkDICOMItem dataset; - ctkDataset.InitializeFromFile(filePath); - if ( ctkDataset.IsInitialized() ) + dataset.InitializeFromFile(filePath); + if ( dataset.IsInitialized() ) { - d->insert( ctkDataset, filePath, storeFile, generateThumbnail ); + d->insert( dataset, filePath, storeFile, generateThumbnail ); } else { @@ -2002,10 +2296,15 @@ void ctkDICOMDatabase::insert( const QString& filePath, bool storeFile, bool gen } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::setTagsToPrecache( const QStringList tags) +void ctkDICOMDatabase::setTagsToPrecache(const QStringList tags) { Q_D(ctkDICOMDatabase); + if (d->TagsToPrecache == tags) + { + return; + } d->TagsToPrecache = tags; + emit tagsToPrecacheChanged(); } //------------------------------------------------------------------------------ @@ -2034,6 +2333,26 @@ bool ctkDICOMDatabase::fileExistsAndUpToDate(const QString& filePath) return result; } +//------------------------------------------------------------------------------ +bool ctkDICOMDatabase::allFilesModifiedTimes(QMap& modifiedTimeForFilepath) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery allFilesModifiedQuery(database()); + allFilesModifiedQuery.prepare("SELECT Filename, InsertTimestamp FROM Images;"); + bool success = d->loggedExec(allFilesModifiedQuery); + while (allFilesModifiedQuery.next()) + { + QString filename = allFilesModifiedQuery.value(0).toString(); + QDateTime modifiedTime = QDateTime::fromString(allFilesModifiedQuery.value(1).toString(), Qt::ISODate); + if (modifiedTimeForFilepath.contains(filename) && modifiedTimeForFilepath[filename] <= modifiedTime) + { + continue; + } + modifiedTimeForFilepath[filename] = modifiedTime; + } + allFilesModifiedQuery.finish(); + return success; +} //------------------------------------------------------------------------------ bool ctkDICOMDatabase::isOpen() const @@ -2050,14 +2369,14 @@ bool ctkDICOMDatabase::isInMemory() const } //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) +bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clearCachedTags/*=true*/) { Q_D(ctkDICOMDatabase); // get all images from series - QSqlQuery fileExistsQuery ( d->Database ); + QSqlQuery fileExistsQuery(d->Database); fileExistsQuery.prepare("SELECT Filename, SOPInstanceUID, StudyInstanceUID FROM Images,Series WHERE Series.SeriesInstanceUID = Images.SeriesInstanceUID AND Images.SeriesInstanceUID = :seriesID"); - fileExistsQuery.bindValue(":seriesID",seriesInstanceUID); + fileExistsQuery.bindValue(":seriesID", seriesInstanceUID); bool success = fileExistsQuery.exec(); if (!success) { @@ -2065,19 +2384,24 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) return false; } - QList< QPair > removeList; - while ( fileExistsQuery.next() ) + QList< QPair > removeList; + QStringList removeTagCacheSOPInstanceUIDs; + while (fileExistsQuery.next()) { QString dbFilePath = fileExistsQuery.value(fileExistsQuery.record().indexOf("Filename")).toString(); QString sopInstanceUID = fileExistsQuery.value(fileExistsQuery.record().indexOf("SOPInstanceUID")).toString(); QString studyInstanceUID = fileExistsQuery.value(fileExistsQuery.record().indexOf("StudyInstanceUID")).toString(); QString internalFilePath = studyInstanceUID + "/" + seriesInstanceUID + "/" + sopInstanceUID; - removeList << qMakePair(dbFilePath,internalFilePath); + removeList << qMakePair(dbFilePath, internalFilePath); + if (clearCachedTags) + { + removeTagCacheSOPInstanceUIDs << sopInstanceUID; + } } - QSqlQuery fileRemove ( d->Database ); + QSqlQuery fileRemove(d->Database); fileRemove.prepare("DELETE FROM Images WHERE SeriesInstanceUID == :seriesID"); - fileRemove.bindValue(":seriesID",seriesInstanceUID); + fileRemove.bindValue(":seriesID", seriesInstanceUID); logger.debug("SQLITE: removing seriesInstanceUID " + seriesInstanceUID); success = fileRemove.exec(); if (!success) @@ -2086,7 +2410,19 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) logger.error("SQLITE ERROR: " + fileRemove.lastError().driverText()); } + if (!removeTagCacheSOPInstanceUIDs.isEmpty()) + { + d->TagCacheDatabase.transaction(); + // Remove values from tag cache (may be important for patient confidentiality) + foreach(QString sopInstanceUID, removeTagCacheSOPInstanceUIDs) + { + removeCachedTags(sopInstanceUID); + } + d->TagCacheDatabase.commit(); + } + QPair fileToRemove; + QStringList foldersToRemove; foreach (fileToRemove, removeList) { QString dbFilePath = fileToRemove.first; @@ -2097,7 +2433,7 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) { if (!dbFilePath.endsWith(fileToRemove.second)) { - logger.error("Database inconsistency detected during delete!"); + logger.error("Database inconsistency detected during delete (stored file found ouside database folder)"); continue; } if (QFile( dbFilePath ).remove()) @@ -2106,6 +2442,11 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) { logger.debug("Removed file " + dbFilePath ); } + QString fileFolder = QFileInfo(dbFilePath).absoluteDir().path(); + if (foldersToRemove.isEmpty() || foldersToRemove.last() != fileFolder) + { + foldersToRemove << fileFolder; + } } else { @@ -2120,9 +2461,20 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) { logger.warn("Failed to remove thumbnail " + thumbnailToRemove); } + QString fileFolder = QFileInfo(thumbnailFile).absoluteDir().path(); + if (foldersToRemove.isEmpty() || foldersToRemove.last() != fileFolder) + { + foldersToRemove << fileFolder; + } } } + // Delete all empty folders that are left after removing DICOM files + foreach (QString folderToRemove, foldersToRemove) + { + QDir().rmpath(folderToRemove); + } + this->cleanup(); d->resetLastInsertedValues(); @@ -2131,14 +2483,19 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) } //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::cleanup() +bool ctkDICOMDatabase::cleanup(bool vacuum/*=false*/) { Q_D(ctkDICOMDatabase); QSqlQuery seriesCleanup ( d->Database ); seriesCleanup.exec("DELETE FROM Series WHERE ( SELECT COUNT(*) FROM Images WHERE Images.SeriesInstanceUID = Series.SeriesInstanceUID ) = 0;"); seriesCleanup.exec("DELETE FROM Studies WHERE ( SELECT COUNT(*) FROM Series WHERE Series.StudyInstanceUID = Studies.StudyInstanceUID ) = 0;"); seriesCleanup.exec("DELETE FROM Patients WHERE ( SELECT COUNT(*) FROM Studies WHERE Studies.PatientsUID = Patients.UID ) = 0;"); - + if (vacuum) + { + seriesCleanup.exec("VACUUM;"); + QSqlQuery tagcacheCleanup(d->TagCacheDatabase); + seriesCleanup.exec("VACUUM;"); + } return true; } @@ -2335,6 +2692,13 @@ bool ctkDICOMDatabase::cacheTag(const QString sopInstanceUID, const QString tag, bool ctkDICOMDatabase::cacheTags(const QStringList sopInstanceUIDs, const QStringList tags, QStringList values) { Q_D(ctkDICOMDatabase); + int itemCount = sopInstanceUIDs.size(); + if (tags.size() != itemCount || values.size() != itemCount) + { + logger.error("Failed to cache tags: number of inputs do not match"); + return false; + } + if ( !this->tagCacheExists() ) { if ( !this->initializeTagCache() ) @@ -2343,22 +2707,58 @@ bool ctkDICOMDatabase::cacheTags(const QStringList sopInstanceUIDs, const QStrin } } - // replace empty strings with special flag string - QStringList::iterator i; - for (i = values.begin(); i != values.end(); ++i) + d->TagCacheDatabase.transaction(); + + QSqlQuery insertTags(d->TagCacheDatabase); + insertTags.prepare( "INSERT OR REPLACE INTO TagCache VALUES(?,?,?)" ); + + QStringList::const_iterator sopInstanceUIDsIt = sopInstanceUIDs.begin(); + QStringList::const_iterator tagsIt = tags.begin(); + QStringList::const_iterator valuesIt = values.begin(); + bool success = true; + for (int i = 0; iisEmpty()) { - *i = TagNotInInstance; + // replace empty strings with special flag string + insertTags.bindValue(2, TagNotInInstance); } + else + { + insertTags.bindValue(2, *valuesIt); + } + if (!insertTags.exec()) + { + success = false; + } + ++sopInstanceUIDsIt; + ++tagsIt; + ++valuesIt; } - QSqlQuery insertTags( d->TagCacheDatabase ); - insertTags.prepare( "INSERT OR REPLACE INTO TagCache VALUES(?,?,?)" ); - insertTags.addBindValue(sopInstanceUIDs); - insertTags.addBindValue(tags); - insertTags.addBindValue(values); - return d->loggedExecBatch(insertTags); + d->TagCacheDatabase.commit(); + + return success; +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::removeCachedTags(const QString sopInstanceUID) +{ + Q_D(ctkDICOMDatabase); + if (!this->tagCacheExists()) + { + return; + } + QSqlQuery deleteFile(d->TagCacheDatabase); + deleteFile.prepare("DELETE FROM TagCache WHERE SOPInstanceUID == :sopInstanceUID"); + deleteFile.bindValue(":sopInstanceUID", sopInstanceUID); + bool success = deleteFile.exec(); + if (!success) + { + logger.error("SQLITE ERROR deleting tag cache row: " + deleteFile.lastError().driverText()); + } } //------------------------------------------------------------------------------ @@ -2374,7 +2774,7 @@ void ctkDICOMDatabase::updateDisplayedFields() // Populate displayed fields maps from the current display tables QMap > displayedFieldsMapSeries; QMap > displayedFieldsMapStudy; - QVector > displayedFieldsVectorPatient; // The index in the vector is the internal patient UID + QMap > displayedFieldsMapPatient; d->DisplayedFieldGenerator.setDatabase(this); @@ -2390,16 +2790,23 @@ void ctkDICOMDatabase::updateDisplayedFields() this->getCachedTags(sopInstanceUID, cachedTags); // Patient - int displayedFieldsIndexForCurrentPatient = d->getDisplayPatientFieldsIndex( - cachedTags[ctkDICOMItem::TagKeyStripped(DCM_PatientName)], - cachedTags[ctkDICOMItem::TagKeyStripped(DCM_PatientID)], - displayedFieldsVectorPatient ); - if (displayedFieldsIndexForCurrentPatient < 0) + QString patientsName = cachedTags[ctkDICOMItem::TagKeyStripped(DCM_PatientName)]; + QString patientID = cachedTags[ctkDICOMItem::TagKeyStripped(DCM_PatientID)]; + QString studyInstanceUID = cachedTags[ctkDICOMItem::TagKeyStripped(DCM_StudyInstanceUID)]; + if (!d->uidsForDataSet(patientsName, patientID, studyInstanceUID)) + { + // error occurred, message is already logged + continue; + } + QString patientsBirthDate = cachedTags[ctkDICOMItem::TagKeyStripped(DCM_PatientBirthDate)]; + + QString compositeId = d->getDisplayPatientFieldsKey(patientID, patientsName, patientsBirthDate, displayedFieldsMapPatient); + if (compositeId.isEmpty()) { logger.error("Failed to find patient for SOP Instance UID = " + sopInstanceUID); continue; } - QMap displayedFieldsForCurrentPatient = displayedFieldsVectorPatient[ displayedFieldsIndexForCurrentPatient ]; + QMap displayedFieldsForCurrentPatient = displayedFieldsMapPatient[compositeId]; // Study QString displayedFieldsKeyForCurrentStudy = d->getDisplayStudyFieldsKey( @@ -2411,7 +2818,7 @@ void ctkDICOMDatabase::updateDisplayedFields() continue; } QMap displayedFieldsForCurrentStudy = displayedFieldsMapStudy[ displayedFieldsKeyForCurrentStudy ]; - displayedFieldsForCurrentStudy["PatientIndex"] = QString::number(displayedFieldsIndexForCurrentPatient); + displayedFieldsForCurrentStudy["PatientCompositeID"] = compositeId; // Series QString displayedFieldsKeyForCurrentSeries = d->getDisplaySeriesFieldsKey( @@ -2425,13 +2832,13 @@ void ctkDICOMDatabase::updateDisplayedFields() QMap displayedFieldsForCurrentSeries = displayedFieldsMapSeries[ displayedFieldsKeyForCurrentSeries ]; // Do the update of the displayed fields using the roles - d->DisplayedFieldGenerator.updateDisplayedFieldsForInstance(sopInstanceUID, + d->DisplayedFieldGenerator.updateDisplayedFieldsForInstance(sopInstanceUID, cachedTags, displayedFieldsForCurrentSeries, displayedFieldsForCurrentStudy, displayedFieldsForCurrentPatient); // Set updated fields to the series / study / patient displayed fields maps displayedFieldsMapSeries[ displayedFieldsKeyForCurrentSeries ] = displayedFieldsForCurrentSeries; displayedFieldsMapStudy[ displayedFieldsKeyForCurrentStudy ] = displayedFieldsForCurrentStudy; - displayedFieldsVectorPatient[ displayedFieldsIndexForCurrentPatient ] = displayedFieldsForCurrentPatient; + displayedFieldsMapPatient[ compositeId ] = displayedFieldsForCurrentPatient; } // For each instance emit displayedFieldsUpdateProgress(++progressValue); @@ -2444,18 +2851,16 @@ void ctkDICOMDatabase::updateDisplayedFields() // Calculate number of series in each updated study d->setNumberOfSeriesToStudyDisplayedFields(displayedFieldsMapStudy); // Calculate number of studies in each updated patient - d->setNumberOfStudiesToPatientDisplayedFields(displayedFieldsVectorPatient); + d->setNumberOfStudiesToPatientDisplayedFields(displayedFieldsMapPatient); emit displayedFieldsUpdateProgress(++progressValue); // Update/insert the display values if (displayedFieldsMapSeries.count() > 0) { - QSqlQuery transaction(d->Database); - transaction.prepare("BEGIN TRANSACTION"); - transaction.exec(); + d->Database.transaction(); - if (d->applyDisplayedFieldsChanges(displayedFieldsMapSeries, displayedFieldsMapStudy, displayedFieldsVectorPatient)) + if (d->applyDisplayedFieldsChanges(displayedFieldsMapSeries, displayedFieldsMapStudy, displayedFieldsMapPatient)) { // Update image timestamp newFilesQuery.first(); @@ -2469,9 +2874,7 @@ void ctkDICOMDatabase::updateDisplayedFields() } } - transaction = QSqlQuery(d->Database); - transaction.prepare("END TRANSACTION"); - transaction.exec(); + d->Database.commit(); } emit displayedFieldsUpdated(); diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.h b/Libs/DICOM/Core/ctkDICOMDatabase.h index 325d8e40dc..b0cc9fac1d 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase.h @@ -61,6 +61,14 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject Q_PROPERTY(QStringList tagsToPrecache READ tagsToPrecache WRITE setTagsToPrecache) public: + struct IndexingResult + { + QString filePath; + QSharedPointer dataset; + bool copyFile; + bool overwriteExistingDataset; + }; + explicit ctkDICOMDatabase(QObject *parent = 0); explicit ctkDICOMDatabase(QString databaseFile); virtual ~ctkDICOMDatabase(); @@ -159,7 +167,15 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject Q_INVOKABLE QString instanceForFile (const QString fileName); Q_INVOKABLE QDateTime insertDateTimeForInstance (const QString fileName); + Q_INVOKABLE int patientsCount(); + Q_INVOKABLE int studiesCount(); + Q_INVOKABLE int seriesCount(); + Q_INVOKABLE int imagesCount(); + Q_INVOKABLE QStringList allFiles (); + + bool allFilesModifiedTimes(QMap& modifiedTimeForFilepath); + /// \brief Load the header from a file and allow access to elements /// @param sopInstanceUID A string with the uid for a given instance /// (corresponding file will be found via database) @@ -206,6 +222,11 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject 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); + /// Update the fields in the database that are used for displaying information /// from information stored in the tag-cache. /// Displayed fields are useful if the raw DICOM tags are not human readable, or @@ -229,10 +250,18 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject Q_INVOKABLE bool fileExistsAndUpToDate(const QString& filePath); /// Remove the series from the database, including images and thumbnails - Q_INVOKABLE bool removeSeries(const QString& seriesInstanceUID); + /// If clearCachedTags is set to true then cached tags associated with the series are deleted, + /// if set to False the they are left in the database unchanced. + /// 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 cleanup(); + /// 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. + /// Vacuuming may fail if there are multiple connections to the database. + Q_INVOKABLE bool cleanup(bool vacuum=false); /// \brief Access element values for given instance /// @param sopInstanceUID A string with the uid for a given instance @@ -270,6 +299,8 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject Q_INVOKABLE bool cacheTag (const QString sopInstanceUID, const QString tag, const QString value); /// Insert lists of tags into the cache as a batch query operation Q_INVOKABLE bool cacheTags (const QStringList sopInstanceUIDs, const QStringList tags, const QStringList values); + /// Remove all tags corresponding to a SOP instance UID + void removeCachedTags(const QString sopInstanceUID); /// Get displayed name of a given field Q_INVOKABLE QString displayedNameForField(QString table, QString field) const; @@ -288,11 +319,14 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// Get format of a given field /// It contains a json document with the following fields: /// - resizeMode: column resize mode. Accepted values are: "interactive" (default), "stretch", or "resizeToContents". + /// - sort: default sort order. Accepted values are: empty (default), "ascending" or "descending". + /// Only one column (or none) should have non-empty sort order in each table. Q_INVOKABLE QString formatForField(QString table, QString field) const; /// Set format of a given field Q_INVOKABLE void setFormatForField(QString table, QString field, QString format); Q_SIGNALS: + /// Things inserted to database. /// patientAdded arguments: /// - int: database index of patient (unique) within CTK database @@ -311,9 +345,18 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// - instanceUID (unique) void instanceAdded(QString); + /// This signal is emitted when the database has been opened. + void opened(); + + /// This signal is emitted when the database has been closed. + void closed(); + /// Indicate that an in-memory database has been updated void databaseChanged(); + /// Indicate that tagsToPreCache list changed + void tagsToPrecacheChanged(); + /// Indicate that the schema is about to be updated and how many files will be processed void schemaUpdateStarted(int); /// Indicate progress in updating schema (int is file number, string is file name) diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.cpp b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.cpp index 4ce46d9d5b..70f5164508 100644 --- a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.cpp +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.cpp @@ -1,139 +1,140 @@ -/*========================================================================= - - Library: CTK - - Copyright (c) PerkLab 2013 - - 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 - -// ctkDICOM includes -#include "ctkLogger.h" -#include "ctkDICOMDisplayedFieldGenerator.h" -#include "ctkDICOMDisplayedFieldGenerator_p.h" - -#include "ctkDICOMDatabase.h" -#include "ctkDICOMDisplayedFieldGeneratorDefaultRule.h" -#include "ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.h" - -//------------------------------------------------------------------------------ -static ctkLogger logger("org.commontk.dicom.DICOMDisplayedFieldGenerator" ); -//------------------------------------------------------------------------------ - - -//------------------------------------------------------------------------------ -// ctkDICOMDisplayedFieldGeneratorPrivate methods - -//------------------------------------------------------------------------------ -ctkDICOMDisplayedFieldGeneratorPrivate::ctkDICOMDisplayedFieldGeneratorPrivate(ctkDICOMDisplayedFieldGenerator& o) - : q_ptr(&o) - , Database(NULL) -{ - // register commonly used rules - this->AllRules.append(new ctkDICOMDisplayedFieldGeneratorDefaultRule); - this->AllRules.append(new ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule); - - foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, this->AllRules) - { - rule->registerEmptyFieldNames( - this->EmptyFieldNamesSeries, this->EmptyFieldNamesStudies, this->EmptyFieldNamesPatients ); - } -} - -//------------------------------------------------------------------------------ -ctkDICOMDisplayedFieldGeneratorPrivate::~ctkDICOMDisplayedFieldGeneratorPrivate() -{ - foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, this->AllRules) - { - delete rule; - } - this->AllRules.clear(); -} - -//------------------------------------------------------------------------------ - -//------------------------------------------------------------------------------ -// ctkDICOMDisplayedFieldGenerator methods - -//------------------------------------------------------------------------------ -ctkDICOMDisplayedFieldGenerator::ctkDICOMDisplayedFieldGenerator(QObject *parent):d_ptr(new ctkDICOMDisplayedFieldGeneratorPrivate(*this)) -{ - Q_UNUSED(parent); -} - -//------------------------------------------------------------------------------ -ctkDICOMDisplayedFieldGenerator::~ctkDICOMDisplayedFieldGenerator() -{ -} - -//------------------------------------------------------------------------------ -QStringList ctkDICOMDisplayedFieldGenerator::getRequiredTags() -{ - Q_D(ctkDICOMDisplayedFieldGenerator); - - QStringList requiredTags; - foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, d->AllRules) - { - requiredTags << rule->getRequiredDICOMTags(); - } - - // TODO: remove duplicates from requiredTags (maybe also sort) - return requiredTags; -} - -//------------------------------------------------------------------------------ -void ctkDICOMDisplayedFieldGenerator::updateDisplayedFieldsForInstance( QString sopInstanceUID, - QMap &displayedFieldsForCurrentSeries, QMap &displayedFieldsForCurrentStudy, QMap &displayedFieldsForCurrentPatient ) -{ - Q_D(ctkDICOMDisplayedFieldGenerator); - - QMap cachedTagsForInstance; - d->Database->getCachedTags(sopInstanceUID, cachedTagsForInstance); - - QMap newFieldsSeries; - QMap newFieldsStudy; - QMap newFieldsPatient; - foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, d->AllRules) - { - QMap initialFieldsSeries = displayedFieldsForCurrentSeries; - QMap initialFieldsStudy = displayedFieldsForCurrentStudy; - QMap initialFieldsPatient = displayedFieldsForCurrentPatient; - - rule->getDisplayedFieldsForInstance(cachedTagsForInstance, newFieldsSeries, newFieldsStudy, newFieldsPatient); - - rule->mergeDisplayedFieldsForInstance( - initialFieldsSeries, initialFieldsStudy, initialFieldsPatient, // original DB contents - newFieldsSeries, newFieldsStudy, newFieldsPatient, // new value - displayedFieldsForCurrentSeries, displayedFieldsForCurrentStudy, displayedFieldsForCurrentPatient, // new DB contents - d->EmptyFieldNamesSeries, d->EmptyFieldNamesStudies, d->EmptyFieldNamesPatients // empty field names defined by all the rules - ); - } -} - -//------------------------------------------------------------------------------ -void ctkDICOMDisplayedFieldGenerator::setDatabase(ctkDICOMDatabase* database) -{ - Q_D(ctkDICOMDisplayedFieldGenerator); - d->Database=database; -} - -//------------------------------------------------------------------------------ -void ctkDICOMDisplayedFieldGenerator::registerDisplayedFieldGeneratorRule(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule) -{ - Q_D(ctkDICOMDisplayedFieldGenerator); - d->AllRules.append(rule); -} +/*========================================================================= + + Library: CTK + + Copyright (c) PerkLab 2013 + + 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 + +// ctkDICOM includes +#include "ctkLogger.h" +#include "ctkDICOMDisplayedFieldGenerator.h" +#include "ctkDICOMDisplayedFieldGenerator_p.h" + +#include "ctkDICOMDatabase.h" +#include "ctkDICOMDisplayedFieldGeneratorDefaultRule.h" +#include "ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.h" + +//------------------------------------------------------------------------------ +static ctkLogger logger("org.commontk.dicom.DICOMDisplayedFieldGenerator" ); +//------------------------------------------------------------------------------ + + +//------------------------------------------------------------------------------ +// ctkDICOMDisplayedFieldGeneratorPrivate methods + +//------------------------------------------------------------------------------ +ctkDICOMDisplayedFieldGeneratorPrivate::ctkDICOMDisplayedFieldGeneratorPrivate(ctkDICOMDisplayedFieldGenerator& o) + : q_ptr(&o) + , Database(NULL) +{ + // register commonly used rules + this->AllRules.append(new ctkDICOMDisplayedFieldGeneratorDefaultRule); + this->AllRules.append(new ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule); + + foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, this->AllRules) + { + rule->registerEmptyFieldNames( + this->EmptyFieldNamesSeries, this->EmptyFieldNamesStudies, this->EmptyFieldNamesPatients ); + } +} + +//------------------------------------------------------------------------------ +ctkDICOMDisplayedFieldGeneratorPrivate::~ctkDICOMDisplayedFieldGeneratorPrivate() +{ + foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, this->AllRules) + { + delete rule; + } + this->AllRules.clear(); +} + +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// ctkDICOMDisplayedFieldGenerator methods + +//------------------------------------------------------------------------------ +ctkDICOMDisplayedFieldGenerator::ctkDICOMDisplayedFieldGenerator(QObject *parent):d_ptr(new ctkDICOMDisplayedFieldGeneratorPrivate(*this)) +{ + Q_UNUSED(parent); +} + +//------------------------------------------------------------------------------ +ctkDICOMDisplayedFieldGenerator::~ctkDICOMDisplayedFieldGenerator() +{ +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDisplayedFieldGenerator::getRequiredTags() +{ + Q_D(ctkDICOMDisplayedFieldGenerator); + + QStringList requiredTags; + foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, d->AllRules) + { + requiredTags << rule->getRequiredDICOMTags(); + } + requiredTags.removeDuplicates(); + + // TODO: remove duplicates from requiredTags (maybe also sort) + return requiredTags; +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGenerator::updateDisplayedFieldsForInstance( + const QString& sopInstanceUID, const QMap &cachedTagsForInstance, + QMap &displayedFieldsForCurrentSeries, + QMap &displayedFieldsForCurrentStudy, + QMap &displayedFieldsForCurrentPatient ) +{ + Q_D(ctkDICOMDisplayedFieldGenerator); + + QMap newFieldsSeries; + QMap newFieldsStudy; + QMap newFieldsPatient; + foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, d->AllRules) + { + QMap initialFieldsSeries = displayedFieldsForCurrentSeries; + QMap initialFieldsStudy = displayedFieldsForCurrentStudy; + QMap initialFieldsPatient = displayedFieldsForCurrentPatient; + + rule->getDisplayedFieldsForInstance(cachedTagsForInstance, newFieldsSeries, newFieldsStudy, newFieldsPatient); + + rule->mergeDisplayedFieldsForInstance( + initialFieldsSeries, initialFieldsStudy, initialFieldsPatient, // original DB contents + newFieldsSeries, newFieldsStudy, newFieldsPatient, // new value + displayedFieldsForCurrentSeries, displayedFieldsForCurrentStudy, displayedFieldsForCurrentPatient, // new DB contents + d->EmptyFieldNamesSeries, d->EmptyFieldNamesStudies, d->EmptyFieldNamesPatients // empty field names defined by all the rules + ); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGenerator::setDatabase(ctkDICOMDatabase* database) +{ + Q_D(ctkDICOMDisplayedFieldGenerator); + d->Database=database; +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGenerator::registerDisplayedFieldGeneratorRule(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule) +{ + Q_D(ctkDICOMDisplayedFieldGenerator); + d->AllRules.append(rule); +} diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.h b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.h index 4da4092803..ce9d989ac9 100644 --- a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.h +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.h @@ -61,7 +61,8 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDisplayedFieldGenerator : public QObject Q_INVOKABLE QStringList getRequiredTags(); /// Update displayed fields for an instance, invoking all registered rules - Q_INVOKABLE void updateDisplayedFieldsForInstance(QString sopInstanceUID, + Q_INVOKABLE void updateDisplayedFieldsForInstance(const QString& sopInstanceUID, + const QMap &cachedTags, QMap &displayedFieldsForCurrentSeries, QMap &displayedFieldsForCurrentStudy, QMap &displayedFieldsForCurrentPatient); diff --git a/Libs/DICOM/Core/ctkDICOMIndexer.cpp b/Libs/DICOM/Core/ctkDICOMIndexer.cpp index 7ba39ce704..606fb6e9a4 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer.cpp +++ b/Libs/DICOM/Core/ctkDICOMIndexer.cpp @@ -55,29 +55,277 @@ static ctkLogger logger("org.commontk.dicom.DICOMIndexer" ); //------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +// ctkDICOMIndexerPrivate methods + + +//------------------------------------------------------------------------------ +ctkDICOMIndexerPrivateWorker::ctkDICOMIndexerPrivateWorker(DICOMIndexingQueue* queue) +: RequestQueue(queue) +, RemainingRequestCount(0) +, CompletedRequestCount(0) +, TimePercentageIndexing(80.0) +, TimePercentageDatabaseInsert(15.0) +, TimePercentageDatabaseDisplayFieldsUpdate(5.0) +{ +} + +//------------------------------------------------------------------------------ +ctkDICOMIndexerPrivateWorker::~ctkDICOMIndexerPrivateWorker() +{ + this->RequestQueue->setStopRequested(true); +} + +//------------------------------------------------------------------------------ +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()) + { + this->RequestQueue->removeAllIndexingRequests(); + this->RequestQueue->setStopRequested(false); + } + DICOMIndexingQueue::IndexingRequest indexingRequest; + this->RemainingRequestCount = this->RequestQueue->popIndexingRequest(indexingRequest); + if (this->RemainingRequestCount < 0) + { + // finished + this->writeIndexingResultsToDatabase(); + // check if we got any new requests while we were writing results to database + this->RemainingRequestCount = this->RequestQueue->popIndexingRequest(indexingRequest); + if (this->RemainingRequestCount < 0) + { + this->RequestQueue->setIndexing(false); + return; + } + } + this->processIndexingRequest(indexingRequest); + this->CompletedRequestCount++; + } while (true); +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexerPrivateWorker::processIndexingRequest(DICOMIndexingQueue::IndexingRequest& indexingRequest) +{ + if (!indexingRequest.inputFolderPath.isEmpty()) + { + QDir::Filters filters = QDir::Files; + if (indexingRequest.includeHidden) + { + filters |= QDir::Hidden; + } + QDirIterator it(indexingRequest.inputFolderPath, filters, QDirIterator::Subdirectories); + while (it.hasNext()) + { + indexingRequest.inputFilesPath << it.next(); + } + } + + QTime timeProbe; + timeProbe.start(); + + int currentFileIndex = 0; + int lastReportedPercent = 0; + foreach(const QString& filePath, indexingRequest.inputFilesPath) + { + int percent = int(this->TimePercentageIndexing * (this->CompletedRequestCount + double(currentFileIndex++) / double(indexingRequest.inputFilesPath.size())) + / double(this->CompletedRequestCount + this->RemainingRequestCount + 1)); + emit this->progress(percent); + emit progressDetail(filePath); + + QDateTime fileModifiedTime = QFileInfo(filePath).lastModified(); + bool datasetAlreadyInDatabase = this->ModifiedTimeForFilepath.contains(filePath); + if (datasetAlreadyInDatabase && this->ModifiedTimeForFilepath[filePath] >= fileModifiedTime) + { + logger.debug("File " + filePath + " already added."); + continue; + } + this->ModifiedTimeForFilepath[filePath] = fileModifiedTime; + + ctkDICOMDatabase::IndexingResult indexingResult; + indexingResult.dataset = QSharedPointer(new ctkDICOMItem); + indexingResult.dataset->InitializeFromFile(filePath); + if (indexingResult.dataset->IsInitialized()) + { + indexingResult.filePath = filePath; + indexingResult.copyFile = indexingRequest.copyFile; + indexingResult.overwriteExistingDataset = datasetAlreadyInDatabase; + this->RequestQueue->pushIndexingResult(indexingResult); + } + else + { + logger.warn(QString("Could not read DICOM file:") + filePath); + } + + if (this->RequestQueue->isStopRequested()) + { + break; + } + } + + 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() +{ + QDir::Filters filters = QDir::Files; + QList indexingResults; + this->RequestQueue->popAllIndexingResults(indexingResults); + + int patientsAdded = 0; + int studiesAdded = 0; + int seriesAdded = 0; + int imagesAdded = 0; + + ctkDICOMDatabase database; + QObject::connect(&database, SIGNAL(instanceAdded(QString)), this, SLOT(databaseFileInstanceAdded())); + QObject::connect(&database, SIGNAL(displayedFieldsUpdateProgress(int)), this, SLOT(databaseDisplayFieldUpdateProgress(int))); + + if (!indexingResults.isEmpty()) + { + emit progressDetail(""); + emit progressStep("Updating database fields"); + + // Activate batch update + emit progress(this->TimePercentageDatabaseInsert); + emit updatingDatabase(true); + + QTime timeProbe; + timeProbe.start(); + + database.openDatabase(this->RequestQueue->databaseFilename()); + database.setTagsToPrecache(this->RequestQueue->tagsToPrecache()); + + int patientsCount = database.patientsCount(); + int studiesCount = database.studiesCount(); + int seriesCount = database.seriesCount(); + int imagesCount = database.imagesCount(); + + this->NumberOfInstancesToInsert = indexingResults.size(); + this->NumberOfInstancesInserted = 0; + database.insert(indexingResults); + this->NumberOfInstancesToInsert = 0; + this->NumberOfInstancesInserted = 0; + + patientsAdded = database.patientsCount() - patientsCount; + studiesAdded = database.studiesCount() - studiesCount; + seriesAdded = database.seriesCount() - seriesCount; + imagesAdded = database.imagesCount() - imagesCount; + + 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)); + + // Update displayed fields according to inserted DICOM datasets + emit progress(this->TimePercentageIndexing+this->TimePercentageDatabaseInsert); + emit progressStep("Updating database displayed fields"); + + timeProbe.start(); + + database.updateDisplayedFields(); + + elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0; + qDebug() << QString("DICOM indexer has updated display fields for %1 files [%2s]") + .arg(indexingResults.count()).arg(QString::number(elapsedTimeInSeconds, 'f', 2)); + + emit updatingDatabase(false); + } + + QObject::disconnect(&database, SIGNAL(tagsToPrecacheChanged()), this, SLOT(databaseFileInstanceAdded())); + QObject::disconnect(&database, SIGNAL(displayedFieldsUpdateProgress(int)), this, SLOT(databaseDisplayFieldUpdateProgress(int))); + + emit indexingComplete(patientsAdded, studiesAdded, seriesAdded, imagesAdded); +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexerPrivateWorker::databaseFileInstanceAdded() +{ + if (this->NumberOfInstancesToInsert < 1) + { + return; + } + this->NumberOfInstancesInserted++; + emit progress(int(this->TimePercentageIndexing + + this->TimePercentageDatabaseInsert * double(this->NumberOfInstancesInserted) / double(this->NumberOfInstancesToInsert))); +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexerPrivateWorker::databaseDisplayFieldUpdateProgress(int progressValue) +{ + emit progress(int(this->TimePercentageIndexing + this->TimePercentageDatabaseInsert + + this->TimePercentageDatabaseDisplayFieldsUpdate * double(progressValue) / 5.0)); +} + //------------------------------------------------------------------------------ // ctkDICOMIndexerPrivate methods //------------------------------------------------------------------------------ ctkDICOMIndexerPrivate::ctkDICOMIndexerPrivate(ctkDICOMIndexer& o) : q_ptr(&o) - , Canceled(false) - , StartedIndexing(0) + , Database(nullptr) + , BackgroundImportEnabled(false) { + ctkDICOMIndexerPrivateWorker* worker = new ctkDICOMIndexerPrivateWorker(&this->RequestQueue); + worker->moveToThread(&this->WorkerThread); + + connect(&this->WorkerThread, &QThread::finished, worker, &QObject::deleteLater); + connect(this, &ctkDICOMIndexerPrivate::startWorker, worker, &ctkDICOMIndexerPrivateWorker::start); + + // Progress report + 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::updatingDatabase, q_ptr, &ctkDICOMIndexer::updatingDatabase); + connect(worker, &ctkDICOMIndexerPrivateWorker::indexingComplete, q_ptr, &ctkDICOMIndexer::indexingComplete); + + this->WorkerThread.start(); } //------------------------------------------------------------------------------ ctkDICOMIndexerPrivate::~ctkDICOMIndexerPrivate() { + Q_Q(ctkDICOMIndexer); + this->RequestQueue.setStopRequested(true); + this->WorkerThread.quit(); + this->WorkerThread.wait(); + q->setDatabase(nullptr); +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexerPrivate::pushIndexingRequest(const DICOMIndexingQueue::IndexingRequest& request) +{ + Q_Q(ctkDICOMIndexer); + emit q->progressStep("Parsing DICOM files"); + this->RequestQueue.pushIndexingRequest(request); + 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); //------------------------------------------------------------------------------ // ctkDICOMIndexer methods //------------------------------------------------------------------------------ -ctkDICOMIndexer::ctkDICOMIndexer(QObject *parent):d_ptr(new ctkDICOMIndexerPrivate(*this)) +ctkDICOMIndexer::ctkDICOMIndexer(QObject *parent) + : d_ptr(new ctkDICOMIndexerPrivate(*this)) { Q_UNUSED(parent); } @@ -88,95 +336,154 @@ ctkDICOMIndexer::~ctkDICOMIndexer() } //------------------------------------------------------------------------------ -void ctkDICOMIndexer::addFile(ctkDICOMDatabase& database, - const QString filePath, - const QString& destinationDirectoryName) +void ctkDICOMIndexer::setDatabase(ctkDICOMDatabase* database) +{ + Q_D(ctkDICOMIndexer); + if (d->Database == database) + { + return; + } + if (d->Database) + { + QObject::disconnect(d->Database, SIGNAL(opened()), this, SLOT(databaseFilenameChanged())); + QObject::disconnect(d->Database, SIGNAL(tagsToPrecacheChanged()), this, SLOT(tagsToPrecacheChanged())); + } + d->Database = database; + if (d->Database) + { + QObject::connect(d->Database, SIGNAL(opened()), this, SLOT(databaseFilenameChanged())); + QObject::connect(d->Database, SIGNAL(tagsToPrecacheChanged()), this, SLOT(tagsToPrecacheChanged())); + d->RequestQueue.setDatabaseFilename(d->Database->databaseFilename()); + d->RequestQueue.setTagsToPrecache(d->Database->tagsToPrecache()); + } + else + { + d->RequestQueue.setDatabaseFilename(QString()); + d->RequestQueue.setTagsToPrecache(QStringList()); + } +} + +//------------------------------------------------------------------------------ +ctkDICOMDatabase* ctkDICOMIndexer::database() { - ctkDICOMIndexer::ScopedIndexing indexingBatch(*this, database); - emit indexingFilePath(filePath); - // Ignoring destinationDirectoryName parameter, just taking it as indication we should copy - bool copyFileToDatabase = !destinationDirectoryName.isEmpty(); - database.insert(filePath, copyFileToDatabase, true); + Q_D(ctkDICOMIndexer); + 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::addFile(ctkDICOMDatabase* db, const QString filePath, bool copyFile/*=false*/) + { + this->setDatabase(db); + this->addFile(filePath, copyFile); + } + //------------------------------------------------------------------------------ -void ctkDICOMIndexer::addDirectory(ctkDICOMDatabase& database, - const QString& directoryName, - const QString& destinationDirectoryName, - bool includeHidden/*=true*/) +void ctkDICOMIndexer::addFile(const QString filePath, bool copyFile/*=false*/) { - ctkDICOMIndexer::ScopedIndexing indexingBatch(*this, database); + Q_D(ctkDICOMIndexer); + DICOMIndexingQueue::IndexingRequest request; + request.inputFilesPath << filePath; + request.includeHidden = true; + request.copyFile = copyFile; + d->pushIndexingRequest(request); + if (!d->BackgroundImportEnabled) + { + this->waitForImportFinished(); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::addDirectory(ctkDICOMDatabase* db, const QString& directoryName, bool copyFile/*=false*/, bool includeHidden/*=true*/) +{ + this->setDatabase(db); + this->addDirectory(directoryName, copyFile, includeHidden); +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::addDirectory(const QString& directoryName, bool copyFile/*=false*/, bool includeHidden/*=true*/) +{ + Q_D(ctkDICOMIndexer); + QStringList listOfFiles; QDir directory(directoryName); - - if(directory.exists("DICOMDIR")) + if (directory.exists("DICOMDIR")) { - addDicomdir(database,directoryName,destinationDirectoryName); + this->addDicomdir(directoryName, copyFile); } else { - QDir::Filters filters = QDir::Files; - if (includeHidden) - { - filters |= QDir::Hidden; - } - QDirIterator it(directoryName, filters, QDirIterator::Subdirectories); - while(it.hasNext()) - { - listOfFiles << it.next(); - } - emit foundFilesToIndex(listOfFiles.count()); - addListOfFiles(database,listOfFiles,destinationDirectoryName); + DICOMIndexingQueue::IndexingRequest request; + request.inputFolderPath = directoryName; + request.includeHidden = includeHidden; + request.copyFile = copyFile; + d->pushIndexingRequest(request); + } + if (!d->BackgroundImportEnabled) + { + this->waitForImportFinished(); } } //------------------------------------------------------------------------------ -void ctkDICOMIndexer::addListOfFiles(ctkDICOMDatabase& database, - const QStringList& listOfFiles, - const QString& destinationDirectoryName) +void ctkDICOMIndexer::addListOfFiles(ctkDICOMDatabase* db, const QStringList& listOfFiles, bool copyFile/*=false*/) +{ + this->setDatabase(db); + this->addListOfFiles(listOfFiles, copyFile); +} + +//------------------------------------------------------------------------------ +void ctkDICOMIndexer::addListOfFiles(const QStringList& listOfFiles, bool copyFile/*=false*/) { Q_D(ctkDICOMIndexer); - ctkDICOMIndexer::ScopedIndexing indexingBatch(*this, database); - QTime timeProbe; - timeProbe.start(); - d->Canceled = false; - int currentFileIndex = 0; - int lastReportedPercent = 0; - foreach(QString filePath, listOfFiles) + DICOMIndexingQueue::IndexingRequest request; + request.inputFilesPath = listOfFiles; + request.includeHidden = true; + request.copyFile = copyFile; + d->pushIndexingRequest(request); + if (!d->BackgroundImportEnabled) { - int percent = ( 100 * currentFileIndex ) / listOfFiles.size(); - if (lastReportedPercent / 10 < percent / 10) - { - // Reporting progress has a huge overhead (pending events are processed, - // database is updated), therefore only report progress at every 10% increase - emit this->progress(percent); - lastReportedPercent = percent; - } - this->addFile(database, filePath, destinationDirectoryName); - currentFileIndex++; - - if (d->Canceled) - { - break; - } + this->waitForImportFinished(); } +} - // Update displayed fields according to inserted DICOM datasets - emit displayedFieldsUpdateStarted(); - database.updateDisplayedFields(); - - float elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0; - qDebug() << QString("DICOM indexer has successfully processed %1 files [%2s]") - .arg(currentFileIndex).arg(QString::number(elapsedTimeInSeconds,'f', 2)); +//------------------------------------------------------------------------------ +bool ctkDICOMIndexer::addDicomdir(ctkDICOMDatabase* db, const QString& directoryName, bool copyFile/*=false*/) +{ + this->setDatabase(db); + return this->addDicomdir(directoryName, copyFile); } //------------------------------------------------------------------------------ -bool ctkDICOMIndexer::addDicomdir(ctkDICOMDatabase& database, - const QString& directoryName, - const QString& destinationDirectoryName - ) +bool ctkDICOMIndexer::addDicomdir(const QString& directoryName, bool copyFile/*=false*/) { - ctkDICOMIndexer::ScopedIndexing indexingBatch(*this, database); //Initialize dicomdir with directory path QString dcmFilePath = directoryName; dcmFilePath.append("/DICOMDIR"); @@ -263,91 +570,44 @@ bool ctkDICOMIndexer::addDicomdir(ctkDICOMDatabase& database, << QString("DICOM indexer has successfully processed DICOMDIR in %1 [%2s]") .arg(directoryName) .arg(QString::number(elapsedTimeInSeconds,'f', 2)); - emit foundFilesToIndex(listOfInstances.count()); - addListOfFiles(database,listOfInstances,destinationDirectoryName); + this->addListOfFiles(listOfInstances, copyFile); } return success; } //------------------------------------------------------------------------------ -void ctkDICOMIndexer::refreshDatabase(ctkDICOMDatabase& database, const QString& directoryName) +void ctkDICOMIndexer::waitForImportFinished(int msecTimeout /*=-1*/) { - Q_UNUSED(database); - Q_UNUSED(directoryName); - /* - * Probably this should go to the database class as well - * Or we have to extend the interface to make possible what we do here - * without using SQL directly - - /// get all filenames from the database - QSqlQuery allFilesQuery(database.database()); - QStringList databaseFileNames; - QStringList filesToRemove; - this->loggedExec(allFilesQuery, "SELECT Filename from Images;"); - - while (allFilesQuery.next()) - { - QString fileName = allFilesQuery.value(0).toString(); - databaseFileNames.append(fileName); - if (! QFile::exists(fileName) ) - { - filesToRemove.append(fileName); - } - } - - QSet filesytemFiles; - QDirIterator dirIt(directoryName); - while (dirIt.hasNext()) - { - filesytemFiles.insert(dirIt.next()); - } - - // TODO: it looks like this function was never finished... - // - // I guess the next step is to remove all filesToRemove from the database - // and also to add filesystemFiles into the database tables - */ + if (!this->isImporting()) + { + return; } - -//------------------------------------------------------------------------------ -void ctkDICOMIndexer::waitForImportFinished() -{ - // No-op - this had been used when the indexing was multi-threaded, - // and has only been retained for API compatibility. -} - -//---------------------------------------------------------------------------- -void ctkDICOMIndexer::cancel() -{ - Q_D(ctkDICOMIndexer); - d->Canceled = true; + QTimer timer; + timer.setSingleShot(true); + QEventLoop loop; + connect(this, &ctkDICOMIndexer::indexingComplete, &loop, &QEventLoop::quit); + if (msecTimeout >= 0) + { + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.start(msecTimeout); + } + if (!this->isImporting()) + { + return; + } + loop.exec(); } -//---------------------------------------------------------------------------- -void ctkDICOMIndexer::startIndexing(ctkDICOMDatabase& database) +//------------------------------------------------------------------------------ +bool ctkDICOMIndexer::isImporting() { Q_D(ctkDICOMIndexer); - if (d->StartedIndexing == 0) - { - // Indexing has just been started - database.prepareInsert(); - } - d->StartedIndexing++; + return d->RequestQueue.isIndexing(); } //---------------------------------------------------------------------------- -void ctkDICOMIndexer::endIndexing() +void ctkDICOMIndexer::cancel() { Q_D(ctkDICOMIndexer); - d->StartedIndexing--; - if (d->StartedIndexing == 0) - { - // Indexing has just been completed - emit this->indexingComplete(); - } - if (d->StartedIndexing < 0) - { - qWarning() << QString("ctkDICOMIndexer::endIndexing called without matching startIndexing"); - d->StartedIndexing = 0; - } + d->RequestQueue.setStopRequested(true); } diff --git a/Libs/DICOM/Core/ctkDICOMIndexer.h b/Libs/DICOM/Core/ctkDICOMIndexer.h index 1b8ff18336..9c541fa13c 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer.h +++ b/Libs/DICOM/Core/ctkDICOMIndexer.h @@ -37,10 +37,26 @@ class ctkDICOMIndexerPrivate; class CTK_DICOM_CORE_EXPORT ctkDICOMIndexer : public QObject { Q_OBJECT + Q_PROPERTY(bool backgroundImportEnabled READ isBackgroundImportEnabled WRITE setBackgroundImportEnabled) + Q_PROPERTY(bool importing READ isImporting) + public: explicit ctkDICOMIndexer(QObject *parent = 0); virtual ~ctkDICOMIndexer(); + Q_INVOKABLE void setDatabase(ctkDICOMDatabase* database); + Q_INVOKABLE ctkDICOMDatabase* database(); + + /// If enabled, addDirectory and addFile... methods return immediately + /// indexing is performed in a background thread, + /// and progress and completion are indicated by signals. + /// Disabled by default. + void setBackgroundImportEnabled(bool); + bool isBackgroundImportEnabled() const; + + /// Returns with true if background importing is currently in progress. + bool isImporting(); + /// /// \brief Adds directory to database and optionally copies files to /// destinationDirectory. @@ -52,8 +68,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMIndexer : public QObject /// DICOM folders may be created based on series or study name, which sometimes start /// with a . character, therefore it is advisable to include hidden files and folders. /// - Q_INVOKABLE void addDirectory(ctkDICOMDatabase& database, const QString& directoryName, - const QString& destinationDirectoryName = "", bool includeHidden = true); + Q_INVOKABLE void addDirectory(const QString& directoryName, bool copyFile = false, bool includeHidden = true); + /// Kept for backward compatibility + Q_INVOKABLE void addDirectory(ctkDICOMDatabase* db, const QString& directoryName, bool copyFile = false, bool includeHidden = true); /// /// \brief Adds directory to database by using DICOMDIR and optionally copies files to @@ -62,8 +79,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMIndexer : public QObject /// DICOM images accordingly. /// \return Returns false if there was an error while processing the DICOMDIR file. /// - Q_INVOKABLE bool addDicomdir(ctkDICOMDatabase& database, const QString& directoryName, - const QString& destinationDirectoryName = ""); + Q_INVOKABLE bool addDicomdir(const QString& directoryName, bool copyFile = false); + /// Kept for backward compatibility + Q_INVOKABLE bool addDicomdir(ctkDICOMDatabase* db, const QString& directoryName, bool copyFile = false); /// /// \brief Adds a QStringList containing the file path to database and optionally copies files to @@ -72,8 +90,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMIndexer : public QObject /// Scan the directory using Dcmtk and populate the database with all the /// DICOM images accordingly. /// - Q_INVOKABLE void addListOfFiles(ctkDICOMDatabase& database, const QStringList& listOfFiles, - const QString& destinationDirectoryName = ""); + Q_INVOKABLE void addListOfFiles(const QStringList& listOfFiles, bool copyFile = false); + /// Kept for backward compatibility + Q_INVOKABLE void addListOfFiles(ctkDICOMDatabase* db, const QStringList& listOfFiles, bool copyFile = false); /// /// \brief Adds a file to database and optionally copies the file to @@ -85,75 +104,35 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMIndexer : public QObject /// Scan the file using Dcmtk and populate the database with all the /// DICOM fields accordingly. /// - Q_INVOKABLE void addFile(ctkDICOMDatabase& database, const QString filePath, - const QString& destinationDirectoryName = ""); + Q_INVOKABLE void addFile(const QString filePath, bool copyFile = false); + /// Kept for backward compatibility + Q_INVOKABLE void addFile(ctkDICOMDatabase* db, const QString filePath, bool copyFile = false); - Q_INVOKABLE void refreshDatabase(ctkDICOMDatabase& database, const QString& directoryName); - - /// - /// \brief Deprecated - no op. - /// \deprecated - /// Previously ensured that the QFuture threads have all finished indexing - /// before returning control. - /// - Q_INVOKABLE void waitForImportFinished(); - - /// Call this before performing multiple add...() calls in one batch - /// to slightly increase indexing performance and to make only a single - /// indexingComplete() signal emitted for multiple add...() operations. - /// - /// If startIndexing() is called before a batch of insertions, then - /// endIndexing() method must be called after the insertions are completed. /// - /// It is recommended to use ScopedIndexing helper class to call startIndexing - /// and endIndexing automatically. - Q_INVOKABLE void startIndexing(ctkDICOMDatabase& database); - - /// Call this method after batch insertion is completed, and only if startIndexing() - /// was called before batch insertion was started. - Q_INVOKABLE void endIndexing(); - - /// Helper class to automatically call startIndexing and endIndexing. - /// Its constructor calls startIndexing and its destructor calls endIndexing. - /// - /// Example: - /// ... - /// { - /// ctkDICOMIndexer::ScopedIndexing indexingBatch(indexer, database); // this calls startIndexing - /// indexer.addDirectory(database, dir1); - /// indexer.addDirectory(database, dir2); - /// indexer.addDirectory(database, dir3); - /// } // endIndexing is called when indexingBatch goes out of scope - /// - class ScopedIndexing - { - public: - ScopedIndexing(ctkDICOMIndexer& indexer, ctkDICOMDatabase& database) - { - this->Indexer = &indexer; - this->Indexer->startIndexing(database); - } - ~ScopedIndexing() - { - this->Indexer->endIndexing(); - } - private: - ctkDICOMIndexer* Indexer; - }; + /// \brief Wait for all the indexing operations to complete + /// This can be useful to ensure that importing is completed when background indexing is enabled. + /// msecTimeout specifies a maximum timeout. If <0 then it means wait indefinitely. + Q_INVOKABLE void waitForImportFinished(int msecTimeout = -1); Q_SIGNALS: - void foundFilesToIndex(int); - void indexingFileNumber(int); - void indexingFilePath(QString); + /// 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); + /// Progress in percentage void progress(int); - void indexingComplete(); - - /// Trigger showing progress dialog for displayed fields update - void displayedFieldsUpdateStarted(); + /// Indexing is completed. + void indexingComplete(int patientsAdded, int studiesAdded, int seriesAdded, int imagesAdded); + void updatingDatabase(bool); public Q_SLOTS: + /// Stop indexing (all completed indexing results will be added to the database) void cancel(); +protected Q_SLOTS: + void databaseFilenameChanged(); + void tagsToPrecacheChanged(); + protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Core/ctkDICOMIndexer_p.h b/Libs/DICOM/Core/ctkDICOMIndexer_p.h index a1f60835e3..5caeab8901 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer_p.h +++ b/Libs/DICOM/Core/ctkDICOMIndexer_p.h @@ -24,6 +24,190 @@ #include #include "ctkDICOMIndexer.h" +#include "ctkDICOMItem.h" + +class ctkDICOMDatabase; +class ctkDataset; + +class DICOMIndexingQueue +{ +public: + struct IndexingRequest + { + /// Either inputFolderPath or inputFilesPath is used + QString inputFolderPath; + QStringList inputFilesPath; + /// If inputFolderPath is specified, includeHidden is used to decide + /// if hidden files and folders are imported or not. + bool includeHidden; + /// Make a copy of the indexed file into the database. + /// If false then only a link to the existing file is added. + bool copyFile; + }; + + DICOMIndexingQueue() + : Mutex(QMutex::Recursive) + , IsIndexing(false) + , StopRequested(false) + { + } + + virtual ~DICOMIndexingQueue() + { + } + + QString databaseFilename() + { + QMutexLocker locker(&this->Mutex); + return this->DatabaseFilename; + } + + void setDatabaseFilename(const QString& filename) + { + QMutexLocker locker(&this->Mutex); + this->DatabaseFilename = filename; + } + + QStringList tagsToPrecache() + { + QMutexLocker locker(&this->Mutex); + return this->TagsToPrecache; + } + + void setTagsToPrecache(const QStringList& tags) + { + QMutexLocker locker(&this->Mutex); + this->TagsToPrecache = tags; + } + + void removeAllIndexingRequests() + { + QMutexLocker locker(&this->Mutex); + this->IndexingRequests.clear(); + } + + int popIndexingRequest(IndexingRequest& indexingRequest) + { + QMutexLocker locker(&this->Mutex); + if (this->IndexingRequests.empty()) + { + return -1; + } + indexingRequest = this->IndexingRequests.takeFirst(); + return this->IndexingRequests.count(); + } + + void pushIndexingRequest(const IndexingRequest& indexingRequest) + { + QMutexLocker locker(&this->Mutex); + this->IndexingRequests.push_back(indexingRequest); + } + + void popAllIndexingResults(QList& indexingResults) + { + QMutexLocker locker(&this->Mutex); + indexingResults = this->IndexingResults; + this->IndexingResults.clear(); + } + + void pushIndexingResult(const ctkDICOMDatabase::IndexingResult& indexingResult) + { + QMutexLocker locker(&this->Mutex); + this->IndexingResults.push_back(indexingResult); + } + + void modifiedTimeForFilepath(QMap& timesForPaths) + { + QMutexLocker locker(&this->Mutex); + timesForPaths = this->ModifiedTimeForFilepath; + } + + void setModifiedTimeForFilepath(const QMap& timesForPaths) + { + QMutexLocker locker(&this->Mutex); + this->ModifiedTimeForFilepath = timesForPaths; + } + + void setIndexing(bool indexing) + { + QMutexLocker locker(&this->Mutex); + this->IsIndexing = indexing; + } + + bool isIndexing() + { + QMutexLocker locker(&this->Mutex); + return this->IsIndexing; + } + + bool isStopRequested() + { + return this->StopRequested; + } + + void setStopRequested(bool stop) + { + this->StopRequested = stop; + } + +protected: + // List of already indexed file paths and oldest file modified time in the database + QMap ModifiedTimeForFilepath; + + QList IndexingRequests; + QList IndexingResults; + + QString DatabaseFilename; + QStringList TagsToPrecache; + + bool IsIndexing; + bool StopRequested; + + mutable QMutex Mutex; +}; + + +class ctkDICOMIndexerPrivateWorker : public QObject +{ + Q_OBJECT + +public: + ctkDICOMIndexerPrivateWorker(DICOMIndexingQueue* queue); + virtual ~ctkDICOMIndexerPrivateWorker(); + +public Q_SLOTS: + void start(); + void databaseFileInstanceAdded(); + void databaseDisplayFieldUpdateProgress(int); + +Q_SIGNALS: + void progress(int); + void progressDetail(QString); + void progressStep(QString); + void updatingDatabase(bool); + void indexingComplete(int, int, int, int); + +private: + + void processIndexingRequest(DICOMIndexingQueue::IndexingRequest& request); + void writeIndexingResultsToDatabase(); + + DICOMIndexingQueue* RequestQueue; + int NumberOfInstancesToInsert; + int NumberOfInstancesInserted; + + double TimePercentageIndexing; + double TimePercentageDatabaseInsert; + double TimePercentageDatabaseDisplayFieldsUpdate; + + int RemainingRequestCount; // the current request in progress is not included + int CompletedRequestCount; // the current request in progress is not included + + // List of already indexed file paths and oldest file modified time in the database. + // Cached here to avoid locking/unlocking a mutex each time a file is looked up. + QMap ModifiedTimeForFilepath; +}; + //------------------------------------------------------------------------------ class ctkDICOMIndexerPrivate : public QObject @@ -39,18 +223,18 @@ class ctkDICOMIndexerPrivate : public QObject ctkDICOMIndexerPrivate(ctkDICOMIndexer&); ~ctkDICOMIndexerPrivate(); + void pushIndexingRequest(const DICOMIndexingQueue::IndexingRequest& request); + +Q_SIGNALS: + void startWorker(); + +//public Q_SLOTS: + public: - ctkDICOMAbstractThumbnailGenerator* thumbnailGenerator; - bool Canceled; - - // Incremented each time startIndexing is called - // and decremented when endIndexing is called. - // This makes sure that when a batch of indexing - // operations are performed, at any level - // (file, folder, or set of folders) then - // batch processing initialization and finalization - // are performed exactly once. - int StartedIndexing; + DICOMIndexingQueue RequestQueue; + QThread WorkerThread; + ctkDICOMDatabase* Database; + bool BackgroundImportEnabled; }; diff --git a/Libs/DICOM/Core/ctkDICOMRetrieve.cpp b/Libs/DICOM/Core/ctkDICOMRetrieve.cpp index b8c3189367..65d0e11a97 100644 --- a/Libs/DICOM/Core/ctkDICOMRetrieve.cpp +++ b/Libs/DICOM/Core/ctkDICOMRetrieve.cpp @@ -606,11 +606,17 @@ QString ctkDICOMRetrieve::moveDestinationAETitle()const return d->MoveDestinationAETitle; } +static void skipDelete(QObject* obj) +{ + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + //------------------------------------------------------------------------------ void ctkDICOMRetrieve::setDatabase(ctkDICOMDatabase& dicomDatabase) { Q_D(ctkDICOMRetrieve); - d->Database = QSharedPointer(&dicomDatabase); + d->Database = QSharedPointer(&dicomDatabase, skipDelete); } //------------------------------------------------------------------------------ diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMBrowser.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMBrowser.ui index 5a282e4931..5b3737c30c 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMBrowser.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMBrowser.ui @@ -6,8 +6,8 @@ 0 0 - 802 - 607 + 583 + 445 @@ -30,7 +30,7 @@ 0 - 12 + 0 @@ -49,57 +49,50 @@ - - - 12 + + + background-color: rgb(245, 245, 170); - - - - 12 - - - 12 - - - - - - 100 - 20 - - - - LocalDatabase: - - - - - - - - 200 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - + + QFrame::Box + + + + + + + 0 + 0 + + + + Warning + + + + + + + Update database + + + + + + + Create new database + + + + + + + Select database folder + + + + + @@ -113,8 +106,14 @@ 0 - - + + + + 0 + 0 + + + true @@ -124,6 +123,96 @@ + + + + QFrame::Box + + + QFrame::Raised + + + + + + + 0 + 0 + + + + Information + + + + + + + OK + + + + + + + + + + QFrame::Box + + + QFrame::Raised + + + + + + 0 + + + + + + + Cancel + + + + + + + + 0 + 0 + + + + Progress + + + + + + + false + + + 300 + + + false + + + true + + + + + + + + + @@ -141,7 +230,7 @@ Export - Export selected study/series to a DICOM folder (not yet available) + Export selected series to a DICOM folder @@ -149,7 +238,7 @@ Query - Query and Retrieve DICOM studies from a DICOM node + Query and retrieve studies from a DICOM server @@ -160,7 +249,7 @@ Send - Send DICOM Studies to a DICOM node (not yet available) + Send selected series to DICOM server @@ -171,7 +260,7 @@ Remove - Remove selected series, studies, patients from database + Remove selected series from database @@ -285,11 +374,28 @@ + + ActionSend + triggered() + ctkDICOMBrowser + openSendDialog() + + + -1 + -1 + + + 291 + 222 + + + openImportDialog() openExportDialog() openQueryDialog() + openSendDialog() onDatabaseDirectoryChaged(QString) onNextImage() onPreviousImage() diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui index 56a0ff0dc8..29020c49a1 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui @@ -6,8 +6,8 @@ 0 0 - 883 - 516 + 442 + 311 @@ -51,8 +51,8 @@ - - + + 0 diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMQueryRetrieveWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMQueryRetrieveWidget.ui index 7c7f4682bc..843dbb466d 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMQueryRetrieveWidget.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMQueryRetrieveWidget.ui @@ -11,7 +11,7 @@ - Form + DICOM Query/Retrieve diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMTableManager.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMTableManager.ui index dd554a39c2..7b5c3feeb3 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMTableManager.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMTableManager.ui @@ -6,8 +6,8 @@ 0 0 - 833 - 455 + 806 + 473 @@ -26,10 +26,124 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 6 + + + 6 + + + 0 + + + 6 + + + 3 + + + + + Patients: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + Studies: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + Series: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + - + 0 0 @@ -42,12 +156,15 @@ QSplitter::handle:vertical {height: 2px;} Qt::Horizontal + + 0 + false - - - + + + @@ -58,6 +175,11 @@ QSplitter::handle:vertical {height: 2px;} QWidget
ctkDICOMTableView.h
+ + ctkSearchBox + QLineEdit +
ctkSearchBox.h
+
diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMTableView.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMTableView.ui index 3e3133f9af..c8809b1364 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMTableView.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMTableView.ui @@ -6,7 +6,7 @@ 0 0 - 794 + 283 462 @@ -14,48 +14,77 @@ Form + + 3 + + + 6 + + + 3 + + + 6 + + + 3 + - - - - - Table Name - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - - - - - - - - - - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Table Name + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + @@ -97,7 +126,7 @@ ctkSearchBox QLineEdit -
ctkSearchBox.h
+
ctkSearchBox.h
diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest.cpp index aac6db817d..d72ed26993 100644 --- a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest.cpp +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest.cpp @@ -95,7 +95,7 @@ void ctkDICOMBrowserTester::init() void ctkDICOMBrowserTester::testDefaults() { // Clear left over settings - QSettings().remove(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey()); + QSettings().remove("DatabaseDirectory"); ctkDICOMBrowser browser; @@ -103,8 +103,6 @@ void ctkDICOMBrowserTester::testDefaults() QVERIFY(QFileInfo("./ctkDICOM-Database/ctkDICOM.sql").isFile()); QVERIFY(QFileInfo("./ctkDICOM-Database/ctkDICOMTagCache.sql").isFile()); - QCOMPARE(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), QString("DatabaseDirectory")); - QCOMPARE(browser.databaseDirectory(), QString("./ctkDICOM-Database")); QVERIFY(browser.database() != 0); @@ -126,7 +124,7 @@ void ctkDICOMBrowserTester::testDatabaseDirectory() { // Check that value from setting is picked up { - QSettings().setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), this->TemporaryDatabaseDirectoryName); + QSettings().setValue("DatabaseDirectory", this->TemporaryDatabaseDirectoryName); QCOMPARE(QFileInfo(this->TemporaryDatabaseDirectoryName).isDir(), false); ctkDICOMBrowser browser; @@ -140,7 +138,7 @@ void ctkDICOMBrowserTester::testDatabaseDirectory() // ---------------------------------------------------------------------------- void ctkDICOMBrowserTester::testImportDirectoryMode() { - QSettings().setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), this->TemporaryDatabaseDirectoryName); + QSettings().setValue("DatabaseDirectory", this->TemporaryDatabaseDirectoryName); ctkDICOMBrowser browser; @@ -165,7 +163,7 @@ void ctkDICOMBrowserTester::testImportDirectories() QFETCH(QStringList, directories); QFETCH(ctkDICOMBrowser::ImportDirectoryMode, importDirectoryMode); - QSettings().setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), this->TemporaryDatabaseDirectoryName); + QSettings().setValue("DatabaseDirectory", this->TemporaryDatabaseDirectoryName); ctkDICOMBrowser browser; @@ -234,7 +232,7 @@ void ctkDICOMBrowserTester::testImportDirectory() QFETCH(QStringList, directories); QFETCH(ctkDICOMBrowser::ImportDirectoryMode, importDirectoryMode); - QSettings().setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), this->TemporaryDatabaseDirectoryName); + QSettings().setValue("DatabaseDirectory", this->TemporaryDatabaseDirectoryName); ctkDICOMBrowser browser; @@ -277,7 +275,7 @@ void ctkDICOMBrowserTester::testOnImportDirectory() QFETCH(QStringList, directories); QFETCH(ctkDICOMBrowser::ImportDirectoryMode, importDirectoryMode); - QSettings().setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), this->TemporaryDatabaseDirectoryName); + QSettings().setValue("DatabaseDirectory", this->TemporaryDatabaseDirectoryName); ctkDICOMBrowser browser; diff --git a/Libs/DICOM/Widgets/ctkDICOMAppWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMAppWidget.cpp index aa6d7d63ba..1f0f39b329 100644 --- a/Libs/DICOM/Widgets/ctkDICOMAppWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMAppWidget.cpp @@ -114,6 +114,7 @@ ctkDICOMAppWidgetPrivate::ctkDICOMAppWidgetPrivate(ctkDICOMAppWidget* parent): q ThumbnailGenerator = QSharedPointer (new ctkDICOMThumbnailGenerator); DICOMDatabase->setThumbnailGenerator(ThumbnailGenerator.data()); DICOMIndexer = QSharedPointer (new ctkDICOMIndexer); + DICOMIndexer->setDatabase(DICOMDatabase.data()); IndexerProgress = 0; UpdateSchemaProgress = 0; DisplayImportSummary = true; @@ -278,13 +279,13 @@ ctkDICOMAppWidget::ctkDICOMAppWidget(QWidget* _parent):Superclass(_parent), //initialize directory from settings, then listen for changes QSettings settings; - if ( settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), "") == "" ) + if ( settings.value("DatabaseDirectory", "") == "" ) { QString directory = QString("./ctkDICOM-Database"); - settings.setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), directory); + settings.setValue("DatabaseDirectory", directory); settings.sync(); } - QString databaseDirectory = settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey()).toString(); + QString databaseDirectory = settings.value("DatabaseDirectory").toString(); this->setDatabaseDirectory(databaseDirectory); d->DirectoryButton->setDirectory(databaseDirectory); @@ -393,7 +394,7 @@ void ctkDICOMAppWidget::setDatabaseDirectory(const QString& directory) Q_D(ctkDICOMAppWidget); QSettings settings; - settings.setValue(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(), directory); + settings.setValue("DatabaseDirectory", directory); settings.sync(); //close the active DICOM database @@ -434,7 +435,7 @@ void ctkDICOMAppWidget::setDatabaseDirectory(const QString& directory) QString ctkDICOMAppWidget::databaseDirectory() const { QSettings settings; - return settings.value(ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey()).toString(); + return settings.value("DatabaseDirectory").toString(); } //---------------------------------------------------------------------------- @@ -670,10 +671,7 @@ void ctkDICOMAppWidget::onImportDirectory(QString directory) { QCheckBox* copyOnImport = qobject_cast(d->ImportDialog->bottomWidget()); QString targetDirectory; - if (copyOnImport->checkState() == Qt::Checked) - { - targetDirectory = d->DICOMDatabase->databaseDirectory(); - } + bool copyFiles = (copyOnImport->checkState() == Qt::Checked); // reset counts d->PatientsAddedDuringImport = 0; @@ -683,7 +681,7 @@ void ctkDICOMAppWidget::onImportDirectory(QString directory) // show progress dialog and perform indexing d->showIndexerDialog(); - d->DICOMIndexer->addDirectory(*d->DICOMDatabase,directory,targetDirectory); + d->DICOMIndexer->addDirectory(directory, copyFiles); // display summary result if (d->DisplayImportSummary) diff --git a/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp b/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp index 023667ed74..dcaa3fd05f 100644 --- a/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp @@ -23,10 +23,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -36,6 +38,7 @@ #include #include #include +#include #include // ctkWidgets includes @@ -49,16 +52,69 @@ // ctkDICOMWidgets includes #include "ctkDICOMBrowser.h" +#include "ctkDICOMObjectListWidget.h" #include "ctkDICOMQueryResultsTabWidget.h" #include "ctkDICOMQueryRetrieveWidget.h" #include "ctkDICOMQueryWidget.h" #include "ctkDICOMTableManager.h" +#include "ctkDICOMTableView.h" #include "ui_ctkDICOMBrowser.h" -//logger -#include -static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMBrowser"); +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 (!savedGeometry.isEmpty()) + { + this->restoreGeometry(savedGeometry); + } + } + + void hideEvent(QHideEvent *event) + { + this->savedGeometry = this->saveGeometry(); + QDialog::hideEvent(event); + } + +protected: + ctkDICOMObjectListWidget* tagListWidget; + QByteArray savedGeometry; +}; //---------------------------------------------------------------------------- class ctkDICOMBrowserPrivate: public Ui_ctkDICOMBrowser @@ -67,7 +123,7 @@ class ctkDICOMBrowserPrivate: public Ui_ctkDICOMBrowser ctkDICOMBrowser* const q_ptr; Q_DECLARE_PUBLIC(ctkDICOMBrowser); - ctkDICOMBrowserPrivate(ctkDICOMBrowser*, QString); + ctkDICOMBrowserPrivate(ctkDICOMBrowser*, QSharedPointer database); ~ctkDICOMBrowserPrivate(); void init(); @@ -77,25 +133,22 @@ class ctkDICOMBrowserPrivate: public Ui_ctkDICOMBrowser void importOldSettings(); ctkFileDialog* ImportDialog; + ctkDICOMMetadataDialog* MetadataDialog; ctkDICOMQueryRetrieveWidget* QueryRetrieveWidget; QSharedPointer DICOMDatabase; QSharedPointer DICOMIndexer; - QProgressDialog *IndexerProgress; QProgressDialog *UpdateSchemaProgress; QProgressDialog *UpdateDisplayedFieldsProgress; QProgressDialog *ExportProgress; - void showIndexerDialog(); void showUpdateSchemaDialog(); - // used when suspending the ctkDICOMModel - QSqlDatabase EmptyDatabase; - bool DisplayImportSummary; bool ConfirmRemove; bool ShemaUpdateAutoCreateDirectory; + bool SendActionVisible; // local count variables to keep track of the number of items // added to the database during an import operation @@ -108,49 +161,25 @@ class ctkDICOMBrowserPrivate: public Ui_ctkDICOMBrowser QString DatabaseDirectorySettingsKey; // Default database path to use if there is nothing in settings - QString DefaultDatabasePath; -}; + QString DefaultDatabaseDirectory; + QString DatabaseDirectory; -//---------------------------------------------------------------------------- -class ctkDICOMImportStats -{ -public: - ctkDICOMImportStats(ctkDICOMBrowserPrivate* dicomBrowserPrivate) : - DICOMBrowserPrivate(dicomBrowserPrivate) - { - // reset counts - ctkDICOMBrowserPrivate* d = this->DICOMBrowserPrivate; - d->PatientsAddedDuringImport = 0; - d->StudiesAddedDuringImport = 0; - d->SeriesAddedDuringImport = 0; - d->InstancesAddedDuringImport = 0; - } - - QString summary() - { - ctkDICOMBrowserPrivate* d = this->DICOMBrowserPrivate; - QString message = "Directory import completed.\n\n"; - message += QString("%1 New Patients\n").arg(QString::number(d->PatientsAddedDuringImport)); - message += QString("%1 New Studies\n").arg(QString::number(d->StudiesAddedDuringImport)); - message += QString("%1 New Series\n").arg(QString::number(d->SeriesAddedDuringImport)); - message += QString("%1 New Instances\n").arg(QString::number(d->InstancesAddedDuringImport)); - return message; - } - - ctkDICOMBrowserPrivate* DICOMBrowserPrivate; + bool BatchUpdateBeforeIndexingUpdate; }; +CTK_GET_CPP(ctkDICOMBrowser, bool, isSendActionVisible, SendActionVisible); + //---------------------------------------------------------------------------- // ctkDICOMBrowserPrivate methods //---------------------------------------------------------------------------- -ctkDICOMBrowserPrivate::ctkDICOMBrowserPrivate(ctkDICOMBrowser* parent, QString databaseDirectorySettingsKey) +ctkDICOMBrowserPrivate::ctkDICOMBrowserPrivate(ctkDICOMBrowser* parent, QSharedPointer database) : q_ptr(parent) , ImportDialog(0) + , MetadataDialog(0) , QueryRetrieveWidget(0) - , DICOMDatabase( QSharedPointer(new ctkDICOMDatabase) ) + , DICOMDatabase(database) , DICOMIndexer( QSharedPointer(new ctkDICOMIndexer) ) - , IndexerProgress(0) , UpdateSchemaProgress(0) , UpdateDisplayedFieldsProgress(0) , ExportProgress(0) @@ -161,22 +190,20 @@ ctkDICOMBrowserPrivate::ctkDICOMBrowserPrivate(ctkDICOMBrowser* parent, QString , StudiesAddedDuringImport(0) , SeriesAddedDuringImport(0) , InstancesAddedDuringImport(0) - , DefaultDatabasePath("./ctkDICOM-Database") - , DatabaseDirectorySettingsKey(databaseDirectorySettingsKey) + , DefaultDatabaseDirectory("./ctkDICOM-Database") + , SendActionVisible(false) + , BatchUpdateBeforeIndexingUpdate(false) { - if (this->DatabaseDirectorySettingsKey.isEmpty()) + if (this->DICOMDatabase.isNull()) { - this->DatabaseDirectorySettingsKey = ctkDICOMBrowser::defaultDatabaseDirectorySettingsKey(); + this->DICOMDatabase = QSharedPointer(new ctkDICOMDatabase); } } //---------------------------------------------------------------------------- ctkDICOMBrowserPrivate::~ctkDICOMBrowserPrivate() { - if ( IndexerProgress ) - { - delete IndexerProgress; - } + this->DICOMIndexer->waitForImportFinished(); if ( UpdateSchemaProgress ) { delete UpdateSchemaProgress; @@ -221,51 +248,29 @@ void ctkDICOMBrowserPrivate::showUpdateSchemaDialog() } //---------------------------------------------------------------------------- -void ctkDICOMBrowserPrivate::showIndexerDialog() +void ctkDICOMBrowserPrivate::init() { Q_Q(ctkDICOMBrowser); - if (IndexerProgress == 0) - { - // - // Set up the Indexer Progress Dialog - // - IndexerProgress = new QProgressDialog( q->tr("DICOM Import"), "Cancel", 0, 100, q, - Qt::WindowTitleHint | Qt::WindowSystemMenuHint); - // We don't want the progress dialog to resize itself, so we bypass the label - // by creating our own - QLabel* progressLabel = new QLabel(q->tr("Initialization...")); - IndexerProgress->setLabel(progressLabel); - IndexerProgress->setWindowModality(Qt::ApplicationModal); - IndexerProgress->setMinimumDuration(0); - IndexerProgress->setValue(0); - - q->connect(IndexerProgress, SIGNAL(canceled()), DICOMIndexer.data(), SLOT(cancel())); + qRegisterMetaType("ctkDICOMBrowser::ImportDirectoryMode"); - q->connect(DICOMIndexer.data(), SIGNAL(progress(int)), IndexerProgress, SLOT(setValue(int))); - q->connect(DICOMIndexer.data(), SIGNAL(indexingFilePath(QString)), progressLabel, SLOT(setText(QString))); - q->connect(DICOMIndexer.data(), SIGNAL(indexingFilePath(QString)), q, SLOT(onFileIndexed(QString))); + this->DICOMIndexer->setDatabase(this->DICOMDatabase.data()); + this->DICOMIndexer->setBackgroundImportEnabled(true); - // close the dialog - q->connect(DICOMIndexer.data(), SIGNAL(indexingComplete()), IndexerProgress, SLOT(close())); - // stop indexing and reset the database if canceled - q->connect(IndexerProgress, SIGNAL(canceled()), DICOMIndexer.data(), SLOT(cancel())); - - // allow users of this widget to know that the process has finished - q->connect(IndexerProgress, SIGNAL(canceled()), q, SIGNAL(directoryImported())); - q->connect(DICOMIndexer.data(), SIGNAL(indexingComplete()), q, SIGNAL(directoryImported())); - } - IndexerProgress->show(); -} + this->setupUi(q); -//---------------------------------------------------------------------------- -void ctkDICOMBrowserPrivate::init() -{ - Q_Q(ctkDICOMBrowser); + this->ActionSend->setVisible(this->SendActionVisible); - qRegisterMetaType("ctkDICOMBrowser::ImportDirectoryMode"); + this->DatabaseDirectoryProblemFrame->hide(); + this->InformationMessageFrame->hide(); + this->ProgressFrame->hide(); - this->setupUi(q); + q->connect(this->ProgressCancelButton, SIGNAL(clicked()), DICOMIndexer.data(), SLOT(cancel())); + q->connect(DICOMIndexer.data(), SIGNAL(progress(int)), q, SLOT(onIndexingProgress(int))); + q->connect(DICOMIndexer.data(), SIGNAL(progressStep(QString)), q, SLOT(onIndexingProgressStep(QString))); + q->connect(DICOMIndexer.data(), SIGNAL(progressDetail(QString)), q, SLOT(onIndexingProgressDetail(QString))); + q->connect(DICOMIndexer.data(), SIGNAL(indexingComplete(int,int,int,int)), q, SLOT(onIndexingComplete(int,int,int,int))); + q->connect(DICOMIndexer.data(), SIGNAL(updatingDatabase(bool)), q, SLOT(onIndexingUpdatingDatabase(bool))); // Signals related to tracking inserts q->connect(this->DICOMDatabase.data(), SIGNAL(patientAdded(int,QString,QString,QString)), q, SLOT(onPatientAdded(int,QString,QString,QString))); @@ -275,9 +280,14 @@ void ctkDICOMBrowserPrivate::init() q->connect(this->DirectoryButton, SIGNAL(directoryChanged(QString)), q, SLOT(setDatabaseDirectory(QString))); + q->connect(this->SelectDatabaseDirectoryButton, SIGNAL(clicked()), q, SLOT(selectDatabaseDirectory())); + q->connect(this->CreateNewDatabaseButton, SIGNAL(clicked()), q, SLOT(createNewDatabaseDirectory())); + q->connect(this->UpdateDatabaseButton, SIGNAL(clicked()), q, SLOT(updateDatabase())); + + q->connect(this->InformationMessageDismissButton, SIGNAL(clicked()), InformationMessageFrame, SLOT(hide())); + // Signal for displayed fields update q->connect(this->DICOMDatabase.data(), SIGNAL(displayedFieldsUpdateStarted()), q, SLOT(showUpdateDisplayedFieldsDialog())); - q->connect(this->DICOMIndexer.data(), SIGNAL(displayedFieldsUpdateStarted()), q, SLOT(showUpdateDisplayedFieldsDialog())); // Set ToolBar button style this->ToolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); @@ -285,30 +295,25 @@ void ctkDICOMBrowserPrivate::init() // Initialize Q/R widget this->QueryRetrieveWidget = new ctkDICOMQueryRetrieveWidget(); this->QueryRetrieveWidget->setWindowModality ( Qt::ApplicationModal ); - - // Initialize directory from settings, then listen for changes - QSettings settings; - if ( settings.value(this->DatabaseDirectorySettingsKey, "") == "" ) - { - settings.setValue(this->DatabaseDirectorySettingsKey, this->DefaultDatabasePath); - settings.sync(); - } - QString databaseDirectory = q->databaseDirectory(); - q->setDatabaseDirectory(databaseDirectory); - databaseDirectory = q->databaseDirectory(); // In case a new database has been created instead of updating schema in place - bool wasBlocked = this->DirectoryButton->blockSignals(true); - this->DirectoryButton->setDirectory(databaseDirectory); - this->DirectoryButton->blockSignals(wasBlocked); - + this->QueryRetrieveWidget->useProgressDialog(true); + this->dicomTableManager->setDICOMDatabase(this->DICOMDatabase.data()); // TableView signals q->connect(this->dicomTableManager, SIGNAL(patientsSelectionChanged(const QItemSelection&, const QItemSelection&)), - q, SLOT(onModelSelected(const QItemSelection&,const QItemSelection&))); + this->InformationMessageFrame, SLOT(hide())); q->connect(this->dicomTableManager, SIGNAL(studiesSelectionChanged(const QItemSelection&, const QItemSelection&)), - q, SLOT(onModelSelected(const QItemSelection&,const QItemSelection&))); + this->InformationMessageFrame, SLOT(hide())); q->connect(this->dicomTableManager, SIGNAL(seriesSelectionChanged(const QItemSelection&, const QItemSelection&)), - q, SLOT(onModelSelected(const QItemSelection&,const QItemSelection&))); + this->InformationMessageFrame, SLOT(hide())); + + q->connect(this->dicomTableManager, SIGNAL(patientsSelectionChanged(const QItemSelection&, const QItemSelection&)), + q, SLOT(onModelSelected(const QItemSelection&, const QItemSelection&))); + q->connect(this->dicomTableManager, SIGNAL(studiesSelectionChanged(const QItemSelection&, const QItemSelection&)), + q, SLOT(onModelSelected(const QItemSelection&, const QItemSelection&))); + q->connect(this->dicomTableManager, SIGNAL(seriesSelectionChanged(const QItemSelection&, const QItemSelection&)), + q, SLOT(onModelSelected(const QItemSelection&, const QItemSelection&))); + // set up context menus for working on selected patients, studies, series q->connect(this->dicomTableManager, SIGNAL(patientsRightClicked(const QPoint&)), @@ -345,6 +350,10 @@ void ctkDICOMBrowserPrivate::init() this->ImportDialog->setWindowTitle("Import DICOM files from directory ..."); this->ImportDialog->setWindowModality(Qt::ApplicationModal); + this->MetadataDialog = new ctkDICOMMetadataDialog(); + this->MetadataDialog->setObjectName("DICOMMetadata"); + this->MetadataDialog->setWindowTitle("DICOM File Metadata"); + //connect signal and slots q->connect(this->ImportDialog, SIGNAL(filesSelected(QStringList)), q,SLOT(onImportDirectoriesSelected(QStringList))); @@ -360,9 +369,18 @@ void ctkDICOMBrowserPrivate::init() // ctkDICOMBrowser methods //---------------------------------------------------------------------------- -ctkDICOMBrowser::ctkDICOMBrowser(QWidget* _parent, QString databaseDirectorySettingsKey) +ctkDICOMBrowser::ctkDICOMBrowser(QWidget* _parent) : Superclass(_parent), - d_ptr(new ctkDICOMBrowserPrivate(this, databaseDirectorySettingsKey)) + d_ptr(new ctkDICOMBrowserPrivate(this, QSharedPointer())) +{ + Q_D(ctkDICOMBrowser); + d->init(); +} + +//---------------------------------------------------------------------------- +ctkDICOMBrowser::ctkDICOMBrowser(QSharedPointer sharedDatabase, QWidget* _parent) + : Superclass(_parent), + d_ptr(new ctkDICOMBrowserPrivate(this, sharedDatabase)) { Q_D(ctkDICOMBrowser); d->init(); @@ -375,6 +393,7 @@ ctkDICOMBrowser::~ctkDICOMBrowser() d->QueryRetrieveWidget->deleteLater(); d->ImportDialog->deleteLater(); + d->MetadataDialog->deleteLater(); } //---------------------------------------------------------------------------- @@ -409,22 +428,6 @@ void ctkDICOMBrowser::setConfirmRemove(bool onOff) d->ConfirmRemove = onOff; } -//---------------------------------------------------------------------------- -bool ctkDICOMBrowser::schemaUpdateAutoCreateDirectory() -{ - Q_D(ctkDICOMBrowser); - - return d->ShemaUpdateAutoCreateDirectory; -} - -//---------------------------------------------------------------------------- -void ctkDICOMBrowser::setShemaUpdateAutoCreateDirectory(bool onOff) -{ - Q_D(ctkDICOMBrowser); - - d->ShemaUpdateAutoCreateDirectory = onOff; -} - //---------------------------------------------------------------------------- int ctkDICOMBrowser::patientsAddedDuringImport() { @@ -458,171 +461,205 @@ int ctkDICOMBrowser::instancesAddedDuringImport() } //---------------------------------------------------------------------------- -QString ctkDICOMBrowser::updateDatabaseSchemaIfNeeded() +void ctkDICOMBrowser::createNewDatabaseDirectory() { Q_D(ctkDICOMBrowser); - if (d->DICOMDatabase->schemaVersionLoaded() == d->DICOMDatabase->schemaVersion()) + // Use the current database folder as a basis for the new name + QString baseFolder = this->databaseDirectory(); + if (baseFolder.isEmpty()) { - // Return if no update is needed (empty string means no new database has been set) - return QString(); + baseFolder = d->DefaultDatabaseDirectory; } - - ctkDICOMBrowser::SchemaUpdateOption updateOption = this->schemaUpdateOption(); - bool updateSchema = (updateOption == ctkDICOMBrowser::AlwaysUpdate); - if (updateOption == ctkDICOMBrowser::AskUser) - { - QString messageText = QString("DICOM database at location (%1) is incompatible with this version of the software.\n" - "Updating the database may take several minutes.\n\nAlternatively you may create a new, empty database (the old one will not be modified).") - .arg(this->databaseDirectory()); - ctkMessageBox schemaUpdateMsgBox; - schemaUpdateMsgBox.setWindowTitle(tr("DICOM database update")); - schemaUpdateMsgBox.setText(messageText); - QPushButton* updateButton = schemaUpdateMsgBox.addButton(tr(" Update database "), QMessageBox::AcceptRole); - QPushButton* createButton = schemaUpdateMsgBox.addButton(tr(" Choose different folder "), QMessageBox::RejectRole); - schemaUpdateMsgBox.setDefaultButton(updateButton); - schemaUpdateMsgBox.exec(); - if (schemaUpdateMsgBox.clickedButton() == updateButton) + else + { + // only use existing folder name as a basis if it is empty or + // a valid database + if (!QDir(baseFolder).isEmpty()) { - updateSchema = true; + QString databaseFileName = baseFolder + QString("/ctkDICOM.sql"); + if (!QFile(databaseFileName).exists()) + { + // current folder is a non-empty and not a DICOM database folder + // create a subfolder for the new DICOM database based on the name + // of default database path + QFileInfo defaultFolderInfo(d->DefaultDatabaseDirectory); + QString defaultSubfolderName = defaultFolderInfo.fileName(); + if (defaultSubfolderName.isEmpty()) + { + defaultSubfolderName = defaultFolderInfo.dir().dirName(); + } + baseFolder += "/" + defaultSubfolderName; + } } } - - QString dir; - if (d->ShemaUpdateAutoCreateDirectory) + // Remove existing numerical suffix + QString separator = "_"; + bool isSuffixValid = false; + QString suffixStr = baseFolder.split(separator).last(); + int suffixStart = suffixStr.toInt(&isSuffixValid); + if (isSuffixValid) { - // Auto-generate new database folder name - QString newDatabaseDirPath = this->databaseDirectory(); - newDatabaseDirPath.append("-"); - newDatabaseDirPath.append(d->DICOMDatabase->schemaVersion()); - dir = newDatabaseDirPath; + QStringList baseFolderComponents = baseFolder.split(separator); + baseFolderComponents.removeLast(); + baseFolder = baseFolderComponents.join(separator); } - else + // Try folder names, starting with the current one, + // incrementing the original numerical suffix. + int attemptsCount = 100; + for (int attempt=0; attemptDirectoryButton->browse() because it will cause circular calls) - - // See https://bugreports.qt-project.org/browse/QTBUG-10244 - class ExcludeReadOnlyFilterProxyModel : public QSortFilterProxyModel + QString newFolder = baseFolder; + int suffix = (suffixStart + attempt) % attemptsCount; + if (suffix) { - public: - ExcludeReadOnlyFilterProxyModel(QPalette palette, QObject *parent) - : QSortFilterProxyModel(parent) - , Palette(palette) - { - } - virtual Qt::ItemFlags flags(const QModelIndex& index)const + newFolder += separator + QString::number(suffix); + } + if (!QDir(newFolder).exists()) + { + if (!QDir().mkpath(newFolder)) { - QString filePath = - this->sourceModel()->data(this->mapToSource(index), QFileSystemModel::FilePathRole).toString(); - if (!QFileInfo(filePath).isWritable()) - { - // Double clickable (to open) but can't be "chosen". - return Qt::ItemIsSelectable; - } - return this->QSortFilterProxyModel::flags(index); + continue; } - QPalette Palette; - }; - - QScopedPointer fileDialog( - new ctkFileDialog(this, "Choose existing database / Select empty folder for new DICOM database", this->databaseDirectory())); -#ifdef USE_QFILEDIALOG_OPTIONS - fileDialog->setOptions(QFileDialog::ShowDirsOnly; -#else - fileDialog->setOptions(QFlags(int(ctkDirectoryButton::ShowDirsOnly))); -#endif - fileDialog->setAcceptMode(QFileDialog::AcceptSave); - fileDialog->setFileMode(QFileDialog::DirectoryOnly); - // Gray out the non-writable folders. They can still be opened with double click, - // but they can't be selected because they don't have the ItemIsEnabled - // flag and because ctkFileDialog would not let it to be selected. - fileDialog->setProxyModel( - new ExcludeReadOnlyFilterProxyModel(this->palette(), fileDialog.data())); - - if (fileDialog->exec()) - { - dir = fileDialog->selectedFiles().at(0); } - // An empty directory means either that the user canceled the dialog or the selected directory is readonly - if (dir.isEmpty()) + if (!QDir(newFolder).isEmpty()) { - qCritical() << Q_FUNC_INFO << ": Either user canceled database folder dialog or the selected directory is readonly"; - return QString(); + continue; } + // Folder exists and empty, try to use this + setDatabaseDirectory(newFolder); + return; } + std::cerr << "Failed to create new database in folder: " << qPrintable(baseFolder) << "\n"; + d->InformationMessageFrame->hide(); + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText(tr("Failed to create new database in folder %1.").arg(QDir(baseFolder).absolutePath())); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); +} - if (updateSchema) +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::updateDatabase() +{ + Q_D(ctkDICOMBrowser); + d->InformationMessageFrame->hide(); + d->DatabaseDirectoryProblemFrame->hide(); + d->showUpdateSchemaDialog(); + QString dir = this->databaseDirectory(); + // open DICOM database on the directory + QString databaseFileName = dir + QString("/ctkDICOM.sql"); + try { - d->showUpdateSchemaDialog(); - d->DICOMDatabase->updateSchema(":/dicom/dicom-schema.sql", dir.toLatin1().constData()); + d->DICOMDatabase->openDatabase(databaseFileName); } - - return dir; + catch (std::exception e) + { + std::cerr << "Database error: " << qPrintable(d->DICOMDatabase->lastError()) << "\n"; + d->DICOMDatabase->closeDatabase(); + return; + } + d->DICOMDatabase->updateSchema(); + // Update GUI + this->setDatabaseDirectory(dir); } //---------------------------------------------------------------------------- void ctkDICOMBrowser::setDatabaseDirectory(const QString& directory) { Q_D(ctkDICOMBrowser); + d->InformationMessageFrame->hide(); - // If needed, create database directory - if (!QDir(directory).exists()) - { - QDir().mkdir(directory); - } + QString absDirectory = QDir(directory).absolutePath(); // close the active DICOM database d->DICOMDatabase->closeDatabase(); // open DICOM database on the directory QString databaseFileName = directory + QString("/ctkDICOM.sql"); - try + + bool success = true; + + if (!QDir(directory).exists() + || (!QDir(directory).isEmpty() && !QFile(databaseFileName).exists())) { - d->DICOMDatabase->openDatabase( databaseFileName ); - } - catch (std::exception e) - { - std::cerr << "Database error: " << qPrintable(d->DICOMDatabase->lastError()) << "\n"; - d->DICOMDatabase->closeDatabase(); - return; + std::cerr << "Database folder does not contain ctkDICOM.sql file: " << qPrintable(absDirectory) << "\n"; + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText(tr("No valid DICOM database found in folder %1.").arg(absDirectory)); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); + success = false; } - // update the database schema if needed and provide progress - QString updatedDatabaseDirectory = this->updateDatabaseSchemaIfNeeded(); - if (!updatedDatabaseDirectory.isEmpty()) + if (success) { - // close the active DICOM database, which needed to be updated - d->DICOMDatabase->closeDatabase(); - - // open DICOM database on the directory - QString updatedDatabaseFileName = updatedDatabaseDirectory + QString("/ctkDICOM.sql"); + bool databaseOpenSuccess = false; try { - d->DICOMDatabase->openDatabase( updatedDatabaseFileName ); + d->DICOMDatabase->openDatabase(databaseFileName); + databaseOpenSuccess = d->DICOMDatabase->isOpen(); } catch (std::exception e) + { + databaseOpenSuccess = false; + } + if (!databaseOpenSuccess || d->DICOMDatabase->schemaVersionLoaded().isEmpty()) { std::cerr << "Database error: " << qPrintable(d->DICOMDatabase->lastError()) << "\n"; d->DICOMDatabase->closeDatabase(); - return; + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText(tr("No valid DICOM database found in folder %1.").arg(absDirectory)); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); + success = false; + } + } + + if (success) + { + if (d->DICOMDatabase->schemaVersionLoaded() != d->DICOMDatabase->schemaVersion()) + { + std::cerr << "Database version mismatch: version of selected database = " + << qPrintable(d->DICOMDatabase->schemaVersionLoaded()) + << ", version required = " << qPrintable(d->DICOMDatabase->schemaVersion()) << "\n"; + d->DICOMDatabase->closeDatabase(); + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + tr("Incompatible DICOM database version found in folder %1.").arg(absDirectory)); + d->UpdateDatabaseButton->show(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); + success = false; } } - QString currentDatabaseDirectory(!updatedDatabaseDirectory.isEmpty() ? updatedDatabaseDirectory : directory); + if (success) + { + d->DatabaseDirectoryProblemFrame->hide(); + } - // Save new database directory in settings. + // Save new database directory in this object and in application settings. + d->DatabaseDirectory = directory; QSettings settings; - settings.setValue(Self::databaseDirectorySettingsKey(), currentDatabaseDirectory); - settings.sync(); + if (!d->DatabaseDirectorySettingsKey.isEmpty()) + { + settings.setValue(d->DatabaseDirectorySettingsKey, directory); + settings.sync(); + } // pass DICOM database instance to Import widget d->QueryRetrieveWidget->setRetrieveDatabase(d->DICOMDatabase); // update the button and let any connected slots know about the change - d->DirectoryButton->setDirectory(currentDatabaseDirectory); + bool wasBlocked = d->DirectoryButton->blockSignals(true); + d->DirectoryButton->setDirectory(directory); + d->DirectoryButton->blockSignals(wasBlocked); + d->dicomTableManager->updateTableViews(); - emit databaseDirectoryChanged(currentDatabaseDirectory); + + emit databaseDirectoryChanged(directory); } //---------------------------------------------------------------------------- @@ -631,7 +668,7 @@ QString ctkDICOMBrowser::databaseDirectory() const Q_D(const ctkDICOMBrowser); // If override settings is specified then try to get database directory from there first - return QSettings().value(this->databaseDirectorySettingsKey()).toString(); + return d->DatabaseDirectory; } //------------------------------------------------------------------------------ @@ -641,6 +678,17 @@ QString ctkDICOMBrowser::databaseDirectorySettingsKey() const return d->DatabaseDirectorySettingsKey; } +//------------------------------------------------------------------------------ +void ctkDICOMBrowser::setDatabaseDirectorySettingsKey(const QString& key) +{ + Q_D(ctkDICOMBrowser); + d->DatabaseDirectorySettingsKey = key; + + QSettings settings; + QString databaseDirectory = settings.value(d->DatabaseDirectorySettingsKey, "").toString(); + this->setDatabaseDirectory(databaseDirectory); +} + //------------------------------------------------------------------------------ void ctkDICOMBrowser::setTagsToPrecache( const QStringList tags) { @@ -656,7 +704,8 @@ const QStringList ctkDICOMBrowser::tagsToPrecache() } //---------------------------------------------------------------------------- -ctkDICOMDatabase* ctkDICOMBrowser::database(){ +ctkDICOMDatabase* ctkDICOMBrowser::database() +{ Q_D(ctkDICOMBrowser); return d->DICOMDatabase.data(); } @@ -668,12 +717,6 @@ ctkDICOMTableManager* ctkDICOMBrowser::dicomTableManager() return d->dicomTableManager; } -//---------------------------------------------------------------------------- -void ctkDICOMBrowser::onFileIndexed(const QString& filePath) -{ - Q_UNUSED(filePath); -} - //---------------------------------------------------------------------------- void ctkDICOMBrowser::openImportDialog() { @@ -686,7 +729,15 @@ void ctkDICOMBrowser::openImportDialog() //---------------------------------------------------------------------------- void ctkDICOMBrowser::openExportDialog() { + // Export selected series + this->exportSelectedItems(ctkDICOMModel::SeriesType); +} +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::openSendDialog() +{ + // Export selected series + emit sendRequested(this->fileListForCurrentSelection(ctkDICOMModel::SeriesType)); } //---------------------------------------------------------------------------- @@ -708,31 +759,7 @@ void ctkDICOMBrowser::onQueryRetrieveFinished() void ctkDICOMBrowser::onRemoveAction() { Q_D(ctkDICOMBrowser); - - QStringList selectedPatientUIDs = d->dicomTableManager->currentPatientsSelection(); - - // Confirm removal if needed. Note that this function always removes the patient - if (d->ConfirmRemove && !this->confirmDeleteSelectedUIDs(selectedPatientUIDs)) - { - return; - } - - QStringList selectedSeriesUIDs = d->dicomTableManager->currentSeriesSelection(); - foreach (const QString& uid, selectedSeriesUIDs) - { - d->DICOMDatabase->removeSeries(uid); - } - QStringList selectedStudiesUIDs = d->dicomTableManager->currentStudiesSelection(); - foreach (const QString& uid, selectedStudiesUIDs) - { - d->DICOMDatabase->removeStudy(uid); - } - foreach (const QString& uid, selectedPatientUIDs) - { - d->DICOMDatabase->removePatient(uid); - } - // Update the table views - d->dicomTableManager->updateTableViews(); + this->removeSelectedItems(ctkDICOMModel::SeriesType); } //---------------------------------------------------------------------------- @@ -763,6 +790,10 @@ void ctkDICOMBrowser::onRepairAction() if (corruptedSeries.size() == 0) { + bool wasBatchUpdate = d->dicomTableManager->setBatchUpdate(true); + d->DICOMDatabase->updateDisplayedFields(); + d->dicomTableManager->setBatchUpdate(wasBatchUpdate); + repairMessageBox->setText("All the files in the local database are available."); repairMessageBox->addButton(QMessageBox::Ok); repairMessageBox->exec(); @@ -773,7 +804,7 @@ void ctkDICOMBrowser::onRepairAction() repairMessageBox->addButton(QMessageBox::No); QSet::iterator i; for (i = corruptedSeries.begin(); i != corruptedSeries.end(); ++i) - { + { QStringList fileList (d->DICOMDatabase->filesForSeries(*i)); QString unavailableFileNames; QStringList::const_iterator it; @@ -800,7 +831,14 @@ void ctkDICOMBrowser::onRepairAction() d->dicomTableManager->updateTableViews(); } } + + bool wasBatchUpdate = d->dicomTableManager->setBatchUpdate(true); + d->DICOMDatabase->updateDisplayedFields(); + d->dicomTableManager->setBatchUpdate(wasBatchUpdate); } + + // Force refresh of table views + d->DICOMDatabase->databaseChanged(); } //---------------------------------------------------------------------------- @@ -863,38 +901,22 @@ void ctkDICOMBrowser::onImportDirectoryComboBoxCurrentIndexChanged(int index) void ctkDICOMBrowser::importDirectories(QStringList directories, ctkDICOMBrowser::ImportDirectoryMode mode) { Q_D(ctkDICOMBrowser); - ctkDICOMImportStats stats(d); - if (!d->DICOMDatabase || !d->DICOMIndexer) { qWarning() << Q_FUNC_INFO << " failed: database or indexer is invalid"; return; } - - // Only emit one indexingComplete event, when all imports have been completed - ctkDICOMIndexer::ScopedIndexing indexingBatch(*d->DICOMIndexer, *d->DICOMDatabase); - foreach (const QString& directory, directories) { d->importDirectory(directory, mode); } - - if (d->DisplayImportSummary) - { - QMessageBox::information(d->ImportDialog,"DICOM Directory Import", stats.summary()); - } } //---------------------------------------------------------------------------- void ctkDICOMBrowser::importDirectory(QString directory, ctkDICOMBrowser::ImportDirectoryMode mode) { Q_D(ctkDICOMBrowser); - ctkDICOMImportStats stats(d); d->importDirectory(directory, mode); - if (d->DisplayImportSummary) - { - QMessageBox::information(d->ImportDialog,"DICOM Directory Import", stats.summary()); - } } //---------------------------------------------------------------------------- @@ -903,6 +925,13 @@ void ctkDICOMBrowser::onImportDirectory(QString directory, ctkDICOMBrowser::Impo this->importDirectory(directory, mode); } +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::waitForImportFinished() +{ + Q_D(ctkDICOMBrowser); + d->DICOMIndexer->waitForImportFinished(); +} + //---------------------------------------------------------------------------- void ctkDICOMBrowserPrivate::importDirectory(QString directory, ctkDICOMBrowser::ImportDirectoryMode mode) { @@ -917,9 +946,8 @@ void ctkDICOMBrowserPrivate::importDirectory(QString directory, ctkDICOMBrowser: targetDirectory = this->DICOMDatabase->databaseDirectory(); } - // show progress dialog and perform indexing - this->showIndexerDialog(); - this->DICOMIndexer->addDirectory(*this->DICOMDatabase, directory, targetDirectory); + // Start background indexing + this->DICOMIndexer->addDirectory(directory, mode == ctkDICOMBrowser::ImportDirectoryCopy); } //---------------------------------------------------------------------------- @@ -968,54 +996,6 @@ void ctkDICOMBrowser::setImportDirectoryMode(ctkDICOMBrowser::ImportDirectoryMod comboBox->setCurrentIndex(comboBox->findData(mode)); } -//---------------------------------------------------------------------------- -ctkDICOMBrowser::SchemaUpdateOption ctkDICOMBrowser::schemaUpdateOption()const -{ - Q_D(const ctkDICOMBrowser); - QSettings settings; - return ctkDICOMBrowser::schemaUpdateOptionFromString( - settings.value("DICOM/SchemaUpdateOption", ctkDICOMBrowser::schemaUpdateOptionToString(ctkDICOMBrowser::AlwaysUpdate)).toString() ); -} - -//---------------------------------------------------------------------------- -void ctkDICOMBrowser::setSchemaUpdateOption(ctkDICOMBrowser::SchemaUpdateOption option) -{ - Q_D(ctkDICOMBrowser); - - QSettings settings; - settings.setValue("DICOM/SchemaUpdateOption", ctkDICOMBrowser::schemaUpdateOptionToString(option)); -} - -//---------------------------------------------------------------------------- -ctkDICOMBrowser::SchemaUpdateOption ctkDICOMBrowser::schemaUpdateOptionFromString(QString option) -{ - if (option == "NeverUpdate") - { - return ctkDICOMBrowser::NeverUpdate; - } - else if (option == "AskUser") - { - return ctkDICOMBrowser::AskUser; - } - - // AlwaysUpdate is the default - return ctkDICOMBrowser::AlwaysUpdate; -} - -//---------------------------------------------------------------------------- -QString ctkDICOMBrowser::schemaUpdateOptionToString(ctkDICOMBrowser::SchemaUpdateOption option) -{ - switch (option) - { - case ctkDICOMBrowser::NeverUpdate: - return "NeverUpdate"; - case ctkDICOMBrowser::AskUser: - return "AskUser"; - default: - return "AlwaysUpdate"; - } -} - //---------------------------------------------------------------------------- void ctkDICOMBrowser::onModelSelected(const QItemSelection &item1, const QItemSelection &item2) { @@ -1023,6 +1003,8 @@ void ctkDICOMBrowser::onModelSelected(const QItemSelection &item1, const QItemSe Q_UNUSED(item2); Q_D(ctkDICOMBrowser); d->ActionRemove->setEnabled(true); + d->ActionSend->setEnabled(true); + d->ActionExport->setEnabled(true); } //---------------------------------------------------------------------------- @@ -1101,49 +1083,54 @@ void ctkDICOMBrowser::onPatientsRightClicked(const QPoint &point) } QMenu *patientsMenu = new QMenu(d->dicomTableManager); + QString selectedPatientsCount; + if (numPatients > 1) + { + selectedPatientsCount = QString(" %1 selected patients").arg(numPatients); + } - QString deleteString = QString("Delete ") - + QString::number(numPatients) - + QString(" selected patients"); - QAction *deleteAction = new QAction(deleteString, patientsMenu); + QString metadataString = QString("View DICOM metadata"); + if (numPatients > 1) + { + metadataString += QString(" of") + selectedPatientsCount; + } + QAction *metadataAction = new QAction(metadataString, patientsMenu); + patientsMenu->addAction(metadataAction); + QString deleteString = QString("Delete") + selectedPatientsCount; + QAction *deleteAction = new QAction(deleteString, patientsMenu); patientsMenu->addAction(deleteAction); - QString exportString = QString("Export ") - + QString::number(numPatients) - + QString(" selected patients to file system"); + QString exportString = QString("Export%1 to file system").arg(selectedPatientsCount); QAction *exportAction = new QAction(exportString, patientsMenu); - patientsMenu->addAction(exportAction); + QString sendString = QString("Send%1 to DICOM server").arg(selectedPatientsCount); + QAction* sendAction = new QAction(sendString, patientsMenu); + if (this->isSendActionVisible()) + { + patientsMenu->addAction(sendAction); + } + // the table took care of mapping it to a global position so that the // menu will pop up at the correct place over this table. QAction *selectedAction = patientsMenu->exec(point); - if (selectedAction == deleteAction - && this->confirmDeleteSelectedUIDs(selectedPatientsUIDs)) + if (selectedAction == metadataAction) { - qDebug() << "Deleting " << numPatients << " patients"; - foreach (const QString& uid, selectedPatientsUIDs) - { - d->DICOMDatabase->removePatient(uid); - d->dicomTableManager->updateTableViews(); - } + this->showMetadata(this->fileListForCurrentSelection(ctkDICOMModel::PatientType)); + } + else if (selectedAction == deleteAction) + { + this->removeSelectedItems(ctkDICOMModel::PatientType); } else if (selectedAction == exportAction) { - ctkFileDialog* directoryDialog = new ctkFileDialog(); - directoryDialog->setOption(QFileDialog::DontUseNativeDialog); - directoryDialog->setOption(QFileDialog::ShowDirsOnly); - directoryDialog->setFileMode(QFileDialog::DirectoryOnly); - bool res = directoryDialog->exec(); - if (res) - { - QStringList dirs = directoryDialog->selectedFiles(); - QString dirPath = dirs[0]; - this->exportSelectedPatients(dirPath, selectedPatientsUIDs); - } - delete directoryDialog; + this->exportSelectedItems(ctkDICOMModel::PatientType); + } + else if (selectedAction == sendAction) + { + emit sendRequested(this->fileListForCurrentSelection(ctkDICOMModel::PatientType)); } } @@ -1162,26 +1149,44 @@ void ctkDICOMBrowser::onStudiesRightClicked(const QPoint &point) } QMenu *studiesMenu = new QMenu(d->dicomTableManager); + QString selectedStudiesCount; + if (numStudies > 1) + { + selectedStudiesCount = QString(" %1 selected studies").arg(numStudies); + } - QString deleteString = QString("Delete ") - + QString::number(numStudies) - + QString(" selected studies"); - QAction *deleteAction = new QAction(deleteString, studiesMenu); + QString metadataString = QString("View DICOM metadata"); + if (numStudies > 1) + { + metadataString += QString(" of") + selectedStudiesCount; + } + QAction *metadataAction = new QAction(metadataString, studiesMenu); + studiesMenu->addAction(metadataAction); + QString deleteString = QString("Delete") + selectedStudiesCount; + QAction *deleteAction = new QAction(deleteString, studiesMenu); studiesMenu->addAction(deleteAction); - QString exportString = QString("Export ") - + QString::number(numStudies) - + QString(" selected studies to file system"); + QString exportString = QString("Export%1 to file system").arg(selectedStudiesCount); QAction *exportAction = new QAction(exportString, studiesMenu); - studiesMenu->addAction(exportAction); + QString sendString = QString("Send%1 to DICOM server").arg(selectedStudiesCount); + QAction* sendAction = new QAction(sendString, studiesMenu); + if (this->isSendActionVisible()) + { + studiesMenu->addAction(sendAction); + } + // the table took care of mapping it to a global position so that the // menu will pop up at the correct place over this table. QAction *selectedAction = studiesMenu->exec(point); - if (selectedAction == deleteAction + if (selectedAction == metadataAction) + { + this->showMetadata(this->fileListForCurrentSelection(ctkDICOMModel::StudyType)); + } + else if (selectedAction == deleteAction && this->confirmDeleteSelectedUIDs(selectedStudiesUIDs)) { foreach (const QString& uid, selectedStudiesUIDs) @@ -1192,18 +1197,11 @@ void ctkDICOMBrowser::onStudiesRightClicked(const QPoint &point) } else if (selectedAction == exportAction) { - ctkFileDialog* directoryDialog = new ctkFileDialog(); - directoryDialog->setOption(QFileDialog::DontUseNativeDialog); - directoryDialog->setOption(QFileDialog::ShowDirsOnly); - directoryDialog->setFileMode(QFileDialog::DirectoryOnly); - bool res = directoryDialog->exec(); - if (res) - { - QStringList dirs = directoryDialog->selectedFiles(); - QString dirPath = dirs[0]; - this->exportSelectedStudies(dirPath, selectedStudiesUIDs); - } - delete directoryDialog; + this->exportSelectedItems(ctkDICOMModel::StudyType); + } + else if (selectedAction == sendAction) + { + emit sendRequested(this->fileListForCurrentSelection(ctkDICOMModel::StudyType)); } } @@ -1222,25 +1220,44 @@ void ctkDICOMBrowser::onSeriesRightClicked(const QPoint &point) } QMenu *seriesMenu = new QMenu(d->dicomTableManager); + QString selectedSeriesCount; + if (numSeries > 1) + { + selectedSeriesCount = QString(" %1 selected series").arg(numSeries); - QString deleteString = QString("Delete ") - + QString::number(numSeries) - + QString(" selected series"); - QAction *deleteAction = new QAction(deleteString, seriesMenu); + } + QString metadataString = QString("View DICOM metadata"); + if (numSeries > 1) + { + metadataString += QString(" of") + selectedSeriesCount; + } + QAction *metadataAction = new QAction(metadataString, seriesMenu); + seriesMenu->addAction(metadataAction); + QString deleteString = QString("Delete") + selectedSeriesCount; + QAction *deleteAction = new QAction(deleteString, seriesMenu); seriesMenu->addAction(deleteAction); - QString exportString = QString("Export ") - + QString::number(numSeries) - + QString(" selected series to file system"); + QString exportString = QString("Export%1 to file system").arg(selectedSeriesCount); QAction *exportAction = new QAction(exportString, seriesMenu); seriesMenu->addAction(exportAction); + QString sendString = QString("Send%1 to DICOM server").arg(selectedSeriesCount); + QAction* sendAction = new QAction(sendString, seriesMenu); + if (this->isSendActionVisible()) + { + seriesMenu->addAction(sendAction); + } + // the table took care of mapping it to a global position so that the // menu will pop up at the correct place over this table. QAction *selectedAction = seriesMenu->exec(point); - if (selectedAction == deleteAction + if (selectedAction == metadataAction) + { + this->showMetadata(this->fileListForCurrentSelection(ctkDICOMModel::SeriesType)); + } + else if (selectedAction == deleteAction && this->confirmDeleteSelectedUIDs(selectedSeriesUIDs)) { foreach (const QString& uid, selectedSeriesUIDs) @@ -1251,23 +1268,17 @@ void ctkDICOMBrowser::onSeriesRightClicked(const QPoint &point) } else if (selectedAction == exportAction) { - ctkFileDialog* directoryDialog = new ctkFileDialog(); - directoryDialog->setOption(QFileDialog::DontUseNativeDialog); - directoryDialog->setOption(QFileDialog::ShowDirsOnly); - directoryDialog->setFileMode(QFileDialog::DirectoryOnly); - bool res = directoryDialog->exec(); - if (res) - { - QStringList dirs = directoryDialog->selectedFiles(); - QString dirPath = dirs[0]; - this->exportSelectedSeries(dirPath, selectedSeriesUIDs); - } - delete directoryDialog; + this->exportSelectedItems(ctkDICOMModel::SeriesType); + } + else if (selectedAction == sendAction) + { + emit sendRequested(this->fileListForCurrentSelection(ctkDICOMModel::SeriesType)); } + } //---------------------------------------------------------------------------- -void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) +void ctkDICOMBrowser::exportSeries(QString dirPath, QStringList uids) { Q_D(ctkDICOMBrowser); @@ -1408,30 +1419,6 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) } } -//---------------------------------------------------------------------------- -void ctkDICOMBrowser::exportSelectedStudies(QString dirPath, QStringList uids) -{ - Q_D(ctkDICOMBrowser); - - foreach (const QString& uid, uids) - { - QStringList seriesUIDs = d->DICOMDatabase->seriesForStudy(uid); - this->exportSelectedSeries(dirPath, seriesUIDs); - } -} - -//---------------------------------------------------------------------------- -void ctkDICOMBrowser::exportSelectedPatients(QString dirPath, QStringList uids) -{ - Q_D(ctkDICOMBrowser); - - foreach (const QString& uid, uids) - { - QStringList studiesUIDs = d->DICOMDatabase->studiesForPatient(uid); - this->exportSelectedStudies(dirPath, studiesUIDs); - } -} - //---------------------------------------------------------------------------- void ctkDICOMBrowser::showUpdateDisplayedFieldsDialog() { @@ -1456,3 +1443,273 @@ void ctkDICOMBrowser::showUpdateDisplayedFieldsDialog() d->UpdateDisplayedFieldsProgress->show(); QApplication::processEvents(); } + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::setToolbarVisible(bool state) +{ + Q_D(ctkDICOMBrowser); + d->ToolBar->setVisible(state); +} + +//---------------------------------------------------------------------------- +bool ctkDICOMBrowser::isToolbarVisible() const +{ + Q_D(const ctkDICOMBrowser); + return d->ToolBar->isVisible(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::setSendActionVisible(bool visible) +{ + Q_D(ctkDICOMBrowser); + d->SendActionVisible = visible; + d->ActionSend->setVisible(visible); +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::setDatabaseDirectorySelectorVisible(bool state) +{ + Q_D(ctkDICOMBrowser); + d->DirectoryButton->setVisible(state); +} + +//---------------------------------------------------------------------------- +bool ctkDICOMBrowser::isDatabaseDirectorySelectorVisible() const +{ + Q_D(const ctkDICOMBrowser); + return d->DirectoryButton->isVisible(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::selectDatabaseDirectory() +{ + Q_D(const ctkDICOMBrowser); + d->InformationMessageFrame->hide(); + d->DatabaseDirectoryProblemFrame->hide(); + d->DirectoryButton->browse(); +} + +//---------------------------------------------------------------------------- +QStringList ctkDICOMBrowser::fileListForCurrentSelection(ctkDICOMModel::IndexType level) +{ + Q_D(const ctkDICOMBrowser); + + QStringList selectedStudyUIDs; + if (level == ctkDICOMModel::PatientType) + { + QStringList uids = d->dicomTableManager->currentPatientsSelection(); + foreach(const QString& uid, uids) + { + selectedStudyUIDs << d->DICOMDatabase->studiesForPatient(uid); + } + } + if (level == ctkDICOMModel::StudyType) + { + selectedStudyUIDs = d->dicomTableManager->currentStudiesSelection(); + } + + QStringList selectedSeriesUIDs; + if (level == ctkDICOMModel::SeriesType) + { + selectedSeriesUIDs = d->dicomTableManager->currentSeriesSelection(); + } + else + { + foreach(const QString& uid, selectedStudyUIDs) + { + selectedSeriesUIDs << d->DICOMDatabase->seriesForStudy(uid); + } + } + + QStringList fileList; + foreach(const QString& selectedSeriesUID, selectedSeriesUIDs) + { + fileList << d->DICOMDatabase->filesForSeries(selectedSeriesUID); + } + return fileList; +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::showMetadata(const QStringList& fileList) +{ + Q_D(const ctkDICOMBrowser); + d->MetadataDialog->setFileList(fileList); + d->MetadataDialog->show(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::exportSelectedItems(ctkDICOMModel::IndexType level) +{ + Q_D(const ctkDICOMBrowser); + ctkFileDialog* directoryDialog = new ctkFileDialog(); + directoryDialog->setOption(QFileDialog::DontUseNativeDialog); + 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 selectedStudyUIDs; + if (level == ctkDICOMModel::PatientType) + { + QStringList uids = d->dicomTableManager->currentPatientsSelection(); + foreach(const QString & uid, uids) + { + selectedStudyUIDs << d->DICOMDatabase->studiesForPatient(uid); + } + } + if (level == ctkDICOMModel::StudyType) + { + selectedStudyUIDs = d->dicomTableManager->currentStudiesSelection(); + } + + QStringList selectedSeriesUIDs; + if (level == ctkDICOMModel::SeriesType) + { + selectedSeriesUIDs = d->dicomTableManager->currentSeriesSelection(); + } + else + { + foreach(const QString & uid, selectedStudyUIDs) + { + selectedSeriesUIDs << d->DICOMDatabase->seriesForStudy(uid); + } + } + + this->exportSeries(dirPath, selectedSeriesUIDs); +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::removeSelectedItems(ctkDICOMModel::IndexType level) +{ + Q_D(const ctkDICOMBrowser); + QStringList selectedPatientUIDs; + QStringList selectedStudyUIDs; + if (level == ctkDICOMModel::PatientType) + { + selectedPatientUIDs = d->dicomTableManager->currentPatientsSelection(); + if (!this->confirmDeleteSelectedUIDs(selectedPatientUIDs)) + { + return; + } + foreach(const QString & uid, selectedPatientUIDs) + { + selectedStudyUIDs << d->DICOMDatabase->studiesForPatient(uid); + } + } + if (level == ctkDICOMModel::StudyType) + { + selectedStudyUIDs = d->dicomTableManager->currentStudiesSelection(); + if (!this->confirmDeleteSelectedUIDs(selectedStudyUIDs)) + { + return; + } + } + + QStringList selectedSeriesUIDs; + if (level == ctkDICOMModel::SeriesType) + { + selectedSeriesUIDs = d->dicomTableManager->currentSeriesSelection(); + if (!this->confirmDeleteSelectedUIDs(selectedSeriesUIDs)) + { + return; + } + } + else + { + foreach(const QString & uid, selectedStudyUIDs) + { + selectedSeriesUIDs << d->DICOMDatabase->seriesForStudy(uid); + } + } + + foreach(const QString & uid, selectedSeriesUIDs) + { + d->DICOMDatabase->removeSeries(uid); + } + foreach(const QString & uid, selectedStudyUIDs) + { + d->DICOMDatabase->removeStudy(uid); + } + foreach(const QString & uid, selectedPatientUIDs) + { + d->DICOMDatabase->removePatient(uid); + } + // Update the table views + d->dicomTableManager->updateTableViews(); +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::onIndexingProgress(int percent) +{ + Q_D(const ctkDICOMBrowser); + d->ProgressFrame->show(); + d->ProgressBar->setValue(percent); +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::onIndexingProgressStep(const QString& step) +{ + Q_D(const ctkDICOMBrowser); + d->ProgressLabel->setText(step); +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::onIndexingProgressDetail(const QString& detail) +{ + Q_D(const ctkDICOMBrowser); + if (detail.isEmpty()) + { + d->ProgressDetailLineEdit->hide(); + } + else + { + d->ProgressDetailLineEdit->setText(detail); + d->ProgressDetailLineEdit->show(); + } +} + + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::onIndexingUpdatingDatabase(bool updating) +{ + Q_D(ctkDICOMBrowser); + if (updating) + { + d->BatchUpdateBeforeIndexingUpdate = d->dicomTableManager->setBatchUpdate(true); + } + else + { + d->dicomTableManager->setBatchUpdate(d->BatchUpdateBeforeIndexingUpdate); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::onIndexingComplete(int patientsAdded, int studiesAdded, int seriesAdded, int imagesAdded) +{ + Q_D(const ctkDICOMBrowser); + d->ProgressFrame->hide(); + d->ProgressDetailLineEdit->hide(); + + if (d->DisplayImportSummary) + { + QString message = QString("Import completed: added %1 patients, %2 studies, %3 series, %4 instances.") + .arg(QString::number(patientsAdded)) + .arg(QString::number(studiesAdded)) + .arg(QString::number(seriesAdded)) + .arg(QString::number(imagesAdded)); + d->InformationMessageLabel->setText(message); + d->InformationMessageFrame->show(); + } + + d->dicomTableManager->updateTableViews(); + + // allow users of this widget to know that the process has finished + emit directoryImported(); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMBrowser.h b/Libs/DICOM/Widgets/ctkDICOMBrowser.h index 9238ea8f73..6b74286902 100644 --- a/Libs/DICOM/Widgets/ctkDICOMBrowser.h +++ b/Libs/DICOM/Widgets/ctkDICOMBrowser.h @@ -27,6 +27,8 @@ #include "ctkDICOMWidgetsExport.h" +#include "ctkDICOMModel.h" + class ctkDICOMBrowserPrivate; class ctkDICOMDatabase; class ctkDICOMTableManager; @@ -45,18 +47,20 @@ class QModelIndex; /// /// Supported operations are: /// -/// * Import +/// * Import from file system /// * Export -/// * Send /// * Query -/// * Remove +/// * Send (emits signal only, requires external implementation) /// * Repair +/// * Remove +/// * Metadata /// class CTK_DICOM_WIDGETS_EXPORT ctkDICOMBrowser : 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(int patientsAddedDuringImport READ patientsAddedDuringImport) Q_PROPERTY(int studiesAddedDuringImport READ studiesAddedDuringImport) Q_PROPERTY(int seriesAddedDuringImport READ seriesAddedDuringImport) @@ -64,26 +68,29 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMBrowser : public QWidget Q_PROPERTY(QStringList tagsToPrecache READ tagsToPrecache WRITE setTagsToPrecache) Q_PROPERTY(bool displayImportSummary READ displayImportSummary WRITE setDisplayImportSummary) Q_PROPERTY(ctkDICOMBrowser::ImportDirectoryMode ImportDirectoryMode READ importDirectoryMode WRITE setImportDirectoryMode) - Q_PROPERTY(SchemaUpdateOption schemaUpdateOption READ schemaUpdateOption WRITE setSchemaUpdateOption) - Q_PROPERTY(bool schemaUpdateAutoCreateDirectory READ schemaUpdateAutoCreateDirectory WRITE setShemaUpdateAutoCreateDirectory) Q_PROPERTY(bool confirmRemove READ confirmRemove WRITE setConfirmRemove) + Q_PROPERTY(bool toolbarVisible READ isToolbarVisible WRITE setToolbarVisible) + Q_PROPERTY(bool databaseDirectorySelectorVisible READ isDatabaseDirectorySelectorVisible WRITE setDatabaseDirectorySelectorVisible) + Q_PROPERTY(bool sendActionVisible READ isSendActionVisible WRITE setSendActionVisible) public: typedef ctkDICOMBrowser Self; typedef QWidget Superclass; - /// databaseDirectorySettingsKey allows getting/setting different database folder from a custom settings key - /// This is useful if the user wants to use the old database with the older version application. - explicit ctkDICOMBrowser(QWidget* parent=0, QString databaseDirectorySettingsKey=QString()); + explicit ctkDICOMBrowser(QWidget* parent=0); + explicit ctkDICOMBrowser(QSharedPointer sharedDatabase, QWidget* parent=0); virtual ~ctkDICOMBrowser(); /// Directory being used to store the dicom database QString databaseDirectory() const; - /// Return settings key used to store the directory. - Q_INVOKABLE QString databaseDirectorySettingsKey() const; + /// Get settings key used to store DatabaseDirectory in application settings. + QString databaseDirectorySettingsKey() const; - Q_INVOKABLE static QString defaultDatabaseDirectorySettingsKey() { return QString("DatabaseDirectory"); }; + /// 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); /// See ctkDICOMDatabase for description - these accessors /// delegate to the corresponding routines of the internal @@ -92,14 +99,6 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMBrowser : public QWidget void setTagsToPrecache(const QStringList tags); const QStringList tagsToPrecache(); - /// If the schema version of the loaded database does not match the one supported, then - /// based on \sa schemaUpdateOption update the database, don't update, or ask the user. - /// Provides a dialog box for progress if updating. - /// Setting the updated database happens in \sa setDatabaseDirectory - /// \return Directory path of the updated folder (it might be a different folder). - /// Empty string if new database has not been set. - Q_INVOKABLE QString updateDatabaseSchemaIfNeeded(); - Q_INVOKABLE ctkDICOMDatabase* database(); Q_INVOKABLE ctkDICOMTableManager* dicomTableManager(); @@ -111,10 +110,6 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMBrowser : public QWidget /// Option to show dialog to confirm removal from the database (Remove action). Off by default. void setConfirmRemove(bool); bool confirmRemove(); - /// Option to determine whether the new database folder is automatically created or set by the user in a popup. - /// Automatically created folder will be ../[CurrentDatabaseFolderName]-[NewSchemaVersion]. Off by default. - void setShemaUpdateAutoCreateDirectory(bool); - bool schemaUpdateAutoCreateDirectory(); /// Accessors to status of last directory import operation int patientsAddedDuringImport(); @@ -133,27 +128,20 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMBrowser : public QWidget /// \sa setImportDirectoryMode(ctkDICOMBrowser::ImportDirectoryMode) ctkDICOMBrowser::ImportDirectoryMode importDirectoryMode()const; - /// Schema update behavior: what to do when the supported schema version is different from that of the loaded database - enum SchemaUpdateOption - { - AlwaysUpdate = 0, - NeverUpdate, - AskUser - }; - /// Get \sa SchemaUpdateOption enum from string - static ctkDICOMBrowser::SchemaUpdateOption schemaUpdateOptionFromString(QString option); - /// Get string from \sa SchemaUpdateOption enum - static QString schemaUpdateOptionToString(ctkDICOMBrowser::SchemaUpdateOption option); - - /// Get schema update option (whether to update automatically). Default is always update - /// \sa setSchemaUpdateOption - ctkDICOMBrowser::SchemaUpdateOption schemaUpdateOption()const; - /// \brief Return instance of import dialog. /// /// \internal Q_INVOKABLE ctkFileDialog* importDialog()const; + void setToolbarVisible(bool state); + bool isToolbarVisible() const; + + void setDatabaseDirectorySelectorVisible(bool visible); + bool isDatabaseDirectorySelectorVisible() const; + + void setSendActionVisible(bool visible); + bool isSendActionVisible() const; + public Q_SLOTS: /// \brief Set value of ImportDirectoryMode settings. @@ -164,12 +152,7 @@ public Q_SLOTS: /// \sa importDirectoryMode() void setImportDirectoryMode(ctkDICOMBrowser::ImportDirectoryMode mode); - /// Set schema update option (whether to update automatically). Default is always update - /// \sa schemaUpdateOption - void setSchemaUpdateOption(ctkDICOMBrowser::SchemaUpdateOption option); - void setDatabaseDirectory(const QString& directory); - void onFileIndexed(const QString& filePath); /// \brief Pop-up file dialog allowing to select and import one or multiple /// DICOM directories. @@ -181,6 +164,7 @@ public Q_SLOTS: void openImportDialog(); void openExportDialog(); + void openSendDialog(); void openQueryDialog(); void onRemoveAction(); void onRepairAction(); @@ -201,6 +185,9 @@ public Q_SLOTS: /// By default, \a mode is ImportDirectoryMode::ImportDirectoryAddLink is set. void importDirectory(QString directory, ctkDICOMBrowser::ImportDirectoryMode mode = ImportDirectoryAddLink); + /// Wait for all import operations to complete + void waitForImportFinished(); + /// \deprecated importDirectory() should be used void onImportDirectory(QString directory, ctkDICOMBrowser::ImportDirectoryMode mode = ImportDirectoryAddLink); @@ -211,9 +198,32 @@ public Q_SLOTS: void onSeriesAdded(QString); void onInstanceAdded(QString); + void onIndexingProgress(int); + void onIndexingProgressStep(const QString&); + void onIndexingProgressDetail(const QString&); + void onIndexingUpdatingDatabase(bool updating); + void onIndexingComplete(int patientsAdded, int studiesAdded, int seriesAdded, int imagesAdded); + + /// Show pop-up window for the user to select database directory + void selectDatabaseDirectory(); + + /// Create new database directory. + /// Current database directory used as a basis. + void createNewDatabaseDirectory(); + + /// Update database in-place to required schema version + void updateDatabase(); + /// Show progress dialog for update displayed fields void showUpdateDisplayedFieldsDialog(); + QStringList fileListForCurrentSelection(ctkDICOMModel::IndexType level); + + /// Show window that displays DICOM fields of all selected items + void showMetadata(const QStringList& fileList); + + void removeSelectedItems(ctkDICOMModel::IndexType level); + Q_SIGNALS: /// Emitted when directory is changed void databaseDirectoryChanged(const QString&); @@ -221,6 +231,8 @@ public Q_SLOTS: void queryRetrieveFinished(); /// Emitted when the directory import operation has completed void directoryImported(); + /// Emitted when user requested network send. String list contains list of files to be exported. + void sendRequested(const QStringList&); protected: QScopedPointer d_ptr; @@ -257,13 +269,11 @@ protected Q_SLOTS: /// Called to export the series associated with the selected UIDs /// \sa exportSelectedStudies, exportSelectedPatients - void exportSelectedSeries(QString dirPath, QStringList uids); + void exportSeries(QString dirPath, QStringList uids); + /// Called to export the studies associated with the selected UIDs /// \sa exportSelectedSeries, exportSelectedPatients - void exportSelectedStudies(QString dirPath, QStringList uids); - /// Called to export the patients associated with the selected UIDs - /// \sa exportSelectedStudies, exportSelectedSeries - void exportSelectedPatients(QString dirPath, QStringList uids); + void exportSelectedItems(ctkDICOMModel::IndexType level); /// To be called when dialog finishes void onQueryRetrieveFinished(); diff --git a/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp index de68fd8e58..d93ab3eed4 100644 --- a/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMQueryRetrieveWidget.cpp @@ -153,16 +153,29 @@ void ctkDICOMQueryRetrieveWidget::query() Q_D(ctkDICOMQueryRetrieveWidget); d->RetrieveButton->setEnabled(false); - - // create a database in memory to hold query results - try { d->QueryResultDatabase.openDatabase( ":memory:", "QUERY-DB" ); } - catch (std::exception e) + + if (!d->QueryResultDatabase.isOpen()) { - logger.error ( "Database error: " + d->QueryResultDatabase.lastError() ); - d->QueryResultDatabase.closeDatabase(); - return; + // create a database in memory to hold query results + try + { + d->QueryResultDatabase.openDatabase(":memory:"); + } + catch (std::exception e) + { + logger.error("Database error: " + d->QueryResultDatabase.lastError()); + d->QueryResultDatabase.closeDatabase(); + return; + } } + // Clear the database and set schema. + // Use a special schema that works well with fields received in query results + // and does not rely on displayed field setting. + // (Current limitation is that displayed fields cannot be computed if files + // are not inserted into the database). + d->QueryResultDatabase.initializeDatabase(":/dicom/dicom-qr-schema.sql"); + d->QueriesByStudyUID.clear(); // for each of the selected server nodes, send the query QProgressDialog progress("Query DICOM servers", "Cancel", 0, 100, this, @@ -207,7 +220,9 @@ void ctkDICOMQueryRetrieveWidget::query() this, SLOT(onQueryProgressChanged(int))); // run the query against the selected server and put results in database + bool wasBatchUpdate = d->dicomTableManager->setBatchUpdate(true); query->query ( d->QueryResultDatabase ); + d->dicomTableManager->setBatchUpdate(wasBatchUpdate); disconnect(query, SIGNAL(progress(QString)), progressLabel, SLOT(setText(QString))); @@ -238,6 +253,12 @@ void ctkDICOMQueryRetrieveWidget::query() } d->RetrieveButton->setEnabled(d->QueriesByStudyUID.keys().size() != 0); + // We would need to call database.updateDisplayedFields() now, but currently + // updateDisplayedFields requires entries in the Image table and tag cache + // and they are not set when inserting query results. + // Therefore, for now we do not compute displayed fields and use a schema + // that only shows raw DICOM values. + progress.setValue(progress.maximum()); d->ProgressDialog = 0; d->CurrentQuery = 0; @@ -356,6 +377,8 @@ void ctkDICOMQueryRetrieveWidget::retrieve() logger.info ( "Retrieve success" ); } + retrieve->database()->updateDisplayedFields(); + if(d->UseProgressDialog) { QString message(tr("Retrieve Process Finished")); diff --git a/Libs/DICOM/Widgets/ctkDICOMTableManager.cpp b/Libs/DICOM/Widgets/ctkDICOMTableManager.cpp index 7f0a236dcd..5861aa26fe 100644 --- a/Libs/DICOM/Widgets/ctkDICOMTableManager.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMTableManager.cpp @@ -43,6 +43,8 @@ class ctkDICOMTableManagerPrivate : public Ui_ctkDICOMTableManager void init(); void setDICOMDatabase(ctkDICOMDatabase *db); + void collapseTopHeader(bool collapse); + void showFilterActiveWarning(ctkSearchBox* searchBox, bool showWarning); ctkDICOMDatabase* dicomDatabase; @@ -83,6 +85,33 @@ void ctkDICOMTableManagerPrivate::init() this->seriesTable->setQueryTableName("Series"); this->seriesTable->setQueryForeignKey("StudyInstanceUID"); + this->patientsSearchBox->setAlwaysShowClearIcon(true); + this->patientsSearchBox->setShowSearchIcon(true); + QObject::connect(this->patientsSearchBox, SIGNAL(textChanged(QString)), + this->patientsTable, SLOT(setFilterText(QString))); + QObject::connect(this->patientsTable, SIGNAL(filterTextChanged(QString)), + this->patientsSearchBox, SLOT(setText(QString))); + QObject::connect(this->patientsTable, SIGNAL(showFilterActiveWarning(bool)), + q, SLOT(showPatientsFilterActiveWarning(bool))); + + this->studiesSearchBox->setAlwaysShowClearIcon(true); + this->studiesSearchBox->setShowSearchIcon(true); + QObject::connect(this->studiesSearchBox, SIGNAL(textChanged(QString)), + this->studiesTable, SLOT(setFilterText(QString))); + QObject::connect(this->studiesTable, SIGNAL(filterTextChanged(QString)), + this->studiesSearchBox, SLOT(setText(QString))); + QObject::connect(this->studiesTable, SIGNAL(showFilterActiveWarning(bool)), + q, SLOT(showStudiesFilterActiveWarning(bool))); + + this->seriesSearchBox->setAlwaysShowClearIcon(true); + this->seriesSearchBox->setShowSearchIcon(true); + QObject::connect(this->seriesSearchBox, SIGNAL(textChanged(QString)), + this->seriesTable, SLOT(setFilterText(QString))); + QObject::connect(this->seriesTable, SIGNAL(filterTextChanged(QString)), + this->seriesSearchBox, SLOT(setText(QString))); + QObject::connect(this->seriesTable, SIGNAL(showFilterActiveWarning(bool)), + q, SLOT(showSeriesFilterActiveWarning(bool))); + // For propagating patient selection changes QObject::connect(this->patientsTable, SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), q, SIGNAL(patientsSelectionChanged(const QItemSelection&, const QItemSelection&))); @@ -116,10 +145,11 @@ void ctkDICOMTableManagerPrivate::init() QObject::connect(this->seriesTable, SIGNAL(customContextMenuRequested(const QPoint&)), q, SIGNAL(seriesRightClicked(const QPoint&))); + + q->setTableOrientation(this->tableSplitter->orientation()); } //------------------------------------------------------------------------------ - void ctkDICOMTableManagerPrivate::setDICOMDatabase(ctkDICOMDatabase* db) { this->patientsTable->setDicomDataBase(db); @@ -128,6 +158,31 @@ void ctkDICOMTableManagerPrivate::setDICOMDatabase(ctkDICOMDatabase* db) this->dicomDatabase = db; } +//------------------------------------------------------------------------------ +void ctkDICOMTableManagerPrivate::collapseTopHeader(bool collapse) +{ + Q_Q(ctkDICOMTableManager); + this->patientsTable->setHeaderVisible(!collapse); + this->studiesTable->setHeaderVisible(!collapse); + this->seriesTable->setHeaderVisible(!collapse); + this->headerWidget->setVisible(collapse); +} + +//------------------------------------------------------------------------------ +void ctkDICOMTableManagerPrivate::showFilterActiveWarning(ctkSearchBox* searchBox, bool showWarning) +{ + QPalette palette; + if (showWarning) + { + palette.setColor(QPalette::Base, Qt::yellow); + } + else + { + palette.setColor(QPalette::Base, Qt::white); + } + searchBox->setPalette(palette); +} + //---------------------------------------------------------------------------- // ctkDICOMTableManager methods @@ -164,10 +219,11 @@ void ctkDICOMTableManager::setDICOMDatabase(ctkDICOMDatabase* db) } //------------------------------------------------------------------------------ -void ctkDICOMTableManager::setTableOrientation(const Qt::Orientation &o) const +void ctkDICOMTableManager::setTableOrientation(const Qt::Orientation &o) { - Q_D(const ctkDICOMTableManager); + Q_D(ctkDICOMTableManager); d->tableSplitter->setOrientation(o); + d->collapseTopHeader(o == Qt::Vertical); } //------------------------------------------------------------------------------ @@ -379,8 +435,8 @@ void ctkDICOMTableManager::resizeEvent(QResizeEvent *e) if (!d->m_DynamicTableLayout) return; - //Minimum size = 800 * 1.28 = 1024 => use horizontal layout (otherwise table size would be too small) - this->setTableOrientation(e->size().width() > 1.28*this->minimumWidth() ? Qt::Horizontal : Qt::Vertical); + // If the table is 2x wider than tall then use horizontal layout + this->setTableOrientation(e->size().width() > 2 * e->size().height() ? Qt::Horizontal : Qt::Vertical); } //------------------------------------------------------------------------------ @@ -401,3 +457,44 @@ ctkDICOMTableView* ctkDICOMTableManager::seriesTable() Q_D( ctkDICOMTableManager ); return(d->seriesTable); } + +//------------------------------------------------------------------------------ +bool ctkDICOMTableManager::isBatchUpdate()const +{ + Q_D(const ctkDICOMTableManager); + return d->patientsTable->isBatchUpdate(); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMTableManager::setBatchUpdate(bool enable) +{ + Q_D(ctkDICOMTableManager); + if (enable == this->isBatchUpdate()) + { + return enable; + } + d->patientsTable->setBatchUpdate(enable); + d->studiesTable->setBatchUpdate(enable); + d->seriesTable->setBatchUpdate(enable); + return !enable; +} + +//---------------------------------------------------------------------------- +void ctkDICOMTableManager::showPatientsFilterActiveWarning(bool showWarning) +{ + Q_D(ctkDICOMTableManager); + d->showFilterActiveWarning(d->patientsSearchBox, showWarning); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTableManager::showStudiesFilterActiveWarning(bool showWarning) +{ + Q_D(ctkDICOMTableManager); + d->showFilterActiveWarning(d->studiesSearchBox, showWarning); +} +//---------------------------------------------------------------------------- +void ctkDICOMTableManager::showSeriesFilterActiveWarning(bool showWarning) +{ + Q_D(ctkDICOMTableManager); + d->showFilterActiveWarning(d->seriesSearchBox, showWarning); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMTableManager.h b/Libs/DICOM/Widgets/ctkDICOMTableManager.h index cba05d7708..4f78e13119 100644 --- a/Libs/DICOM/Widgets/ctkDICOMTableManager.h +++ b/Libs/DICOM/Widgets/ctkDICOMTableManager.h @@ -74,7 +74,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableManager : public QWidget */ Q_INVOKABLE void setDICOMDatabase(ctkDICOMDatabase* db); - void setTableOrientation(const Qt::Orientation&) const; + void setTableOrientation(const Qt::Orientation&); Qt::Orientation tableOrientation(); /** @@ -100,12 +100,29 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableManager : public QWidget Q_INVOKABLE ctkDICOMTableView* studiesTable(); Q_INVOKABLE ctkDICOMTableView* seriesTable(); + /** + * @brief Get if view is in batch update mode. + * \sa setBatchUpdate + */ + Q_INVOKABLE bool isBatchUpdate() const; + /** + * @brief Enable/disable batch update on the view. + * While in batch update mode, database changes will not update the view. + * When batch update is disabled then pending notifications are be processed. + */ + Q_INVOKABLE bool setBatchUpdate(bool); + public Q_SLOTS: void onPatientsQueryChanged(const QStringList&); void onStudiesQueryChanged(const QStringList&); void onPatientsSelectionChanged(const QStringList&); void onStudiesSelectionChanged(const QStringList&); +protected Q_SLOTS: + void showPatientsFilterActiveWarning(bool); + void showStudiesFilterActiveWarning(bool); + void showSeriesFilterActiveWarning(bool); + Q_SIGNALS: /// Signals for propagating selection changes of the different tables void patientsSelectionChanged(const QItemSelection&, const QItemSelection&); diff --git a/Libs/DICOM/Widgets/ctkDICOMTableView.cpp b/Libs/DICOM/Widgets/ctkDICOMTableView.cpp index 6baa05fcd0..43c1f61f2c 100644 --- a/Libs/DICOM/Widgets/ctkDICOMTableView.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMTableView.cpp @@ -57,6 +57,11 @@ class ctkDICOMTableViewPrivate : public Ui_ctkDICOMTableView QStringList currentSelection; + bool batchUpdate; + /// Set to true if database modification is notified while in batch update mode + bool batchUpdateModificationPending; + bool batchUpdateInstanceAddedPending; + /// Key = QString for columns, Values = QStringList QHash sqlWhereConditions; @@ -68,6 +73,9 @@ ctkDICOMTableViewPrivate::ctkDICOMTableViewPrivate(ctkDICOMTableView &obj) { this->dicomSQLFilterModel = new QSortFilterProxyModel(&obj); this->dicomDatabase = new ctkDICOMDatabase(&obj); + this->batchUpdate = false; + this->batchUpdateModificationPending = false; + this->batchUpdateInstanceAddedPending = false; } //------------------------------------------------------------------------------ @@ -125,10 +133,7 @@ void ctkDICOMTableViewPrivate::init() SIGNAL(customContextMenuRequested(const QPoint&)), q, SLOT(onCustomContextMenuRequested(const QPoint&))); - QObject::connect(this->leSearchBox, SIGNAL(textChanged(QString)), - this->dicomSQLFilterModel, SLOT(setFilterWildcard(QString))); - - QObject::connect(this->leSearchBox, SIGNAL(textChanged(QString)), q, SLOT(onFilterChanged())); + QObject::connect(this->leSearchBox, SIGNAL(textChanged(QString)), q, SLOT(onFilterChanged(QString))); } //---------------------------------------------------------------------------- @@ -157,15 +162,17 @@ void ctkDICOMTableViewPrivate::applyColumnProperties() { if (!this->dicomDatabase || !this->dicomDatabase->isOpen()) { - qCritical() << Q_FUNC_INFO << ": Database not accessible"; return; } + bool stretchedColumnFound = false; + int defaultSortByColumn = -1; + Qt::SortOrder defaultSortOrder = Qt::AscendingOrder; + QHeaderView* header = this->tblDicomDatabaseView->horizontalHeader(); int columnCount = this->dicomSQLModel.columnCount(); QList columnWeights; QMap visualIndexToColumnIndexMap; - bool stretchedColumnFound = false; for (int col=0; coldicomSQLModel.headerData(col, Qt::Horizontal).toString(); @@ -209,6 +216,8 @@ void ctkDICOMTableViewPrivate::applyColumnProperties() if (!fieldFormatObj.isEmpty()) { // format string successfully decoded from json + + // resize mode QString resizeModeStr = fieldFormatObj.value(QString("resizeMode")).toString("interactive"); if (resizeModeStr == "interactive") { @@ -227,6 +236,25 @@ void ctkDICOMTableViewPrivate::applyColumnProperties() qWarning() << "Invalid ColumnDisplayProperties Format string for column " << columnName << ": resizeMode must be interactive, stretch, or resizeToContents"; } + // default sort order + QString sortStr = fieldFormatObj.value(QString("sort")).toString(); + if (!sortStr.isEmpty()) + { + defaultSortByColumn = col; + if (sortStr == "ascending") + { + defaultSortOrder = Qt::AscendingOrder; + } + else if (sortStr == "descending") + { + defaultSortOrder = Qt::DescendingOrder; + } + else + { + qWarning() << "Invalid ColumnDisplayProperties Format string for column " << columnName << ": sort must be ascending or descending"; + } + } + } else { @@ -245,16 +273,22 @@ void ctkDICOMTableViewPrivate::applyColumnProperties() // If no stretched column is shown then stretch the last column to make the table look nicely aligned this->tblDicomDatabaseView->horizontalHeader()->setStretchLastSection(!stretchedColumnFound); + if (defaultSortByColumn >= 0) + { + this->tblDicomDatabaseView->sortByColumn(defaultSortByColumn, defaultSortOrder); + } + // First restore original order of the columns so that it can be sorted by weights (use bubble sort). // This extra complexity is needed because the only mechanism for column order is by moving or swapping bool wasBlocked = header->blockSignals(true); if (!visualIndexToColumnIndexMap.isEmpty()) { QList columnIndicesByVisualIndex = visualIndexToColumnIndexMap.values(); - for (int i=0; i columnIndicesByVisualIndex[j+1]) { @@ -325,21 +359,36 @@ void ctkDICOMTableView::setDicomDataBase(ctkDICOMDatabase *dicomDatabase) { Q_D(ctkDICOMTableView); - //Do nothing if no database is set - if (!dicomDatabase) + if (d->dicomDatabase == dicomDatabase) { + // no change return; } - d->dicomDatabase = dicomDatabase; + if (d->dicomDatabase) + { + QObject::disconnect(d->dicomDatabase, SIGNAL(instanceAdded(const QString&)), this, SLOT(onInstanceAdded())); + QObject::disconnect(d->dicomDatabase, SIGNAL(databaseChanged()), this, SLOT(onDatabaseChanged())); + QObject::disconnect(d->dicomDatabase, SIGNAL(opened()), this, SLOT(onDatabaseOpened())); + QObject::disconnect(d->dicomDatabase, SIGNAL(closed()), this, SLOT(onDatabaseClosed())); + QObject::disconnect(d->dicomDatabase, SIGNAL(schemaUpdated()), this, SLOT(onDatabaseSchemaUpdated())); + } - //Create connections for new database - QObject::connect(d->dicomDatabase, SIGNAL(instanceAdded(const QString&)), this, SLOT(onInstanceAdded())); - QObject::connect(d->dicomDatabase, SIGNAL(databaseChanged()), this, SLOT(onDatabaseChanged())); + d->dicomDatabase = dicomDatabase; + if (d->dicomDatabase) + { + //Create connections for new database + QObject::connect(d->dicomDatabase, SIGNAL(instanceAdded(const QString&)), this, SLOT(onInstanceAdded())); + QObject::connect(d->dicomDatabase, SIGNAL(databaseChanged()), this, SLOT(onDatabaseChanged())); + QObject::connect(d->dicomDatabase, SIGNAL(opened()), this, SLOT(onDatabaseOpened())); + QObject::connect(d->dicomDatabase, SIGNAL(closed()), this, SLOT(onDatabaseClosed())); + QObject::connect(d->dicomDatabase, SIGNAL(schemaUpdated()), this, SLOT(onDatabaseSchemaUpdated())); + } this->setQuery(); - d->applyColumnProperties(); + + this->setEnabled(d->dicomDatabase && d->dicomDatabase->isOpen()); } //------------------------------------------------------------------------------ @@ -363,15 +412,42 @@ void ctkDICOMTableView::onSelectionChanged() } //------------------------------------------------------------------------------ -void ctkDICOMTableView::onDatabaseChanged() +void ctkDICOMTableView::onDatabaseOpened() { Q_D(ctkDICOMTableView); + this->setQuery(); + d->applyColumnProperties(); + this->setEnabled(true); +} +//------------------------------------------------------------------------------ +void ctkDICOMTableView::onDatabaseClosed() +{ + Q_D(ctkDICOMTableView); this->setQuery(); + this->setEnabled(false); +} +//------------------------------------------------------------------------------ +void ctkDICOMTableView::onDatabaseSchemaUpdated() +{ + Q_D(ctkDICOMTableView); + this->setQuery(); d->applyColumnProperties(); } +//------------------------------------------------------------------------------ +void ctkDICOMTableView::onDatabaseChanged() +{ + Q_D(ctkDICOMTableView); + if (d->batchUpdate) + { + d->batchUpdateModificationPending = true; + return; + } + this->setQuery(); +} + //------------------------------------------------------------------------------ void ctkDICOMTableView::onUpdateQuery(const QStringList& uids) { @@ -379,31 +455,50 @@ void ctkDICOMTableView::onUpdateQuery(const QStringList& uids) this->setQuery(uids); - d->showFilterActiveWarning( d->dicomSQLFilterModel->rowCount() == 0 && - d->leSearchBox->text().length() != 0 ); + bool showWarning = d->dicomSQLFilterModel->rowCount() == 0 && + d->leSearchBox->text().length() != 0; + d->showFilterActiveWarning(showWarning); + emit showFilterActiveWarning(showWarning); const QStringList& newUIDS = this->uidsForAllRows(); emit queryChanged(newUIDS); } //------------------------------------------------------------------------------ -void ctkDICOMTableView::onFilterChanged() +void ctkDICOMTableView::setFilterText(const QString& filterText) +{ + Q_D(ctkDICOMTableView); + d->leSearchBox->setText(filterText); +} + +//------------------------------------------------------------------------------ +void ctkDICOMTableView::onFilterChanged(const QString& filterText) { Q_D(ctkDICOMTableView); + d->dicomSQLFilterModel->setFilterWildcard(filterText); + const QStringList uids = this->uidsForAllRows(); - d->showFilterActiveWarning( d->dicomSQLFilterModel->rowCount() == 0 && - d->dicomSQLModel.rowCount() != 0); + bool showWarning = d->dicomSQLFilterModel->rowCount() == 0 && + d->dicomSQLModel.rowCount() != 0; + d->showFilterActiveWarning(showWarning); + emit showFilterActiveWarning(showWarning); d->tblDicomDatabaseView->clearSelection(); emit queryChanged(uids); + emit filterTextChanged(filterText); } //------------------------------------------------------------------------------ void ctkDICOMTableView::onInstanceAdded() { Q_D(ctkDICOMTableView); + if (d->batchUpdate) + { + d->batchUpdateInstanceAddedPending = true; + return; + } d->sqlWhereConditions.clear(); d->tblDicomDatabaseView->clearSelection(); d->leSearchBox->clear(); @@ -482,6 +577,10 @@ void ctkDICOMTableView::setQuery(const QStringList &uids) { d->dicomSQLModel.setQuery(query.arg(d->queryTableName()), d->dicomDatabase->database()); } + else + { + d->dicomSQLModel.clear(); + } } void ctkDICOMTableView::addSqlWhereCondition(const std::pair &condition) @@ -550,5 +649,52 @@ void ctkDICOMTableView::onCustomContextMenuRequested(const QPoint &point) QTableView* ctkDICOMTableView::tableView() { Q_D( ctkDICOMTableView ); - return(d->tblDicomDatabaseView); + return d->tblDicomDatabaseView; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMTableView::isBatchUpdate()const +{ + Q_D(const ctkDICOMTableView); + return d->batchUpdate; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMTableView::setBatchUpdate(bool enable) +{ + Q_D(ctkDICOMTableView); + if (enable == d->batchUpdate) + { + return d->batchUpdate; + } + d->batchUpdate = enable; + if (!d->batchUpdate) + { + // Process pending modification events + if (d->batchUpdateModificationPending) + { + this->onDatabaseChanged(); + } + if (d->batchUpdateInstanceAddedPending) + { + this->onInstanceAdded(); + } + } + d->batchUpdateModificationPending = false; + d->batchUpdateInstanceAddedPending = false; + return !d->batchUpdate; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMTableView::isHeaderVisible()const +{ + Q_D(const ctkDICOMTableView); + return d->headerWidget->isVisible(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMTableView::setHeaderVisible(bool visible) +{ + Q_D(ctkDICOMTableView); + return d->headerWidget->setVisible(visible); } diff --git a/Libs/DICOM/Widgets/ctkDICOMTableView.h b/Libs/DICOM/Widgets/ctkDICOMTableView.h index dbe9607e8f..cca0c7d15f 100644 --- a/Libs/DICOM/Widgets/ctkDICOMTableView.h +++ b/Libs/DICOM/Widgets/ctkDICOMTableView.h @@ -45,6 +45,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableView : public QWidget Q_OBJECT Q_PROPERTY(bool filterActive READ filterActive) Q_PROPERTY( QTableView* tblDicomDatabaseView READ tableView ) + Q_PROPERTY(bool headerVisible READ isHeaderVisible WRITE setHeaderVisible) public: typedef QWidget Superclass; @@ -123,6 +124,27 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableView : public QWidget */ Q_INVOKABLE QTableView* tableView(); + /** + * @brief Get if view is in batch update mode. + * \sa setBatchUpdate + */ + Q_INVOKABLE bool isBatchUpdate() const; + + /** + * @brief Enable/disable batch update on the view. + * While in batch update mode, database changes will not update the view. + * When batch update is disabled then pending notifications are be processed. + * @return previous value of batch update + */ + bool setBatchUpdate(bool); + + /** + * @brief Show/hide table header + * Table header shows table name and search box. + */ + void setHeaderVisible(bool state); + bool isHeaderVisible() const; + public Q_SLOTS: /** * @brief slot is called if the selection of the tableview is changed @@ -158,7 +180,27 @@ public Q_SLOTS: */ void clearSelection(); + /** + * @brief Set text in the filter box. + */ + void setFilterText(const QString& filterText); + protected Q_SLOTS: + /** + * @brief Called when the database is opened + */ + void onDatabaseOpened(); + + /** + * @brief Called when the database is closed + */ + void onDatabaseClosed(); + + /** + * @brief Called when the database is schema is updated + */ + void onDatabaseSchemaUpdated(); + /** * @brief Called when the underlying database changes */ @@ -167,7 +209,7 @@ protected Q_SLOTS: /** * @brief Called when the text of the ctkSearchBox has changed */ - void onFilterChanged(); + void onFilterChanged(const QString& filterText); /** * @brief Called if a new instance was added to the database @@ -189,6 +231,18 @@ protected Q_SLOTS: */ void selectionChanged(const QItemSelection&,const QItemSelection&); + /** + * @brief Is emitted when filter text is changed. + */ + void filterTextChanged(const QString& filterText); + + /** + * @brief Is emitted when filter active warning should be shown or hidden. + * Filter warning is displayed when no item is shown in the view because of + * the entered filter criteria. + */ + void showFilterActiveWarning(bool showWarning); + /** * @brief Is emitted when the query text has changed * @param uids the list of uids of the objects included in the query diff --git a/Libs/DICOM/Widgets/ctkDICOMWidgetsPythonQtDecorators.h b/Libs/DICOM/Widgets/ctkDICOMWidgetsPythonQtDecorators.h index 309e74ca12..d06b6c6bac 100644 --- a/Libs/DICOM/Widgets/ctkDICOMWidgetsPythonQtDecorators.h +++ b/Libs/DICOM/Widgets/ctkDICOMWidgetsPythonQtDecorators.h @@ -26,7 +26,6 @@ // CTK includes -#include // NOTE: // // For decorators it is assumed that the methods will never be called @@ -42,20 +41,14 @@ class ctkDICOMWidgetsPythonQtDecorators : public QObject ctkDICOMWidgetsPythonQtDecorators() { - PythonQt::self()->registerClass(&ctkDICOMBrowser::staticMetaObject, "CTKDICOMWidgets"); } public Q_SLOTS: // - // ctkDICOMBrowser + // None yet - refer to other libs for examples // - ctkDICOMBrowser* new_ctkDICOMBrowser(QWidget* parent=0, QString databaseDirectorySettingsKey=QString()) - { - return new ctkDICOMBrowser(parent, databaseDirectorySettingsKey); - } - }; //-----------------------------------------------------------------------------