diff --git a/Libs/DICOM/Core/CMakeLists.txt b/Libs/DICOM/Core/CMakeLists.txt index 4b9a74b5f9..00491f320c 100644 --- a/Libs/DICOM/Core/CMakeLists.txt +++ b/Libs/DICOM/Core/CMakeLists.txt @@ -13,6 +13,8 @@ set(KIT_SRCS ctkDICOMDatabase.cpp ctkDICOMDatabase.h ctkDICOMItem.h + ctkDICOMDisplayedFieldGenerator.cpp + ctkDICOMDisplayedFieldGenerator.h ctkDICOMFilterProxyModel.cpp ctkDICOMFilterProxyModel.h ctkDICOMIndexer.cpp @@ -32,11 +34,17 @@ set(KIT_SRCS ctkDICOMTester.h ctkDICOMUtil.cpp ctkDICOMUtil.h + ctkDICOMDisplayedFieldGeneratorAbstractRule.h + ctkDICOMDisplayedFieldGeneratorDefaultRule.h + ctkDICOMDisplayedFieldGeneratorDefaultRule.cpp + ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.h + ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.cpp ) # Abstract class should not be wrapped ! set_source_files_properties( ctkDICOMAbstractThumbnailGenerator.h + ctkDICOMDisplayedFieldGeneratorAbstractRule.h WRAP_EXCLUDE ) @@ -44,6 +52,8 @@ set_source_files_properties( set(KIT_MOC_SRCS ctkDICOMAbstractThumbnailGenerator.h ctkDICOMDatabase.h + ctkDICOMDisplayedFieldGenerator.h + ctkDICOMDisplayedFieldGenerator_p.h ctkDICOMIndexer.h ctkDICOMIndexer_p.h ctkDICOMFilterProxyModel.h diff --git a/Libs/DICOM/Core/Resources/dicom-schema.sql b/Libs/DICOM/Core/Resources/dicom-schema.sql index 6faa7b53ce..c93996a441 100644 --- a/Libs/DICOM/Core/Resources/dicom-schema.sql +++ b/Libs/DICOM/Core/Resources/dicom-schema.sql @@ -1,10 +1,10 @@ --- --- A simple SQLITE3 database schema for modelling locally stored DICOM files --- +-- +-- A simple SQLITE3 database schema for modelling locally stored DICOM files +-- -- 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 +-- Note: be sure to update ctkDICOMDatabase and SchemaInfo Version -- whenever you make a change to this schema -- ; @@ -13,6 +13,7 @@ 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' ; @@ -21,13 +22,14 @@ DROP INDEX IF EXISTS 'SeriesStudyIndex' ; DROP INDEX IF EXISTS 'StudiesPatientIndex' ; CREATE TABLE 'SchemaInfo' ( 'Version' VARCHAR(1024) NOT NULL ); -INSERT INTO 'SchemaInfo' VALUES('0.5.3'); +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, @@ -35,9 +37,29 @@ CREATE TABLE 'Patients' ( 'PatientID' VARCHAR(255) NULL , 'PatientsBirthDate' DATE NULL , 'PatientsBirthTime' TIME NULL , - 'PatientsSex' varchar(1) NULL , - 'PatientsAge' varchar(10) NULL , - 'PatientsComments' VARCHAR(255) 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 , @@ -53,20 +75,12 @@ CREATE TABLE 'Series' ( '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 TABLE 'Studies' ( - 'StudyInstanceUID' VARCHAR(64) NOT NULL , - 'PatientsUID' INT NOT NULL , - 'StudyID' VARCHAR(255) NULL , - 'StudyDate' DATE NULL , - 'StudyTime' VARCHAR(20) NULL , - 'AccessionNumber' VARCHAR(255) NULL , - 'ModalitiesInStudy' VARCHAR(255) NULL , - 'InstitutionName' VARCHAR(255) NULL , - 'ReferringPhysician' VARCHAR(255) NULL , - 'PerformingPhysiciansName' VARCHAR(255) NULL , - 'StudyDescription' VARCHAR(255) NULL , - PRIMARY KEY ('StudyInstanceUID') ); CREATE UNIQUE INDEX IF NOT EXISTS 'ImagesFilenameIndex' ON 'Images' ('Filename'); CREATE INDEX IF NOT EXISTS 'ImagesSeriesIndex' ON 'Images' ('SeriesInstanceUID'); @@ -76,3 +90,60 @@ 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', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientID', 'Patient ID', 1, 2, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsBirthDate', 'Birth date', 1, 3, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsBirthTime', 'Birth time', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'PatientsSex', 'Sex', 1, 4, ''); +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', 'DisplayedPatientsName', 'Patient name', 1, 1, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Patients', 'DisplayedNumberOfStudies', 'Number of studies', 1, 5, ''); +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, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyTime', 'Study time', 0, 0, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'StudyDescription', 'Study description', 1, 3, ''); +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', 1, 5, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Studies', 'DisplayedNumberOfSeries', 'Number of series', 1, 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, 1, ''); +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, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'Modality', 'Modality', 1, 3, ''); +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', 1, 6, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedCount', 'Count', 1, 4, ''); +INSERT INTO 'ColumnDisplayProperties' VALUES('Series', 'DisplayedSize', 'Size', 1, 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/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index b9cad03cf3..1120de7f4e 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -41,6 +41,8 @@ #include "ctkLogger.h" +#include "ctkDICOMDisplayedFieldGenerator.h" + // DCMTK includes #include #include @@ -61,12 +63,12 @@ static ctkLogger logger("org.commontk.dicom.DICOMDatabase" ); //------------------------------------------------------------------------------ -// Flag for tag cache to avoid repeated serarches for -// tags that do no exist. +/// Flag for tag cache to avoid repeated searches for tags that do no exist static QString TagNotInInstance("__TAG_NOT_IN_INSTANCE__"); -// Flag for tag cache indicating that the value -// really is the empty string +/// Flag for tag cache indicating that the value really is the empty string static QString ValueIsEmptyString("__VALUE_IS_EMPTY_STRING__"); +/// Separator character for table and field names to be used in display rules manager +static QString TableFieldSeparator(":"); //------------------------------------------------------------------------------ class ctkDICOMDatabasePrivate @@ -81,46 +83,72 @@ class ctkDICOMDatabasePrivate void init(QString databaseFile); void registerCompressionLibraries(); bool executeScript(const QString script); - /// - /// \brief runs a query and prints debug output of status - /// + + /// Run a query and prints debug output of status bool loggedExec(QSqlQuery& query); bool loggedExec(QSqlQuery& query, const QString& queryString); bool loggedExecBatch(QSqlQuery& query); bool LoggedExecVerbose; - /// - /// \brief group several inserts into a single transaction - /// + /// Group several inserts into a single transaction void beginTransaction(); void endTransaction(); - // dataset must be set always - // filePath has to be set if this is an import of an actual file + /// 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); - /// - /// copy the complete list of files to an extra table - /// + /// Copy the complete list of files to an extra table void createBackupFileList(); - /// - /// remove the extra table containing the backup - /// + /// Remove the extra table containing the backup void removeBackupFileList(); - - /// - /// get all Filename values from table + /// Update database tables from the displayed fields determined by the plugin roles + /// \return Success flag + bool applyDisplayedFieldsChanges( QMap > &displayedFieldsMapSeries, + QMap > &displayedFieldsMapStudy, + QVector > &displayedFieldsVectorPatient ); + + /// 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 + /// 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); + + /// 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 + /// \return The study instance UID if successfully found, empty string otherwise + QString getDisplayStudyFieldsKey(QString studyInstanceUID, QMap > &displayedFieldsMapStudy); + + /// Find series by instance UID and insert it in the given fields map + /// \param displayedFieldsMapSeries Map of series field maps (name, value pairs) to which the found series has been inserted on success + /// \return The series instance UID if successfully found, empty string otherwise + QString getDisplaySeriesFieldsKey(QString seriesInstanceUID, QMap > &displayedFieldsMapSeries); + + /// Get all Filename values from table QStringList filenames(QString table); + QVector > displayedFieldsVectorPatient; // The index in the vector is the internal patient UID + /// Calculate count (number of objects in interest) for each series in the displayed fields container + /// \param displayedFieldsMapSeries (SeriesInstanceUID -> (DisplayField -> Value) ) + void setCountToSeriesDisplayedFields(QMap > &displayedFieldsMapSeries); + /// Calculate number of series for each study in the displayed fields container + /// \param displayedFieldsMapStudy (StudyInstanceUID -> (DisplayField -> Value) ) + 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); + /// Name of the database file (i.e. for SQLITE the sqlite file) - QString DatabaseFileName; - QString LastError; + QString DatabaseFileName; + QString LastError; QSqlDatabase Database; QMap LoadedHeader; - ctkDICOMAbstractThumbnailGenerator* thumbnailGenerator; + ctkDICOMAbstractThumbnailGenerator* ThumbnailGenerator; + + ctkDICOMDisplayedFieldGenerator DisplayedFieldGenerator; /// these are for optimizing the import of image sequences /// since most information are identical for all slices @@ -156,7 +184,7 @@ class ctkDICOMDatabasePrivate //------------------------------------------------------------------------------ ctkDICOMDatabasePrivate::ctkDICOMDatabasePrivate(ctkDICOMDatabase& o): q_ptr(&o) { - this->thumbnailGenerator = NULL; + this->ThumbnailGenerator = NULL; this->LoggedExecVerbose = false; this->TagCacheVerified = false; this->resetLastInsertedValues(); @@ -212,26 +240,26 @@ bool ctkDICOMDatabasePrivate::loggedExec(QSqlQuery& query, const QString& queryS { bool success; if (queryString.compare("")) - { - success = query.exec(queryString); - } + { + success = query.exec(queryString); + } else - { - success = query.exec(); - } + { + success = query.exec(); + } if (!success) - { - QSqlError sqlError = query.lastError(); - logger.debug( "SQL failed\n Bad SQL: " + query.lastQuery()); - logger.debug( "Error text: " + sqlError.text()); - } + { + QSqlError sqlError = query.lastError(); + logger.debug( "SQL failed\n Bad SQL: " + query.lastQuery()); + logger.debug( "Error text: " + sqlError.text()); + } else + { + if (LoggedExecVerbose) { - if (LoggedExecVerbose) - { logger.debug( "SQL worked!\n SQL: " + query.lastQuery()); - } } + } return (success); } @@ -241,18 +269,18 @@ bool ctkDICOMDatabasePrivate::loggedExecBatch(QSqlQuery& query) bool success; success = query.execBatch(); if (!success) - { - QSqlError sqlError = query.lastError(); - logger.debug( "SQL failed\n Bad SQL: " + query.lastQuery()); - logger.debug( "Error text: " + sqlError.text()); - } + { + QSqlError sqlError = query.lastError(); + logger.debug( "SQL failed\n Bad SQL: " + query.lastQuery()); + logger.debug( "Error text: " + sqlError.text()); + } else + { + if (LoggedExecVerbose) { - if (LoggedExecVerbose) - { logger.debug( "SQL worked!\n SQL: " + query.lastQuery()); - } } + } return (success); } @@ -287,134 +315,15 @@ void ctkDICOMDatabasePrivate::removeBackupFileList() loggedExec(query, "DROP TABLE main.Filenames_backup; " ); } - - -//------------------------------------------------------------------------------ -void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& connectionName ) -{ - Q_D(ctkDICOMDatabase); - d->DatabaseFileName = databaseFile; - QString verifiedConnectionName = connectionName; - if (verifiedConnectionName.isEmpty()) - { - verifiedConnectionName = QUuid::createUuid().toString(); - } - d->Database = QSqlDatabase::addDatabase("QSQLITE", verifiedConnectionName); - d->Database.setDatabaseName(databaseFile); - if ( ! (d->Database.open()) ) - { - d->LastError = d->Database.lastError().text(); - return; - } - if ( d->Database.tables().empty() ) - { - if (!initializeDatabase()) - { - d->LastError = QString("Unable to initialize DICOM database!"); - return; - } - } - d->resetLastInsertedValues(); - - if (!isInMemory()) - { - QFileSystemWatcher* watcher = new QFileSystemWatcher(QStringList(databaseFile),this); - 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" ); - d->TagCacheVerified = false; - if ( !this->tagCacheExists() ) - { - this->initializeTagCache(); - } -} - -//------------------------------------------------------------------------------ -// ctkDICOMDatabase methods - -//------------------------------------------------------------------------------ -ctkDICOMDatabase::ctkDICOMDatabase(QString databaseFile) - : d_ptr(new ctkDICOMDatabasePrivate(*this)) -{ - Q_D(ctkDICOMDatabase); - d->registerCompressionLibraries(); - d->init(databaseFile); -} - -ctkDICOMDatabase::ctkDICOMDatabase(QObject* parent) - : d_ptr(new ctkDICOMDatabasePrivate(*this)) -{ - Q_UNUSED(parent); - Q_D(ctkDICOMDatabase); - d->registerCompressionLibraries(); -} - -//------------------------------------------------------------------------------ -ctkDICOMDatabase::~ctkDICOMDatabase() -{ -} - -//---------------------------------------------------------------------------- - -//------------------------------------------------------------------------------ -const QString ctkDICOMDatabase::lastError() const { - Q_D(const ctkDICOMDatabase); - return d->LastError; -} - -//------------------------------------------------------------------------------ -const QString ctkDICOMDatabase::databaseFilename() const { - Q_D(const ctkDICOMDatabase); - return d->DatabaseFileName; -} - -//------------------------------------------------------------------------------ -const QString ctkDICOMDatabase::databaseDirectory() const { - QString databaseFile = databaseFilename(); - if (!QFileInfo(databaseFile).isAbsolute()) - { - databaseFile.prepend(QDir::currentPath() + "/"); - } - return QFileInfo ( databaseFile ).absoluteDir().path(); -} - -//------------------------------------------------------------------------------ -const QSqlDatabase& ctkDICOMDatabase::database() const { - Q_D(const ctkDICOMDatabase); - return d->Database; -} - -//------------------------------------------------------------------------------ -void ctkDICOMDatabase::setThumbnailGenerator(ctkDICOMAbstractThumbnailGenerator *generator){ - Q_D(ctkDICOMDatabase); - d->thumbnailGenerator = generator; -} - -//------------------------------------------------------------------------------ -ctkDICOMAbstractThumbnailGenerator* ctkDICOMDatabase::thumbnailGenerator(){ - Q_D(const ctkDICOMDatabase); - return d->thumbnailGenerator; -} - //------------------------------------------------------------------------------ bool ctkDICOMDatabasePrivate::executeScript(const QString script) { QFile scriptFile(script); scriptFile.open(QIODevice::ReadOnly); if ( !scriptFile.isOpen() ) - { - qDebug() << "Script file " << script << " could not be opened!\n"; - return false; - } + { + qDebug() << "Script file " << script << " could not be opened!\n"; + return false; + } QString sqlCommands( QTextStream(&scriptFile).readAll() ); sqlCommands.replace( '\n', ' ' ); @@ -426,22 +335,22 @@ bool ctkDICOMDatabasePrivate::executeScript(const QString script) { QSqlQuery query(Database); for (QStringList::iterator it = sqlCommandsLines.begin(); it != sqlCommandsLines.end()-1; ++it) + { + if (! (*it).startsWith("--") ) { - if (! (*it).startsWith("--") ) - { - if (LoggedExecVerbose) - { - qDebug() << *it << "\n"; - } - query.exec(*it); - if (query.lastError().type()) - { - qDebug() << "There was an error during execution of the statement: " << (*it); - qDebug() << "Error message: " << query.lastError().text(); - return false; - } - } + if (LoggedExecVerbose) + { + qDebug() << *it << "\n"; + } + query.exec(*it); + if (query.lastError().type()) + { + qDebug() << "There was an error during execution of the statement: " << (*it); + qDebug() << "Error message: " << query.lastError().text(); + return false; + } } + } return true; } @@ -461,1133 +370,1602 @@ QStringList ctkDICOMDatabasePrivate::filenames(QString table) } //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::initializeDatabase(const char* sqlFileName) +int ctkDICOMDatabasePrivate::insertPatient(const ctkDICOMItem& ctkDataset) { - Q_D(ctkDICOMDatabase); - - d->resetLastInsertedValues(); + int dbPatientID; - // remove any existing schema info - this handles the case where an - // old schema should be loaded for testing. - QSqlQuery dropSchemaInfo(d->Database); - d->loggedExec( dropSchemaInfo, QString("DROP TABLE IF EXISTS 'SchemaInfo';") ); - return d->executeScript(sqlFileName); -} + // 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 ctkDICOMDatabase::schemaVersionLoaded() -{ - Q_D(ctkDICOMDatabase); - /// look for the version info in the database - QSqlQuery versionQuery(d->Database); - if ( !d->loggedExec( versionQuery, QString("SELECT Version from SchemaInfo;") ) ) - { - return QString(""); - } + QSqlQuery checkPatientExistsQuery(this->Database); + checkPatientExistsQuery.prepare( "SELECT * FROM Patients WHERE PatientID = ? AND PatientsName = ?" ); + checkPatientExistsQuery.bindValue( 0, patientID ); + checkPatientExistsQuery.bindValue( 1, patientsName ); + loggedExec(checkPatientExistsQuery); - if (versionQuery.next()) - { - return versionQuery.value(0).toString(); - } + if (checkPatientExistsQuery.next()) + { + // we found him + dbPatientID = checkPatientExistsQuery.value(checkPatientExistsQuery.record().indexOf("UID")).toInt(); + qDebug() << "Found patient in the database as UId: " << dbPatientID; + } + 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) ); + + QSqlQuery insertPatientStatement(this->Database); + 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 ); + // 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() ); + loggedExec(insertPatientStatement); + dbPatientID = insertPatientStatement.lastInsertId().toInt(); + logger.debug( "New patient inserted: " + QString().setNum ( dbPatientID ) ); + qDebug() << "New patient inserted as : " << dbPatientID; + } - return QString(""); + return dbPatientID; } //------------------------------------------------------------------------------ -QString ctkDICOMDatabase::schemaVersion() +void ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& ctkDataset, int dbPatientID) { - // When changing schema version: - // * make sure this matches the Version value in the - // SchemaInfo table defined in Resources/dicom-schema.sql - // * make sure the 'Images' contains a 'Filename' column - // so that the ctkDICOMDatabasePrivate::filenames method - // still works. - // - return QString("0.5.3"); -}; + QString studyInstanceUID(ctkDataset.GetElementAsString(DCM_StudyInstanceUID) ); + QSqlQuery checkStudyExistsQuery(this->Database); + checkStudyExistsQuery.prepare( "SELECT * FROM Studies WHERE StudyInstanceUID = ?" ); + checkStudyExistsQuery.bindValue( 0, studyInstanceUID ); + checkStudyExistsQuery.exec(); + if (!checkStudyExistsQuery.next()) + { + 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) ); + + QSqlQuery insertStudyStatement(this->Database); + insertStudyStatement.prepare( "INSERT INTO Studies " + "( 'StudyInstanceUID', 'PatientsUID', 'StudyID', 'StudyDate', 'StudyTime', 'AccessionNumber', 'ModalitiesInStudy', 'InstitutionName', 'ReferringPhysician', 'PerformingPhysiciansName', " + "'StudyDescription', 'InsertTimestamp', 'DisplayedNumberOfSeries', 'DisplayedFieldsUpdatedTimestamp' ) " + "VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL )" ); + insertStudyStatement.bindValue( 0, studyInstanceUID ); + insertStudyStatement.bindValue( 1, dbPatientID ); + insertStudyStatement.bindValue( 2, studyID ); + insertStudyStatement.bindValue( 3, QDate::fromString ( studyDate, "yyyyMMdd" ) ); + insertStudyStatement.bindValue( 4, studyTime ); + insertStudyStatement.bindValue( 5, accessionNumber ); + insertStudyStatement.bindValue( 6, modalitiesInStudy ); + insertStudyStatement.bindValue( 7, institutionName ); + insertStudyStatement.bindValue( 8, referringPhysician ); + insertStudyStatement.bindValue( 9, performingPhysiciansName ); + insertStudyStatement.bindValue( 10, studyDescription ); + insertStudyStatement.bindValue( 11, QDateTime::currentDateTime() ); + if (!insertStudyStatement.exec()) + { + logger.error( "Error executing statement: " + insertStudyStatement.lastQuery() + " Error: " + insertStudyStatement.lastError().text() ); + } + else + { + this->LastStudyInstanceUID = studyInstanceUID; + } + } + else + { + qDebug() << "Used existing study: " << studyInstanceUID; + } +} //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::updateSchemaIfNeeded(const char* schemaFile) +void ctkDICOMDatabasePrivate::insertSeries(const ctkDICOMItem& ctkDataset, QString studyInstanceUID) { - if ( schemaVersionLoaded() != schemaVersion() ) - { - return this->updateSchema(schemaFile); + QString seriesInstanceUID(ctkDataset.GetElementAsString(DCM_SeriesInstanceUID) ); + QSqlQuery checkSeriesExistsQuery(this->Database); + checkSeriesExistsQuery.prepare( "SELECT * FROM Series WHERE SeriesInstanceUID = ?" ); + checkSeriesExistsQuery.bindValue( 0, seriesInstanceUID ); + if (this->LoggedExecVerbose) + { + logger.warn( "Statement: " + checkSeriesExistsQuery.lastQuery() ); + } + checkSeriesExistsQuery.exec(); + if (!checkSeriesExistsQuery.next()) + { + 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) ); + + QSqlQuery insertSeriesStatement(this->Database); + insertSeriesStatement.prepare( "INSERT INTO Series " + "( 'SeriesInstanceUID', 'StudyInstanceUID', 'SeriesNumber', 'SeriesDate', 'SeriesTime', 'SeriesDescription', 'Modality', 'BodyPartExamined', " + "'FrameOfReferenceUID', 'AcquisitionNumber', 'ContrastAgent', 'ScanningSequence', 'EchoNumber', 'TemporalPosition', 'InsertTimestamp' ) " + "VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )" ); + insertSeriesStatement.bindValue( 0, seriesInstanceUID ); + insertSeriesStatement.bindValue( 1, studyInstanceUID ); + insertSeriesStatement.bindValue( 2, static_cast(seriesNumber) ); + insertSeriesStatement.bindValue( 3, QDate::fromString ( seriesDate, "yyyyMMdd" ) ); + insertSeriesStatement.bindValue( 4, seriesTime ); + insertSeriesStatement.bindValue( 5, seriesDescription ); + insertSeriesStatement.bindValue( 6, modality ); + insertSeriesStatement.bindValue( 7, bodyPartExamined ); + insertSeriesStatement.bindValue( 8, frameOfReferenceUID ); + insertSeriesStatement.bindValue( 9, static_cast(acquisitionNumber) ); + insertSeriesStatement.bindValue( 10, contrastAgent ); + insertSeriesStatement.bindValue( 11, scanningSequence ); + insertSeriesStatement.bindValue( 12, static_cast(echoNumber) ); + insertSeriesStatement.bindValue( 13, static_cast(temporalPosition) ); + insertSeriesStatement.bindValue( 14, QDateTime::currentDateTime() ); + if ( !insertSeriesStatement.exec() ) + { + logger.error( "Error executing statement: " + + insertSeriesStatement.lastQuery() + + " Error: " + insertSeriesStatement.lastError().text() ); + this->LastSeriesInstanceUID = ""; + } + else + { + this->LastSeriesInstanceUID = seriesInstanceUID; } + } else - { - emit schemaUpdateStarted(0); - emit schemaUpdated(); - return false; - } + { + qDebug() << "Used existing series: " << seriesInstanceUID; + } } //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::updateSchema(const char* schemaFile) +bool ctkDICOMDatabasePrivate::openTagCacheDatabase() { - // backup filelist - // reinit with the new schema - // reinsert everything + // try to open the database if it's not already open + if ( this->TagCacheDatabase.isOpen() ) + { + return true; + } + this->TagCacheDatabase = QSqlDatabase::addDatabase( + "QSQLITE", this->Database.connectionName() + "TagCache"); + this->TagCacheDatabase.setDatabaseName(this->TagCacheDatabaseFilename); + if ( !this->TagCacheDatabase.open() ) + { + qDebug() << "TagCacheDatabase would not open!\n"; + qDebug() << "TagCacheDatabaseFilename is: " << this->TagCacheDatabaseFilename << "\n"; + return false; + } - Q_D(ctkDICOMDatabase); - d->createBackupFileList(); + // Disable synchronous writing to make modifications faster + QSqlQuery pragmaSyncQuery(this->TagCacheDatabase); + pragmaSyncQuery.exec("PRAGMA synchronous = OFF"); + pragmaSyncQuery.finish(); - d->resetLastInsertedValues(); - this->initializeDatabase(schemaFile); - - QStringList allFiles = d->filenames("Filenames_backup"); - emit schemaUpdateStarted(allFiles.length()); - - int progressValue = 0; - foreach(QString file, allFiles) - { - emit schemaUpdateProgress(progressValue); - emit schemaUpdateProgress(file); - - // TODO: use QFuture - this->insert(file,false,false,true); - - progressValue++; - } - // TODO: check better that everything is ok - d->removeBackupFileList(); - emit schemaUpdated(); return true; - } - //------------------------------------------------------------------------------ -void ctkDICOMDatabase::closeDatabase() +void ctkDICOMDatabasePrivate::precacheTags( const QString sopInstanceUID ) { - Q_D(ctkDICOMDatabase); - d->Database.close(); - d->TagCacheDatabase.close(); -} + Q_Q(ctkDICOMDatabase); -// -// Patient/study/series convenience methods -// + ctkDICOMItem dataset; + QString fileName = q->fileForInstance(sopInstanceUID); + dataset.InitializeFromFile(fileName); -//------------------------------------------------------------------------------ -QStringList ctkDICOMDatabase::patients() -{ - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT UID FROM Patients" ); - query.exec(); - QStringList result; - while (query.next()) + + QStringList sopInstanceUIDs, tags, values; + foreach (const QString &tag, this->TagsToPrecache) { - result << query.value(0).toString(); + unsigned short group, element; + q->tagToGroupElement(tag, group, element); + DcmTagKey tagKey(group, element); + QString value = dataset.GetAllElementValuesAsString(tagKey); + sopInstanceUIDs << sopInstanceUID; + tags << tag; + values << value; } - return( result ); + + QSqlQuery transaction( this->TagCacheDatabase ); + transaction.prepare( "BEGIN TRANSACTION" ); + transaction.exec(); + + q->cacheTags(sopInstanceUIDs, tags, values); + + transaction = QSqlQuery( this->TagCacheDatabase ); + transaction.prepare( "END TRANSACTION" ); + transaction.exec(); } //------------------------------------------------------------------------------ -QStringList ctkDICOMDatabase::studiesForPatient(QString dbPatientID) +void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& ctkDataset, const QString& filePath, bool storeFile, bool generateThumbnail) { - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT StudyInstanceUID FROM Studies WHERE PatientsUID = ?" ); - query.bindValue ( 0, dbPatientID ); - query.exec(); - QStringList result; - while (query.next()) + Q_Q(ctkDICOMDatabase); + + // this is the method that all other insert signatures end up calling + // after they have pre-parsed their arguments + + // 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 sopInstanceUID ( ctkDataset.GetElementAsString(DCM_SOPInstanceUID) ); + + QSqlQuery fileExistsQuery ( Database ); + fileExistsQuery.prepare("SELECT InsertTimestamp,Filename FROM Images WHERE SOPInstanceUID == :sopInstanceUID"); + fileExistsQuery.bindValue(":sopInstanceUID",sopInstanceUID); + { + bool success = fileExistsQuery.exec(); + if (!success) { - result << query.value(0).toString(); + logger.error("SQLITE ERROR: " + fileExistsQuery.lastError().driverText()); + return; } - return( result ); -} - -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::studyForSeries(QString seriesUID) -{ - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT StudyInstanceUID FROM Series WHERE SeriesInstanceUID= ?" ); - query.bindValue ( 0, seriesUID); - query.exec(); - QString result; - if (query.next()) + bool found = fileExistsQuery.next(); + if (this->LoggedExecVerbose) { - result = query.value(0).toString(); + qDebug() << "inserting filePath: " << filePath; } - return( result ); -} - -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::patientForStudy(QString studyUID) -{ - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT PatientsUID FROM Studies WHERE StudyInstanceUID= ?" ); - query.bindValue ( 0, studyUID); - query.exec(); - QString result; - if (query.next()) + if (!found) { - result = query.value(0).toString(); + if (this->LoggedExecVerbose) + { + qDebug() << "database filename for " << sopInstanceUID << " is empty - we should insert on top of it"; + } } - return( result ); -} - -//------------------------------------------------------------------------------ -QHash ctkDICOMDatabase::descriptionsForFile(QString fileName) -{ - Q_D(ctkDICOMDatabase); + else + { + QString databaseFilename(fileExistsQuery.value(1).toString()); + QDateTime fileLastModified(QFileInfo(databaseFilename).lastModified()); + QDateTime databaseInsertTimestamp(QDateTime::fromString(fileExistsQuery.value(0).toString(),Qt::ISODate)); - QString seriesUID(this->seriesForFile(fileName)); - QString studyUID(this->studyForSeries(seriesUID)); - QString patientID(this->patientForStudy(studyUID)); + 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; + } + } + } + } - QSqlQuery query(d->Database); - query.prepare ( "SELECT SeriesDescription FROM Series WHERE SeriesInstanceUID= ?" ); - query.bindValue ( 0, seriesUID); - query.exec(); - QHash result; - if (query.next()) - { - result["SeriesDescription"] = query.value(0).toString(); + //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 + // see: http://www.na-mic.org/Bug/view.php?id=2040 + logger.warn("Patient ID is empty, using studyInstanceUID as patient ID"); + patientID = studyInstanceUID; } - query.prepare ( "SELECT StudyDescription FROM Studies WHERE StudyInstanceUID= ?" ); - query.bindValue ( 0, studyUID); - query.exec(); - if (query.next()) - { - result["StudyDescription"] = query.value(0).toString(); + 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; } - query.prepare ( "SELECT PatientsName FROM Patients WHERE UID= ?" ); - query.bindValue ( 0, patientID); - query.exec(); - if (query.next()) + if ( patientsName.isEmpty() || studyInstanceUID.isEmpty() || patientID.isEmpty() ) { - result["PatientsName"] = query.value(0).toString(); + logger.error("Dataset is missing necessary information (patient name, study instance UID, or patient ID)!"); + return; } - return( result ); -} -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::descriptionForSeries(const QString seriesUID) -{ - Q_D(ctkDICOMDatabase); + // 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() ) + { + // QString studySeriesDirectory = studyInstanceUID + "/" + seriesInstanceUID; + QString destinationDirectoryName = q->databaseDirectory() + "/dicom/"; + QDir destinationDir(destinationDirectoryName); + filename = destinationDirectoryName + + studyInstanceUID + "/" + + seriesInstanceUID + "/" + + sopInstanceUID; - QString result; + destinationDir.mkpath(studyInstanceUID + "/" + + seriesInstanceUID); - QSqlQuery query(d->Database); - query.prepare ( "SELECT SeriesDescription FROM Series WHERE SeriesInstanceUID= ?" ); - query.bindValue ( 0, seriesUID); - query.exec(); - if (query.next()) + if (filePath.isEmpty()) { - result = query.value(0).toString(); - } - - return result; -} + if (this->LoggedExecVerbose) + { + logger.debug("Saving file: " + filename); + } -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::descriptionForStudy(const QString studyUID) -{ - Q_D(ctkDICOMDatabase); + if ( !ctkDataset.SaveToFile( filename) ) + { + logger.error("Error saving file: " + filename); + return; + } + } + else + { + // we're inserting an existing file + QFile currentFile( filePath ); + currentFile.copy(filename); + if (this->LoggedExecVerbose) + { + logger.debug("Copy file from: " + filePath + " to: " + filename); + } + } + } - QString result; + //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; - QSqlQuery query(d->Database); - query.prepare ( "SELECT StudyDescription FROM Studies WHERE StudyInstanceUID= ?" ); - query.bindValue ( 0, studyUID); - query.exec(); - if (query.next()) + if ( patientID != "" && patientsName != "" ) + { + //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 ) { - result = query.value(0).toString(); - } + 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. - return result; -} + dbPatientID = insertPatient( ctkDataset ); -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::nameForPatient(const QString patientUID) -{ - Q_D(ctkDICOMDatabase); + // let users of this class track when things happen + emit q->patientAdded(dbPatientID, patientID, patientsName, patientsBirthDate); - QString result; + /// keep this for the next image + LastPatientUID = dbPatientID; + LastPatientID = patientID; + LastPatientsBirthDate = patientsBirthDate; + LastPatientsName = patientsName; + } - QSqlQuery query(d->Database); - query.prepare ( "SELECT PatientsName FROM Patients WHERE UID= ?" ); - query.bindValue ( 0, patientUID); - query.exec(); - if (query.next()) + if (this->LoggedExecVerbose) { - result = query.value(0).toString(); + qDebug() << "Going to insert this instance with dbPatientID: " << dbPatientID; } - return result; -} + // Patient is in now. Let's continue with the study -//------------------------------------------------------------------------------ -QStringList ctkDICOMDatabase::seriesForStudy(QString studyUID) -{ - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT SeriesInstanceUID FROM Series WHERE StudyInstanceUID=?"); - query.bindValue ( 0, studyUID ); - query.exec(); - QStringList result; - while (query.next()) + if ( studyInstanceUID != "" && LastStudyInstanceUID != studyInstanceUID ) { - result << query.value(0).toString(); + insertStudy(ctkDataset,dbPatientID); + + // let users of this class track when things happen + emit q->studyAdded(studyInstanceUID); + qDebug() << "Study Added"; } - return( result ); -} -//------------------------------------------------------------------------------ -QStringList ctkDICOMDatabase::instancesForSeries(const QString seriesUID) -{ - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare("SELECT SOPInstanceUID FROM Images WHERE SeriesInstanceUID= ?"); - query.bindValue(0, seriesUID); - query.exec(); - QStringList result; - while (query.next()) - { - result << query.value(0).toString(); - } - return result; -} + if ( seriesInstanceUID != "" && seriesInstanceUID != LastSeriesInstanceUID ) + { + insertSeries(ctkDataset, studyInstanceUID); -//------------------------------------------------------------------------------ -QStringList ctkDICOMDatabase::filesForSeries(QString seriesUID) -{ - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT Filename FROM Images WHERE SeriesInstanceUID=?"); - query.bindValue ( 0, seriesUID ); - query.exec(); - QStringList result; - while (query.next()) + // let users of this class track when things happen + emit q->seriesAdded(seriesInstanceUID); + qDebug() << "Series Added"; + } + // TODO: what to do with imported files + // + if ( !filename.isEmpty() && !seriesInstanceUID.isEmpty() ) { - result << query.value(0).toString(); + QSqlQuery checkImageExistsQuery (Database); + checkImageExistsQuery.prepare ( "SELECT * FROM Images WHERE Filename = ?" ); + checkImageExistsQuery.bindValue ( 0, filename ); + checkImageExistsQuery.exec(); + if (this->LoggedExecVerbose) + { + qDebug() << "Maybe add Instance"; + } + if (!checkImageExistsQuery.next()) + { + QSqlQuery insertImageStatement ( Database ); + insertImageStatement.prepare ( "INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )" ); + insertImageStatement.bindValue ( 0, sopInstanceUID ); + insertImageStatement.bindValue ( 1, filename ); + insertImageStatement.bindValue ( 2, seriesInstanceUID ); + insertImageStatement.bindValue ( 3, 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) + { + qDebug() << "Instance Added"; + } + } } - return( result ); -} -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::fileForInstance(QString sopInstanceUID) -{ - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT Filename FROM Images WHERE SOPInstanceUID=?"); - query.bindValue ( 0, sopInstanceUID ); - query.exec(); - QString result; - if (query.next()) + if ( generateThumbnail && ThumbnailGenerator && !seriesInstanceUID.isEmpty() ) { - result = query.value(0).toString(); + 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())) ) + { + QDir(q->databaseDirectory() + "/thumbs/").mkpath(studySeriesDirectory); + DicomImage dcmImage(QDir::toNativeSeparators(filename).toLatin1()); + ThumbnailGenerator->generateThumbnail(&dcmImage, thumbnailPath); + } } - return( result ); -} -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::seriesForFile(QString fileName) -{ - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT SeriesInstanceUID FROM Images WHERE Filename=?"); - query.bindValue ( 0, fileName ); - query.exec(); - QString result; - if (query.next()) + if (q->isInMemory()) { - result = query.value(0).toString(); + emit q->databaseChanged(); } - return( result ); + } + else + { + qDebug() << "No patient name or no patient id - not inserting!"; + } } //------------------------------------------------------------------------------ -QString ctkDICOMDatabase::instanceForFile(QString fileName) +int ctkDICOMDatabasePrivate::getDisplayPatientFieldsIndex(QString patientsName, QString patientID, QVector > &displayedFieldsVectorPatient) { - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT SOPInstanceUID FROM Images WHERE Filename=?"); - query.bindValue ( 0, fileName ); - query.exec(); - QString result; - if (query.next()) + // Look for the patient in the displayed fields cache first + for (int patientIndex=0; patientIndex < displayedFieldsVectorPatient.size(); ++patientIndex) + { + QMap currentPatient = displayedFieldsVectorPatient[patientIndex]; + if ( !currentPatient["PatientID"].compare(patientID) + && !currentPatient["PatientsName"].compare(patientsName) ) { - result = query.value(0).toString(); + return patientIndex; } - return( result ); + } + + // Look for the patient in the display database + QSqlQuery displayPatientsQuery(this->Database); + displayPatientsQuery.prepare( "SELECT * FROM Patients WHERE PatientID = :patientID AND PatientsName = :patientsName ;" ); + displayPatientsQuery.bindValue(":patientID", patientID); + displayPatientsQuery.bindValue(":patientsName", patientsName); + if (!displayPatientsQuery.exec()) + { + logger.error("SQLITE ERROR: " + displayPatientsQuery.lastError().driverText()); + return -1; + } + if (displayPatientsQuery.size() > 1) + { + logger.warn("Multiple patients found with PatientsName=" + patientsName + " and PatientID=" + patientID); + } + if (displayPatientsQuery.next()) + { + QSqlRecord patientRecord = displayPatientsQuery.record(); + QMap patientFieldsMap; + for (int fieldIndex=0; fieldIndexDatabase); + displayStudiesQuery.prepare( QString("SELECT StudyInstanceUID FROM Studies WHERE StudyInstanceUID = :studyInstanceUID ;") ); + displayStudiesQuery.bindValue(":studyInstanceUID", studyInstanceUID); + if (!displayStudiesQuery.exec()) + { + logger.error("SQLITE ERROR: " + displayStudiesQuery.lastError().driverText()); + return QString(); + } + if (displayStudiesQuery.size() > 1) + { + logger.warn("Multiple studies found with StudyInstanceUID=" + studyInstanceUID); + } + if (displayStudiesQuery.next()) + { + QSqlRecord studyRecord = displayStudiesQuery.record(); + QMap studyFieldsMap; + for (int fieldIndex=0; fieldIndexfilenames("Images"); + logger.error("Failed to find study with StudyInstanceUID=" + studyInstanceUID); + return QString(); } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::loadInstanceHeader (QString sopInstanceUID) +QString ctkDICOMDatabasePrivate::getDisplaySeriesFieldsKey(QString seriesInstanceUID, QMap > &displayedFieldsMapSeries) { - Q_D(ctkDICOMDatabase); - QSqlQuery query(d->Database); - query.prepare ( "SELECT Filename FROM Images WHERE SOPInstanceUID=?"); - query.bindValue ( 0, sopInstanceUID ); - query.exec(); - if (query.next()) + // Look for the series in the displayed fields cache first + foreach (QString currentSeriesInstanceUid, displayedFieldsMapSeries.keys()) + { + if ( !displayedFieldsMapSeries[currentSeriesInstanceUid]["SeriesInstanceUID"].compare(seriesInstanceUID) ) { - QString fileName = query.value(0).toString(); - this->loadFileHeader(fileName); + return seriesInstanceUID; } - return; + } + + // Look for the series in the display database + QSqlQuery displaySeriesQuery(this->Database); + displaySeriesQuery.prepare( QString("SELECT SeriesInstanceUID FROM Series WHERE SeriesInstanceUID = :seriesInstanceUID ;") ); + displaySeriesQuery.bindValue(":seriesInstanceUID", seriesInstanceUID); + if (!displaySeriesQuery.exec()) + { + logger.error("SQLITE ERROR: " + displaySeriesQuery.lastError().driverText()); + return QString(); + } + if (displaySeriesQuery.size() > 1) + { + logger.warn("Multiple series found with SeriesInstanceUID=" + seriesInstanceUID); + } + if (displaySeriesQuery.next()) + { + QSqlRecord seriesRecord = displaySeriesQuery.record(); + QMap seriesFieldsMap; + for (int fieldIndex=0; fieldIndexnextObject(stack, true) == EC_Normal) + QString displayPatientsFieldUpdateList; + foreach (QString tagName, currentPatient.keys()) + { + if (tagName == "PatientIndex") { - DcmObject *dO = stack.top(); - if (dO) - { - QString tag = QString("%1,%2").arg( - dO->getGTag(),4,16,QLatin1Char('0')).arg( - dO->getETag(),4,16,QLatin1Char('0')); - std::ostringstream s; - dO->print(s); - d->LoadedHeader[tag] = QString(s.str().c_str()); - } + continue; // Do not write patient index that is only used internally and temporarily } - } - return; -} + displayPatientsFieldUpdateList.append( tagName + "='" + (currentPatient[tagName].isEmpty() ? "" : currentPatient[tagName]) + "', " ); + } -//------------------------------------------------------------------------------ -QStringList ctkDICOMDatabase::headerKeys () -{ - Q_D(ctkDICOMDatabase); - return (d->LoadedHeader.keys()); -} + // Trim the separators from the end + displayPatientsFieldUpdateList = displayPatientsFieldUpdateList.left(displayPatientsFieldUpdateList.size() - 2); -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::headerValue (QString key) -{ - Q_D(ctkDICOMDatabase); - return (d->LoadedHeader[key]); -} + QSqlRecord patientRecord = displayPatientsQuery.record(); + int patientUID = patientRecord.value("UID").toInt(); -// -// instanceValue and fileValue methods -// + QSqlQuery updateDisplayPatientStatement(this->Database); + QString updateDisplayPatientStatementString = + QString("UPDATE Patients SET %1 WHERE UID='%2';").arg(displayPatientsFieldUpdateList).arg(patientUID); + this->loggedExec(updateDisplayPatientStatement, updateDisplayPatientStatementString); -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::instanceValue(QString sopInstanceUID, QString tag) -{ - QString value = this->cachedTag(sopInstanceUID, tag); - if (value == TagNotInInstance || value == ValueIsEmptyString) + QSqlQuery updateDisplayedFieldsUpdatedTimestampStatement(this->Database); + QString updateDisplayedFieldsUpdatedTimestampStatementString = + QString("UPDATE Patients SET DisplayedFieldsUpdatedTimestamp=CURRENT_TIMESTAMP WHERE UID='%1';").arg(patientUID); + this->loggedExec(updateDisplayedFieldsUpdatedTimestampStatement, updateDisplayedFieldsUpdatedTimestampStatementString); + + patientIndexToPatientUidMap[patientIndex] = patientUID; + } + else { - return ""; + logger.error("Failed to find patient with PatientsName=" + currentPatient["PatientsName"] + " and PatientID=" + currentPatient["PatientID"]); + return false; } - if (value != "") + } // For each patient in displayedFieldsVectorPatient + + // Update study fields + foreach (QString currentStudyInstanceUid, displayedFieldsMapStudy.keys()) + { + QMap currentStudy = displayedFieldsMapStudy[currentStudyInstanceUid]; + QSqlQuery displayStudiesQuery(QString("SELECT StudyInstanceUID FROM Studies WHERE StudyInstanceUID='%1' ;").arg(currentStudyInstanceUid), this->Database); + if (!displayStudiesQuery.exec()) { - return value; + logger.error("SQLITE ERROR: " + displayStudiesQuery.lastError().driverText()); + return false; } - unsigned short group, element; - this->tagToGroupElement(tag, group, element); - return( this->instanceValue(sopInstanceUID, group, element) ); -} + if (displayStudiesQuery.next()) + { + QString displayStudiesFieldUpdateList; + foreach (QString tagName, currentStudy.keys()) + { + if (!tagName.compare("PatientIndex")) + { + displayStudiesFieldUpdateList.append( "PatientsUID=" + QString::number(patientIndexToPatientUidMap[currentStudy["PatientIndex"].toInt()]) + ", " ); + } + else + { + displayStudiesFieldUpdateList.append( tagName + "='" + (currentStudy[tagName].isEmpty() ? "" : currentStudy[tagName]) + "', " ); + } + } + // Trim the separators from the end + displayStudiesFieldUpdateList = displayStudiesFieldUpdateList.left(displayStudiesFieldUpdateList.size() - 2); -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::instanceValue(const QString sopInstanceUID, const unsigned short group, const unsigned short element) -{ - QString tag = this->groupElementToTag(group,element); - QString value = this->cachedTag(sopInstanceUID, tag); - if (value == TagNotInInstance || value == ValueIsEmptyString) + QSqlQuery updateDisplayStudyStatement(this->Database); + QString updateDisplayStudyStatementString = + QString("UPDATE Studies SET %1 WHERE StudyInstanceUID='%2';").arg(displayStudiesFieldUpdateList).arg(currentStudy["StudyInstanceUID"]); + this->loggedExec(updateDisplayStudyStatement, updateDisplayStudyStatementString); + + QSqlQuery updateDisplayedFieldsUpdatedTimestampStatement(this->Database); + QString updateDisplayedFieldsUpdatedTimestampStatementString = + QString("UPDATE Studies SET DisplayedFieldsUpdatedTimestamp=CURRENT_TIMESTAMP WHERE StudyInstanceUID='%1';").arg(currentStudy["StudyInstanceUID"]); + this->loggedExec(updateDisplayedFieldsUpdatedTimestampStatement, updateDisplayedFieldsUpdatedTimestampStatementString); + } + else { - return ""; + logger.error("Failed to find study with StudyInstanceUID=" + currentStudyInstanceUid); + return false; } - if (value != "") + } // For each study in displayedFieldsMapStudy + + // Update series fields + foreach (QString currentSeriesInstanceUid, displayedFieldsMapSeries.keys()) + { + // Insert row into Series if does not exist + QMap currentSeries = displayedFieldsMapSeries[currentSeriesInstanceUid]; + QSqlQuery displaySeriesQuery(QString("SELECT SeriesInstanceUID FROM Series WHERE SeriesInstanceUID='%1' ;").arg(currentSeriesInstanceUid), this->Database); + if (!displaySeriesQuery.exec()) { - return value; + logger.error("SQLITE ERROR: " + displaySeriesQuery.lastError().driverText()); + return false; } - QString filePath = this->fileForInstance(sopInstanceUID); - if (filePath != "" ) + if (displaySeriesQuery.next()) { - value = this->fileValue(filePath, group, element); - return( value ); + QString displaySeriesFieldUpdateList; + foreach (QString tagName, currentSeries.keys()) + { + displaySeriesFieldUpdateList.append( tagName + "='" + (currentSeries[tagName].isEmpty() ? "" : currentSeries[tagName]) + "', " ); + } + // Trim the separators from the end + displaySeriesFieldUpdateList = displaySeriesFieldUpdateList.left(displaySeriesFieldUpdateList.size() - 2); + + QSqlQuery updateDisplaySeriesStatement(this->Database); + QString updateDisplaySeriesStatementString = + QString("UPDATE Series SET %1 WHERE SeriesInstanceUID='%2';").arg(displaySeriesFieldUpdateList).arg(currentSeries["SeriesInstanceUID"]); + this->loggedExec(updateDisplaySeriesStatement, updateDisplaySeriesStatementString); + + QSqlQuery updateDisplayedFieldsUpdatedTimestampStatement(this->Database); + QString updateDisplayedFieldsUpdatedTimestampStatementString = + QString("UPDATE Series SET DisplayedFieldsUpdatedTimestamp=CURRENT_TIMESTAMP WHERE SeriesInstanceUID='%1';").arg(currentSeries["SeriesInstanceUID"]); + this->loggedExec(updateDisplayedFieldsUpdatedTimestampStatement, updateDisplayedFieldsUpdatedTimestampStatementString); } - else + else { - return (""); + logger.error("Failed to find series with SeriesInstanceUID=" + currentSeriesInstanceUid); + return false; } -} + } // For each series in displayedFieldsMapSeries + return true; +} //------------------------------------------------------------------------------ -QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) +void ctkDICOMDatabasePrivate::setCountToSeriesDisplayedFields(QMap > &displayedFieldsMapSeries) { - unsigned short group, element; - this->tagToGroupElement(tag, group, element); - QString sopInstanceUID = this->instanceForFile(fileName); - QString value = this->cachedTag(sopInstanceUID, tag); - if (value == TagNotInInstance || value == ValueIsEmptyString) - { - return ""; - } - if (value != "") + foreach (QString currentSeriesInstanceUid, displayedFieldsMapSeries.keys()) + { + QSqlQuery countQuery( + QString("SELECT COUNT(*) FROM TagCache WHERE Tag='%1' AND Value='%2';") + .arg(ctkDICOMItem::TagKeyStripped(DCM_SeriesInstanceUID)).arg(currentSeriesInstanceUid), + this->TagCacheDatabase ); + if (!countQuery.exec()) { - return value; + logger.error("SQLITE ERROR: " + countQuery.lastError().driverText()); + continue; } - return( this->fileValue(fileName, group, element) ); + + countQuery.first(); + int currentCount = countQuery.value(0).toInt(); + + QMap displayedFieldsForCurrentSeries = displayedFieldsMapSeries[currentSeriesInstanceUid]; + displayedFieldsForCurrentSeries["DisplayedCount"] = QString::number(currentCount); + displayedFieldsMapSeries[currentSeriesInstanceUid] = displayedFieldsForCurrentSeries; + } } //------------------------------------------------------------------------------ -QString ctkDICOMDatabase::fileValue(const QString fileName, const unsigned short group, const unsigned short element) +void ctkDICOMDatabasePrivate::setNumberOfSeriesToStudyDisplayedFields(QMap > &displayedFieldsMapStudy) { - // here is where the real lookup happens - // - first we check the tagCache to see if the value exists for this instance tag - // If not, - // - for now we create a ctkDICOMItem and extract the value from there - // - then we convert to the appropriate type of string - // - //As an optimization we could consider - // - check if we are currently looking at the dataset for this fileName - // - if so, are we looking for a group/element that is past the last one - // accessed - // -- if so, keep looking for the requested group/element - // -- if not, start again from the begining - - QString tag = this->groupElementToTag(group, element); - QString sopInstanceUID = this->instanceForFile(fileName); - QString value = this->cachedTag(sopInstanceUID, tag); - if (value == TagNotInInstance || value == ValueIsEmptyString) - { - return ""; - } - if (value != "") + foreach (QString currentStudyInstanceUid, displayedFieldsMapStudy.keys()) + { + QSqlQuery numberOfSeriesQuery( + QString("SELECT COUNT(*) FROM Series WHERE StudyInstanceUID='%1';").arg(currentStudyInstanceUid), + this->Database ); + if (!numberOfSeriesQuery.exec()) { - return value; + logger.error("SQLITE ERROR: " + numberOfSeriesQuery.lastError().driverText()); + continue; } - ctkDICOMItem dataset; - dataset.InitializeFromFile(fileName); - if (!dataset.IsInitialized()) - { - logger.error( "File " + fileName + " could not be initialized."); - return ""; - } + numberOfSeriesQuery.first(); + int currentNumberOfSeries = numberOfSeriesQuery.value(0).toInt(); - DcmTagKey tagKey(group, element); - value = dataset.GetAllElementValuesAsString(tagKey); - this->cacheTag(sopInstanceUID, tag, value); - return value; + QMap displayedFieldsForCurrentStudy = displayedFieldsMapStudy[currentStudyInstanceUid]; + displayedFieldsForCurrentStudy["DisplayedNumberOfSeries"] = QString::number(currentNumberOfSeries); + displayedFieldsMapStudy[currentStudyInstanceUid] = displayedFieldsForCurrentStudy; + } } //------------------------------------------------------------------------------ -bool ctkDICOMDatabase::tagToGroupElement(const QString tag, unsigned short& group, unsigned short& element) +void ctkDICOMDatabasePrivate::setNumberOfStudiesToPatientDisplayedFields(QVector > &displayedFieldsVectorPatient) { - QStringList groupElement = tag.split(","); - bool groupOK, elementOK; - if (groupElement.length() != 2) + for (int patientIndex=0; patientIndex displayedFieldsForCurrentPatient = displayedFieldsVectorPatient[patientIndex]; + int patientUID = displayedFieldsForCurrentPatient["UID"].toInt(); + QSqlQuery numberOfStudiesQuery( + QString("SELECT COUNT(*) FROM Studies WHERE PatientsUID='%1';").arg(patientUID), + this->Database ); + if (!numberOfStudiesQuery.exec()) { - return false; + logger.error("SQLITE ERROR: " + numberOfStudiesQuery.lastError().driverText()); + continue; } - group = groupElement[0].toUInt(&groupOK, 16); - element = groupElement[1].toUInt(&elementOK, 16); - return( groupOK && elementOK ); -} + numberOfStudiesQuery.first(); + int currentNumberOfStudies = numberOfStudiesQuery.value(0).toInt(); -//------------------------------------------------------------------------------ -QString ctkDICOMDatabase::groupElementToTag(const unsigned short& group, const unsigned short& element) -{ - return QString("%1,%2").arg(group,4,16,QLatin1Char('0')).arg(element,4,16,QLatin1Char('0')); + displayedFieldsForCurrentPatient["DisplayedNumberOfStudies"] = QString::number(currentNumberOfStudies); + displayedFieldsVectorPatient[patientIndex] = displayedFieldsForCurrentPatient; + } } -// -// methods related to insert -// //------------------------------------------------------------------------------ -void ctkDICOMDatabase::prepareInsert() +// ctkDICOMDatabase methods +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +ctkDICOMDatabase::ctkDICOMDatabase(QString databaseFile) + : d_ptr(new ctkDICOMDatabasePrivate(*this)) { Q_D(ctkDICOMDatabase); - // Although resetLastInsertedValues is called when items are deleted through - // this database connection, there may be concurrent database modifications - // (even in the same application) through other connections. - // Therefore, we cannot be sure that the last added patient, study, series, - // items are still in the database. We clear cached Last... IDs to make sure - // the patient, study, series items are created. - d->resetLastInsertedValues(); + d->registerCompressionLibraries(); + d->init(databaseFile); } -//------------------------------------------------------------------------------ -void ctkDICOMDatabase::insert( DcmItem *item, bool storeFile, bool generateThumbnail) +ctkDICOMDatabase::ctkDICOMDatabase(QObject* parent) + : d_ptr(new ctkDICOMDatabasePrivate(*this)) { - if (!item) - { - return; - } - ctkDICOMItem ctkDataset; - ctkDataset.InitializeFromItem(item, false /* do not take ownership */); - this->insert(ctkDataset,storeFile,generateThumbnail); + Q_UNUSED(parent); + Q_D(ctkDICOMDatabase); + d->registerCompressionLibraries(); } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::insert( const ctkDICOMItem& ctkDataset, bool storeFile, bool generateThumbnail) +ctkDICOMDatabase::~ctkDICOMDatabase() { - Q_D(ctkDICOMDatabase); - d->insert(ctkDataset, QString(), storeFile, generateThumbnail); } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::insert ( const QString& filePath, bool storeFile, bool generateThumbnail, bool createHierarchy, const QString& destinationDirectoryName) +void ctkDICOMDatabase::openDatabase(const QString databaseFile, const QString& connectionName ) { Q_D(ctkDICOMDatabase); - Q_UNUSED(createHierarchy); - Q_UNUSED(destinationDirectoryName); - - /// first we check if the file is already in the database - if (fileExistsAndUpToDate(filePath)) + d->DatabaseFileName = databaseFile; + QString verifiedConnectionName = connectionName; + if (verifiedConnectionName.isEmpty()) + { + verifiedConnectionName = QUuid::createUuid().toString(); + } + d->Database = QSqlDatabase::addDatabase("QSQLITE", verifiedConnectionName); + d->Database.setDatabaseName(databaseFile); + if ( ! (d->Database.open()) ) + { + d->LastError = d->Database.lastError().text(); + return; + } + if ( d->Database.tables().empty() ) + { + if (!this->initializeDatabase()) { - logger.debug( "File " + filePath + " already added."); + d->LastError = QString("Unable to initialize DICOM database!"); return; } + } + d->resetLastInsertedValues(); - if (d->LoggedExecVerbose) - { - logger.debug( "Processing " + filePath ); - } + if (!isInMemory()) + { + QFileSystemWatcher* watcher = new QFileSystemWatcher(QStringList(databaseFile),this); + connect(watcher, SIGNAL(fileChanged(QString)),this, SIGNAL (databaseChanged()) ); + } - ctkDICOMItem ctkDataset; + // Disable synchronous writing to make modifications faster + QSqlQuery pragmaSyncQuery(d->Database); + pragmaSyncQuery.exec("PRAGMA synchronous = OFF"); + pragmaSyncQuery.finish(); - ctkDataset.InitializeFromFile(filePath); - if ( ctkDataset.IsInitialized() ) - { - d->insert( ctkDataset, filePath, storeFile, generateThumbnail ); - } - else - { - logger.warn(QString("Could not read DICOM file:") + filePath); - } + // Set up the tag cache for use later + QFileInfo fileInfo(d->DatabaseFileName); + d->TagCacheDatabaseFilename = QString( fileInfo.dir().path() + "/ctkDICOMTagCache.sql" ); + d->TagCacheVerified = false; + if ( !this->tagCacheExists() ) + { + this->initializeTagCache(); + } + + this->setTagsToPrecache(d->DisplayedFieldGenerator.getRequiredTags()); } //------------------------------------------------------------------------------ -int ctkDICOMDatabasePrivate::insertPatient(const ctkDICOMItem& ctkDataset) -{ - int dbPatientID; - - // 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) ); - - QSqlQuery checkPatientExistsQuery(Database); - checkPatientExistsQuery.prepare ( "SELECT * FROM Patients WHERE PatientID = ? AND PatientsName = ?" ); - checkPatientExistsQuery.bindValue ( 0, patientID ); - checkPatientExistsQuery.bindValue ( 1, patientsName ); - loggedExec(checkPatientExistsQuery); +const QString ctkDICOMDatabase::lastError() const { + Q_D(const ctkDICOMDatabase); + return d->LastError; +} - if (checkPatientExistsQuery.next()) - { - // we found him - dbPatientID = checkPatientExistsQuery.value(checkPatientExistsQuery.record().indexOf("UID")).toInt(); - qDebug() << "Found patient in the database as UId: " << dbPatientID; - } - 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) ); - - QSqlQuery insertPatientStatement ( Database ); - insertPatientStatement.prepare ( "INSERT INTO Patients ('UID', 'PatientsName', 'PatientID', 'PatientsBirthDate', 'PatientsBirthTime', 'PatientsSex', 'PatientsAge', 'PatientsComments' ) values ( 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 ); - loggedExec(insertPatientStatement); - dbPatientID = insertPatientStatement.lastInsertId().toInt(); - logger.debug ( "New patient inserted: " + QString().setNum ( dbPatientID ) ); - qDebug() << "New patient inserted as : " << dbPatientID; - } - return dbPatientID; +//------------------------------------------------------------------------------ +const QString ctkDICOMDatabase::databaseFilename() const { + Q_D(const ctkDICOMDatabase); + return d->DatabaseFileName; } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::insertStudy(const ctkDICOMItem& ctkDataset, int dbPatientID) -{ - QString studyInstanceUID(ctkDataset.GetElementAsString(DCM_StudyInstanceUID) ); - QSqlQuery checkStudyExistsQuery (Database); - checkStudyExistsQuery.prepare ( "SELECT * FROM Studies WHERE StudyInstanceUID = ?" ); - checkStudyExistsQuery.bindValue ( 0, studyInstanceUID ); - checkStudyExistsQuery.exec(); - if(!checkStudyExistsQuery.next()) - { - 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) ); - - QSqlQuery insertStudyStatement ( Database ); - insertStudyStatement.prepare ( "INSERT INTO Studies ( 'StudyInstanceUID', 'PatientsUID', 'StudyID', 'StudyDate', 'StudyTime', 'AccessionNumber', 'ModalitiesInStudy', 'InstitutionName', 'ReferringPhysician', 'PerformingPhysiciansName', 'StudyDescription' ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )" ); - insertStudyStatement.bindValue ( 0, studyInstanceUID ); - insertStudyStatement.bindValue ( 1, dbPatientID ); - insertStudyStatement.bindValue ( 2, studyID ); - insertStudyStatement.bindValue ( 3, QDate::fromString ( studyDate, "yyyyMMdd" ) ); - insertStudyStatement.bindValue ( 4, studyTime ); - insertStudyStatement.bindValue ( 5, accessionNumber ); - insertStudyStatement.bindValue ( 6, modalitiesInStudy ); - insertStudyStatement.bindValue ( 7, institutionName ); - insertStudyStatement.bindValue ( 8, referringPhysician ); - insertStudyStatement.bindValue ( 9, performingPhysiciansName ); - insertStudyStatement.bindValue ( 10, studyDescription ); - if ( !insertStudyStatement.exec() ) - { - logger.error ( "Error executing statament: " + insertStudyStatement.lastQuery() + " Error: " + insertStudyStatement.lastError().text() ); - } - else - { - LastStudyInstanceUID = studyInstanceUID; - } - } - else - { - qDebug() << "Used existing study: " << studyInstanceUID; - } +const QString ctkDICOMDatabase::databaseDirectory() const { + QString databaseFile = databaseFilename(); + if (!QFileInfo(databaseFile).isAbsolute()) + { + databaseFile.prepend(QDir::currentPath() + "/"); + } + return QFileInfo ( databaseFile ).absoluteDir().path(); } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::insertSeries(const ctkDICOMItem& ctkDataset, QString studyInstanceUID) -{ - QString seriesInstanceUID(ctkDataset.GetElementAsString(DCM_SeriesInstanceUID) ); - QSqlQuery checkSeriesExistsQuery (Database); - checkSeriesExistsQuery.prepare ( "SELECT * FROM Series WHERE SeriesInstanceUID = ?" ); - checkSeriesExistsQuery.bindValue ( 0, seriesInstanceUID ); - if (this->LoggedExecVerbose) - { - logger.warn ( "Statement: " + checkSeriesExistsQuery.lastQuery() ); - } - checkSeriesExistsQuery.exec(); - if(!checkSeriesExistsQuery.next()) - { - 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) ); - - QSqlQuery insertSeriesStatement ( Database ); - insertSeriesStatement.prepare ( "INSERT INTO Series ( 'SeriesInstanceUID', 'StudyInstanceUID', 'SeriesNumber', 'SeriesDate', 'SeriesTime', 'SeriesDescription', 'Modality', 'BodyPartExamined', 'FrameOfReferenceUID', 'AcquisitionNumber', 'ContrastAgent', 'ScanningSequence', 'EchoNumber', 'TemporalPosition' ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )" ); - insertSeriesStatement.bindValue ( 0, seriesInstanceUID ); - insertSeriesStatement.bindValue ( 1, studyInstanceUID ); - insertSeriesStatement.bindValue ( 2, static_cast(seriesNumber) ); - insertSeriesStatement.bindValue ( 3, QDate::fromString ( seriesDate, "yyyyMMdd" ) ); - insertSeriesStatement.bindValue ( 4, seriesTime ); - insertSeriesStatement.bindValue ( 5, seriesDescription ); - insertSeriesStatement.bindValue ( 6, modality ); - insertSeriesStatement.bindValue ( 7, bodyPartExamined ); - insertSeriesStatement.bindValue ( 8, frameOfReferenceUID ); - insertSeriesStatement.bindValue ( 9, static_cast(acquisitionNumber) ); - insertSeriesStatement.bindValue ( 10, contrastAgent ); - insertSeriesStatement.bindValue ( 11, scanningSequence ); - insertSeriesStatement.bindValue ( 12, static_cast(echoNumber) ); - insertSeriesStatement.bindValue ( 13, static_cast(temporalPosition) ); - if ( !insertSeriesStatement.exec() ) - { - logger.error ( "Error executing statament: " - + insertSeriesStatement.lastQuery() - + " Error: " + insertSeriesStatement.lastError().text() ); - LastSeriesInstanceUID = ""; - } - else - { - LastSeriesInstanceUID = seriesInstanceUID; - } - } - else - { - qDebug() << "Used existing series: " << seriesInstanceUID; - } +const QSqlDatabase& ctkDICOMDatabase::database() const { + Q_D(const ctkDICOMDatabase); + return d->Database; } //------------------------------------------------------------------------------ -void ctkDICOMDatabase::setTagsToPrecache( const QStringList tags) -{ +void ctkDICOMDatabase::setThumbnailGenerator(ctkDICOMAbstractThumbnailGenerator *generator){ Q_D(ctkDICOMDatabase); - d->TagsToPrecache = tags; + d->ThumbnailGenerator = generator; } //------------------------------------------------------------------------------ -const QStringList ctkDICOMDatabase::tagsToPrecache() -{ - Q_D(ctkDICOMDatabase); - return d->TagsToPrecache; +ctkDICOMAbstractThumbnailGenerator* ctkDICOMDatabase::thumbnailGenerator(){ + Q_D(const ctkDICOMDatabase); + return d->ThumbnailGenerator; } //------------------------------------------------------------------------------ -bool ctkDICOMDatabasePrivate::openTagCacheDatabase() +bool ctkDICOMDatabase::initializeDatabase(const char* sqlFileName/* = ":/dicom/dicom-schema.sql" */) { - // try to open the database if it's not already open - if ( this->TagCacheDatabase.isOpen() ) - { - return true; - } - this->TagCacheDatabase = QSqlDatabase::addDatabase( - "QSQLITE", this->Database.connectionName() + "TagCache"); - this->TagCacheDatabase.setDatabaseName(this->TagCacheDatabaseFilename); - if ( !this->TagCacheDatabase.open() ) - { - qDebug() << "TagCacheDatabase would not open!\n"; - qDebug() << "TagCacheDatabaseFilename is: " << this->TagCacheDatabaseFilename << "\n"; - return false; - } + Q_D(ctkDICOMDatabase); - // Disable synchronous writing to make modifications faster - QSqlQuery pragmaSyncQuery(this->TagCacheDatabase); - pragmaSyncQuery.exec("PRAGMA synchronous = OFF"); - pragmaSyncQuery.finish(); + d->resetLastInsertedValues(); - return true; + // remove any existing schema info - this handles the case where an + // old schema should be loaded for testing. + QSqlQuery dropSchemaInfo(d->Database); + d->loggedExec( dropSchemaInfo, QString("DROP TABLE IF EXISTS 'SchemaInfo';") ); + return d->executeScript(sqlFileName); } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::precacheTags( const QString sopInstanceUID ) +QString ctkDICOMDatabase::schemaVersionLoaded() { - Q_Q(ctkDICOMDatabase); + Q_D(ctkDICOMDatabase); + /// look for the version info in the database + QSqlQuery versionQuery(d->Database); + if ( !d->loggedExec( versionQuery, QString("SELECT Version from SchemaInfo;") ) ) + { + return QString(""); + } - ctkDICOMItem dataset; - QString fileName = q->fileForInstance(sopInstanceUID); - dataset.InitializeFromFile(fileName); + if (versionQuery.next()) + { + return versionQuery.value(0).toString(); + } + return QString(""); +} - QStringList sopInstanceUIDs, tags, values; - foreach (const QString &tag, this->TagsToPrecache) +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::schemaVersion() +{ + // When changing schema version: + // * make sure this matches the Version value in the + // SchemaInfo table defined in Resources/dicom-schema.sql + // * make sure the 'Images' contains a 'Filename' column + // so that the ctkDICOMDatabasePrivate::filenames method + // still works. + // + return QString("0.6.2"); +}; + +//------------------------------------------------------------------------------ +bool ctkDICOMDatabase::updateSchemaIfNeeded(const char* schemaFile/* = ":/dicom/dicom-schema.sql" */) +{ + if ( schemaVersionLoaded() != schemaVersion() ) + { + return this->updateSchema(schemaFile); + } + else + { + emit schemaUpdateStarted(0); + emit schemaUpdated(); + return false; + } +} + +//------------------------------------------------------------------------------ +bool ctkDICOMDatabase::updateSchema(const char* schemaFile/* = ":/dicom/dicom-schema.sql" */) +{ + // backup filelist + // reinit with the new schema + // reinsert everything + + Q_D(ctkDICOMDatabase); + d->createBackupFileList(); + + d->resetLastInsertedValues(); + this->initializeDatabase(schemaFile); + + QStringList allFiles = d->filenames("Filenames_backup"); + emit schemaUpdateStarted(allFiles.length()); + + int progressValue = 0; + foreach(QString file, allFiles) + { + emit schemaUpdateProgress(progressValue); + emit schemaUpdateProgress(file); + + // TODO: use QFuture + this->insert(file,false,false,true); + + progressValue++; + } + + // Update displayed fields in the updated database + emit displayedFieldsUpdateStarted(); + this->updateDisplayedFields(); + + // TODO: check better that everything is ok + d->removeBackupFileList(); + emit schemaUpdated(); + return true; +} + + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::closeDatabase() +{ + Q_D(ctkDICOMDatabase); + d->Database.close(); + d->TagCacheDatabase.close(); +} + +// +// Patient/study/series convenience methods +// + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::patients() +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT UID FROM Patients" ); + query.exec(); + QStringList result; + while (query.next()) + { + result << query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::studiesForPatient(QString dbPatientID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT StudyInstanceUID FROM Studies WHERE PatientsUID = ?" ); + query.bindValue ( 0, dbPatientID ); + query.exec(); + QStringList result; + while (query.next()) + { + result << query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::studyForSeries(QString seriesUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT StudyInstanceUID FROM Series WHERE SeriesInstanceUID= ?" ); + query.bindValue ( 0, seriesUID); + query.exec(); + QString result; + if (query.next()) + { + result = query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::patientForStudy(QString studyUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT PatientsUID FROM Studies WHERE StudyInstanceUID= ?" ); + query.bindValue ( 0, studyUID); + query.exec(); + QString result; + if (query.next()) + { + result = query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QHash ctkDICOMDatabase::descriptionsForFile(QString fileName) +{ + Q_D(ctkDICOMDatabase); + + QString seriesUID(this->seriesForFile(fileName)); + QString studyUID(this->studyForSeries(seriesUID)); + QString patientID(this->patientForStudy(studyUID)); + + QSqlQuery query(d->Database); + query.prepare ( "SELECT SeriesDescription FROM Series WHERE SeriesInstanceUID= ?" ); + query.bindValue ( 0, seriesUID); + query.exec(); + QHash result; + if (query.next()) + { + result["SeriesDescription"] = query.value(0).toString(); + } + query.prepare ( "SELECT StudyDescription FROM Studies WHERE StudyInstanceUID= ?" ); + query.bindValue ( 0, studyUID); + query.exec(); + if (query.next()) + { + result["StudyDescription"] = query.value(0).toString(); + } + query.prepare ( "SELECT PatientsName FROM Patients WHERE UID= ?" ); + query.bindValue ( 0, patientID); + query.exec(); + if (query.next()) + { + result["PatientsName"] = query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::descriptionForSeries(const QString seriesUID) +{ + Q_D(ctkDICOMDatabase); + + QString result; + + QSqlQuery query(d->Database); + query.prepare ( "SELECT SeriesDescription FROM Series WHERE SeriesInstanceUID= ?" ); + query.bindValue ( 0, seriesUID); + query.exec(); + if (query.next()) + { + result = query.value(0).toString(); + } + + return result; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::descriptionForStudy(const QString studyUID) +{ + Q_D(ctkDICOMDatabase); + + QString result; + + QSqlQuery query(d->Database); + query.prepare ( "SELECT StudyDescription FROM Studies WHERE StudyInstanceUID= ?" ); + query.bindValue ( 0, studyUID); + query.exec(); + if (query.next()) + { + result = query.value(0).toString(); + } + + return result; +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::nameForPatient(const QString patientUID) +{ + Q_D(ctkDICOMDatabase); + + QString result; + + QSqlQuery query(d->Database); + query.prepare ( "SELECT PatientsName FROM Patients WHERE UID= ?" ); + query.bindValue ( 0, patientUID); + query.exec(); + if (query.next()) + { + result = query.value(0).toString(); + } + + return result; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::seriesForStudy(QString studyUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT SeriesInstanceUID FROM Series WHERE StudyInstanceUID=?"); + query.bindValue ( 0, studyUID ); + query.exec(); + QStringList result; + while (query.next()) + { + result << query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::instancesForSeries(const QString seriesUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare("SELECT SOPInstanceUID FROM Images WHERE SeriesInstanceUID= ?"); + query.bindValue(0, seriesUID); + query.exec(); + QStringList result; + while (query.next()) + { + result << query.value(0).toString(); + } + + return result; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::filesForSeries(QString seriesUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT Filename FROM Images WHERE SeriesInstanceUID=?"); + query.bindValue ( 0, seriesUID ); + query.exec(); + QStringList result; + while (query.next()) + { + result << query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::fileForInstance(QString sopInstanceUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT Filename FROM Images WHERE SOPInstanceUID=?"); + query.bindValue ( 0, sopInstanceUID ); + query.exec(); + QString result; + if (query.next()) + { + result = query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::seriesForFile(QString fileName) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT SeriesInstanceUID FROM Images WHERE Filename=?"); + query.bindValue ( 0, fileName ); + query.exec(); + QString result; + if (query.next()) + { + result = query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::instanceForFile(QString fileName) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT SOPInstanceUID FROM Images WHERE Filename=?"); + query.bindValue ( 0, fileName ); + query.exec(); + QString result; + if (query.next()) + { + result = query.value(0).toString(); + } + return( result ); +} + +//------------------------------------------------------------------------------ +QDateTime ctkDICOMDatabase::insertDateTimeForInstance(QString sopInstanceUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT InsertTimestamp FROM Images WHERE SOPInstanceUID=?"); + query.bindValue ( 0, sopInstanceUID ); + query.exec(); + QDateTime result; + if (query.next()) + { + result = QDateTime::fromString(query.value(0).toString(), Qt::ISODate); + } + return( result ); +} + + +// +// instance header methods +// + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::allFiles() +{ + Q_D(ctkDICOMDatabase); + return d->filenames("Images"); +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::loadInstanceHeader (QString sopInstanceUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare ( "SELECT Filename FROM Images WHERE SOPInstanceUID=?"); + query.bindValue ( 0, sopInstanceUID ); + query.exec(); + if (query.next()) + { + QString fileName = query.value(0).toString(); + this->loadFileHeader(fileName); + } + return; +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::loadFileHeader (QString fileName) +{ + Q_D(ctkDICOMDatabase); + d->LoadedHeader.clear(); + DcmFileFormat fileFormat; + OFCondition status = fileFormat.loadFile(fileName.toLatin1().data()); + if (status.good()) + { + DcmDataset *dataset = fileFormat.getDataset(); + DcmStack stack; + while (dataset->nextObject(stack, true) == EC_Normal) { - unsigned short group, element; - q->tagToGroupElement(tag, group, element); - DcmTagKey tagKey(group, element); - QString value = dataset.GetAllElementValuesAsString(tagKey); - sopInstanceUIDs << sopInstanceUID; - tags << tag; - values << value; + DcmObject *dO = stack.top(); + if (dO) + { + QString tag = QString("%1,%2").arg(dO->getGTag(),4,16,QLatin1Char('0')).arg(dO->getETag(),4,16,QLatin1Char('0')); + std::ostringstream s; + dO->print(s); + d->LoadedHeader[tag] = QString(s.str().c_str()); + } } + } + return; +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::headerKeys () +{ + Q_D(ctkDICOMDatabase); + return (d->LoadedHeader.keys()); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::headerValue (QString key) +{ + Q_D(ctkDICOMDatabase); + return (d->LoadedHeader[key]); +} + +// +// instanceValue and fileValue methods +// + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::instanceValue(QString sopInstanceUID, QString tag) +{ + QString value = this->cachedTag(sopInstanceUID, tag); + if (value == TagNotInInstance || value == ValueIsEmptyString) + { + return ""; + } + if (value != "") + { + return value; + } + unsigned short group, element; + this->tagToGroupElement(tag, group, element); + return( this->instanceValue(sopInstanceUID, group, element) ); +} - QSqlQuery transaction( this->TagCacheDatabase ); - transaction.prepare( "BEGIN TRANSACTION" ); - transaction.exec(); +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::instanceValue(const QString sopInstanceUID, const unsigned short group, const unsigned short element) +{ + QString tag = this->groupElementToTag(group,element); + QString value = this->cachedTag(sopInstanceUID, tag); + if (value == TagNotInInstance || value == ValueIsEmptyString) + { + return ""; + } + if (value != "") + { + return value; + } + QString filePath = this->fileForInstance(sopInstanceUID); + if (filePath != "" ) + { + value = this->fileValue(filePath, group, element); + return( value ); + } + else + { + return (""); + } +} - q->cacheTags(sopInstanceUIDs, tags, values); - transaction = QSqlQuery( this->TagCacheDatabase ); - transaction.prepare( "END TRANSACTION" ); - transaction.exec(); +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) +{ + unsigned short group, element; + this->tagToGroupElement(tag, group, element); + QString sopInstanceUID = this->instanceForFile(fileName); + QString value = this->cachedTag(sopInstanceUID, tag); + if (value == TagNotInInstance || value == ValueIsEmptyString) + { + return ""; + } + if (value != "") + { + return value; + } + return( this->fileValue(fileName, group, element) ); } //------------------------------------------------------------------------------ -void ctkDICOMDatabasePrivate::insert( const ctkDICOMItem& ctkDataset, const QString& filePath, bool storeFile, bool generateThumbnail) +QString ctkDICOMDatabase::fileValue(const QString fileName, const unsigned short group, const unsigned short element) { - Q_Q(ctkDICOMDatabase); - - // this is the method that all other insert signatures end up calling - // after they have pre-parsed their arguments - - // 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. - // + // here is where the real lookup happens + // - first we check the tagCache to see if the value exists for this instance tag + // If not, + // - for now we create a ctkDICOMItem and extract the value from there + // - then we convert to the appropriate type of string // + //As an optimization we could consider + // - check if we are currently looking at the dataset for this fileName + // - if so, are we looking for a group/element that is past the last one + // accessed + // -- if so, keep looking for the requested group/element + // -- if not, start again from the beginning - QString sopInstanceUID ( ctkDataset.GetElementAsString(DCM_SOPInstanceUID) ); - - QSqlQuery fileExistsQuery ( Database ); - fileExistsQuery.prepare("SELECT InsertTimestamp,Filename FROM Images WHERE SOPInstanceUID == :sopInstanceUID"); - fileExistsQuery.bindValue(":sopInstanceUID",sopInstanceUID); + QString tag = this->groupElementToTag(group, element); + QString sopInstanceUID = this->instanceForFile(fileName); + QString value = this->cachedTag(sopInstanceUID, tag); + if (value == TagNotInInstance || value == ValueIsEmptyString) { - bool success = fileExistsQuery.exec(); - if (!success) - { - logger.error("SQLITE ERROR: " + fileExistsQuery.lastError().driverText()); - return; - } - bool found = fileExistsQuery.next(); - if (this->LoggedExecVerbose) - { - qDebug() << "inserting filePath: " << filePath; - } - if (!found) - { - if (this->LoggedExecVerbose) - { - qDebug() << "database filename for " << sopInstanceUID << " is empty - we should insert on top of it"; - } - } - else - { - QString databaseFilename(fileExistsQuery.value(1).toString()); - QDateTime fileLastModified(QFileInfo(databaseFilename).lastModified()); - QDateTime databaseInsertTimestamp(QDateTime::fromString(fileExistsQuery.value(0).toString(),Qt::ISODate)); - - 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; - } - } - } + return ""; + } + if (value != "") + { + return value; } - //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 - // see: http://www.na-mic.org/Bug/view.php?id=2040 - logger.warn("Patient ID is empty, using studyInstanceUID as patient ID"); - patientID = studyInstanceUID; + ctkDICOMItem dataset; + dataset.InitializeFromFile(fileName); + if (!dataset.IsInitialized()) + { + logger.error( "File " + fileName + " could not be initialized."); + return ""; } - 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() ) - { - logger.error("Dataset is missing necessary information (patient name, study instance UID, or patient ID)!"); - return; - } - // store the file if the database is not in memomry - // TODO: if we are called from insert(file) we - // have to do something else - // - QString filename = filePath; - if ( storeFile && !q->isInMemory() && !seriesInstanceUID.isEmpty() ) - { - // QString studySeriesDirectory = studyInstanceUID + "/" + seriesInstanceUID; - QString destinationDirectoryName = q->databaseDirectory() + "/dicom/"; - QDir destinationDir(destinationDirectoryName); - filename = destinationDirectoryName + - studyInstanceUID + "/" + - seriesInstanceUID + "/" + - sopInstanceUID; - - destinationDir.mkpath(studyInstanceUID + "/" + - seriesInstanceUID); - - if(filePath.isEmpty()) - { - if (this->LoggedExecVerbose) - { - logger.debug ( "Saving file: " + filename ); - } - - if ( !ctkDataset.SaveToFile( filename) ) - { - logger.error ( "Error saving file: " + filename ); - return; - } - } - else - { - // we're inserting an existing file - - QFile currentFile( filePath ); - currentFile.copy(filename); - if (this->LoggedExecVerbose) - { - logger.debug("Copy file from: " + filePath + " to: " + filename); - } - } - } + DcmTagKey tagKey(group, element); + value = dataset.GetAllElementValuesAsString(tagKey); + this->cacheTag(sopInstanceUID, tag, value); + return value; +} - //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; +//------------------------------------------------------------------------------ +bool ctkDICOMDatabase::tagToGroupElement(const QString tag, unsigned short& group, unsigned short& element) +{ + QStringList groupElement = tag.split(","); + bool groupOK, elementOK; + if (groupElement.length() != 2) + { + return false; + } + group = groupElement[0].toUInt(&groupOK, 16); + element = groupElement[1].toUInt(&elementOK, 16); - if ( patientID != "" && patientsName != "" ) - { - //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. - - dbPatientID = insertPatient( ctkDataset ); - - // let users of this class track when things happen - emit q->patientAdded(dbPatientID, patientID, patientsName, patientsBirthDate); - - /// keep this for the next image - LastPatientUID = dbPatientID; - LastPatientID = patientID; - LastPatientsBirthDate = patientsBirthDate; - LastPatientsName = patientsName; - } + return( groupOK && elementOK ); +} - if (this->LoggedExecVerbose) - { - qDebug() << "Going to insert this instance with dbPatientID: " << dbPatientID; - } +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::groupElementToTag(const unsigned short& group, const unsigned short& element) +{ + return QString("%1,%2").arg(group,4,16,QLatin1Char('0')).arg(element,4,16,QLatin1Char('0')); +} - // Patient is in now. Let's continue with the study +// +// methods related to insert +// - if ( studyInstanceUID != "" && LastStudyInstanceUID != studyInstanceUID ) - { - insertStudy(ctkDataset,dbPatientID); +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::prepareInsert() +{ + Q_D(ctkDICOMDatabase); + // Although resetLastInsertedValues is called when items are deleted through + // this database connection, there may be concurrent database modifications + // (even in the same application) through other connections. + // Therefore, we cannot be sure that the last added patient, study, series, + // items are still in the database. We clear cached Last... IDs to make sure + // the patient, study, series items are created. + d->resetLastInsertedValues(); +} - // let users of this class track when things happen - emit q->studyAdded(studyInstanceUID); - qDebug() << "Study Added"; - } +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::insert( DcmItem *item, bool storeFile, bool generateThumbnail) +{ + if (!item) + { + return; + } + ctkDICOMItem ctkDataset; + ctkDataset.InitializeFromItem(item, false /* do not take ownership */); + this->insert(ctkDataset,storeFile,generateThumbnail); +} +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::insert( const ctkDICOMItem& ctkDataset, bool storeFile, bool generateThumbnail) +{ + Q_D(ctkDICOMDatabase); + d->insert(ctkDataset, QString(), storeFile, generateThumbnail); +} - if ( seriesInstanceUID != "" && seriesInstanceUID != LastSeriesInstanceUID ) - { - insertSeries(ctkDataset, studyInstanceUID); +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::insert( const QString& filePath, bool storeFile, bool generateThumbnail, bool createHierarchy, const QString& destinationDirectoryName) +{ + Q_D(ctkDICOMDatabase); + Q_UNUSED(createHierarchy); + Q_UNUSED(destinationDirectoryName); - // let users of this class track when things happen - emit q->seriesAdded(seriesInstanceUID); - qDebug() << "Series Added"; - } - // TODO: what to do with imported files - // - if ( !filename.isEmpty() && !seriesInstanceUID.isEmpty() ) - { - QSqlQuery checkImageExistsQuery (Database); - checkImageExistsQuery.prepare ( "SELECT * FROM Images WHERE Filename = ?" ); - checkImageExistsQuery.bindValue ( 0, filename ); - checkImageExistsQuery.exec(); - if (this->LoggedExecVerbose) - { - qDebug() << "Maybe add Instance"; - } - if(!checkImageExistsQuery.next()) - { - QSqlQuery insertImageStatement ( Database ); - insertImageStatement.prepare ( "INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )" ); - insertImageStatement.bindValue ( 0, sopInstanceUID ); - insertImageStatement.bindValue ( 1, filename ); - insertImageStatement.bindValue ( 2, seriesInstanceUID ); - insertImageStatement.bindValue ( 3, 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) - { - qDebug() << "Instance Added"; - } - } - } + /// first we check if the file is already in the database + if (fileExistsAndUpToDate(filePath)) + { + logger.debug( "File " + filePath + " already added."); + return; + } - 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()))) - { - QDir(q->databaseDirectory() + "/thumbs/").mkpath(studySeriesDirectory); - DicomImage dcmImage(QDir::toNativeSeparators(filename).toLatin1()); - thumbnailGenerator->generateThumbnail(&dcmImage, thumbnailPath); - } - } + if (d->LoggedExecVerbose) + { + logger.debug( "Processing " + filePath ); + } - if (q->isInMemory()) - { - emit q->databaseChanged(); - } - } + ctkDICOMItem ctkDataset; + + ctkDataset.InitializeFromFile(filePath); + if ( ctkDataset.IsInitialized() ) + { + d->insert( ctkDataset, filePath, storeFile, generateThumbnail ); + } else - { - qDebug() << "No patient name or no patient id - not inserting!"; - } + { + logger.warn(QString("Could not read DICOM file:") + filePath); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::setTagsToPrecache( const QStringList tags) +{ + Q_D(ctkDICOMDatabase); + d->TagsToPrecache = tags; +} + +//------------------------------------------------------------------------------ +const QStringList ctkDICOMDatabase::tagsToPrecache() +{ + Q_D(ctkDICOMDatabase); + return d->TagsToPrecache; } //------------------------------------------------------------------------------ @@ -1600,13 +1978,11 @@ bool ctkDICOMDatabase::fileExistsAndUpToDate(const QString& filePath) check_filename_query.prepare("SELECT InsertTimestamp FROM Images WHERE Filename == ?"); check_filename_query.bindValue(0,filePath); d->loggedExec(check_filename_query); - if ( - check_filename_query.next() && - QFileInfo(filePath).lastModified() < QDateTime::fromString(check_filename_query.value(0).toString(),Qt::ISODate) - ) - { - result = true; - } + if ( check_filename_query.next() && + QFileInfo(filePath).lastModified() < QDateTime::fromString(check_filename_query.value(0).toString(), Qt::ISODate) ) + { + result = true; + } check_filename_query.finish(); return result; } @@ -1637,20 +2013,20 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) fileExistsQuery.bindValue(":seriesID",seriesInstanceUID); bool success = fileExistsQuery.exec(); if (!success) - { - logger.error("SQLITE ERROR: " + fileExistsQuery.lastError().driverText()); - return false; - } + { + logger.error("SQLITE ERROR: " + fileExistsQuery.lastError().driverText()); + return false; + } QList< QPair > removeList; 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); - } + { + 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); + } QSqlQuery fileRemove ( d->Database ); fileRemove.prepare("DELETE FROM Images WHERE SeriesInstanceUID == :seriesID"); @@ -1658,47 +2034,47 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID) logger.debug("SQLITE: removing seriesInstanceUID " + seriesInstanceUID); success = fileRemove.exec(); if (!success) - { - logger.error("SQLITE ERROR: could not remove seriesInstanceUID " + seriesInstanceUID); - logger.error("SQLITE ERROR: " + fileRemove.lastError().driverText()); - } + { + logger.error("SQLITE ERROR: could not remove seriesInstanceUID " + seriesInstanceUID); + logger.error("SQLITE ERROR: " + fileRemove.lastError().driverText()); + } QPair fileToRemove; foreach (fileToRemove, removeList) - { - QString dbFilePath = fileToRemove.first; - QString thumbnailToRemove = databaseDirectory() + "/thumbs/" + fileToRemove.second + ".png"; + { + QString dbFilePath = fileToRemove.first; + QString thumbnailToRemove = databaseDirectory() + "/thumbs/" + fileToRemove.second + ".png"; - // check that the file is below our internal storage - if (dbFilePath.startsWith( databaseDirectory() + "/dicom/")) - { - if (!dbFilePath.endsWith(fileToRemove.second)) - { - logger.error("Database inconsistency detected during delete!"); - continue; - } - if (QFile( dbFilePath ).remove()) - { - if (d->LoggedExecVerbose) - { - logger.debug("Removed file " + dbFilePath ); - } - } - else - { - logger.warn("Failed to remove file " + dbFilePath ); - } - } - // Remove thumbnail (if exists) - QFile thumbnailFile(thumbnailToRemove); - if (thumbnailFile.exists()) + // check that the file is below our internal storage + if (dbFilePath.startsWith( databaseDirectory() + "/dicom/")) + { + if (!dbFilePath.endsWith(fileToRemove.second)) + { + logger.error("Database inconsistency detected during delete!"); + continue; + } + if (QFile( dbFilePath ).remove()) + { + if (d->LoggedExecVerbose) { - if (!thumbnailFile.remove()) - { - logger.warn("Failed to remove thumbnail " + thumbnailToRemove); - } + logger.debug("Removed file " + dbFilePath ); } + } + else + { + logger.warn("Failed to remove file " + dbFilePath ); + } + } + // Remove thumbnail (if exists) + QFile thumbnailFile(thumbnailToRemove); + if (thumbnailFile.exists()) + { + if (!thumbnailFile.remove()) + { + logger.warn("Failed to remove thumbnail " + thumbnailToRemove); + } } + } this->cleanup(); @@ -1715,6 +2091,7 @@ bool ctkDICOMDatabase::cleanup() 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;"); + return true; } @@ -1728,19 +2105,19 @@ bool ctkDICOMDatabase::removeStudy(const QString& studyInstanceUID) seriesForStudy.bindValue(":studyID", studyInstanceUID); bool success = seriesForStudy.exec(); if (!success) - { - logger.error("SQLITE ERROR: " + seriesForStudy.lastError().driverText()); - return false; - } + { + logger.error("SQLITE ERROR: " + seriesForStudy.lastError().driverText()); + return false; + } bool result = true; while ( seriesForStudy.next() ) + { + QString seriesInstanceUID = seriesForStudy.value(seriesForStudy.record().indexOf("SeriesInstanceUID")).toString(); + if ( ! this->removeSeries(seriesInstanceUID) ) { - QString seriesInstanceUID = seriesForStudy.value(seriesForStudy.record().indexOf("SeriesInstanceUID")).toString(); - if ( ! this->removeSeries(seriesInstanceUID) ) - { - result = false; - } + result = false; } + } d->resetLastInsertedValues(); return result; } @@ -1755,19 +2132,19 @@ bool ctkDICOMDatabase::removePatient(const QString& patientID) studiesForPatient.bindValue(":patientsID", patientID); bool success = studiesForPatient.exec(); if (!success) - { - logger.error("SQLITE ERROR: " + studiesForPatient.lastError().driverText()); - return false; - } + { + logger.error("SQLITE ERROR: " + studiesForPatient.lastError().driverText()); + return false; + } bool result = true; while ( studiesForPatient.next() ) + { + QString studyInstanceUID = studiesForPatient.value(studiesForPatient.record().indexOf("StudyInstanceUID")).toString(); + if ( ! this->removeStudy(studyInstanceUID) ) { - QString studyInstanceUID = studiesForPatient.value(studiesForPatient.record().indexOf("StudyInstanceUID")).toString(); - if ( ! this->removeStudy(studyInstanceUID) ) - { - result = false; - } + result = false; } + } d->resetLastInsertedValues(); return result; } @@ -1782,29 +2159,29 @@ bool ctkDICOMDatabase::tagCacheExists() Q_D(ctkDICOMDatabase); if (d->TagCacheVerified) - { + { return true; - } + } if (!d->openTagCacheDatabase()) - { + { return false; - } + } if (d->TagCacheDatabase.tables().count() == 0) - { + { return false; - } + } // check that the table exists QSqlQuery cacheExists( d->TagCacheDatabase ); cacheExists.prepare("SELECT * FROM TagCache LIMIT 1"); bool success = d->loggedExec(cacheExists); if (success) - { + { d->TagCacheVerified = true; return true; - } + } qDebug() << "TagCacheDatabase NOT verified based on table check!\n"; return false; } @@ -1816,12 +2193,12 @@ bool ctkDICOMDatabase::initializeTagCache() // First, drop any existing table if ( this->tagCacheExists() ) - { + { qDebug() << "TagCacheDatabase drop existing table\n"; QSqlQuery dropCacheTable( d->TagCacheDatabase ); dropCacheTable.prepare( "DROP TABLE TagCache" ); d->loggedExec(dropCacheTable); - } + } // now create a table qDebug() << "TagCacheDatabase adding table\n"; @@ -1830,9 +2207,9 @@ bool ctkDICOMDatabase::initializeTagCache() "CREATE TABLE TagCache (SOPInstanceUID, Tag, Value, PRIMARY KEY (SOPInstanceUID, Tag))" ); bool success = d->loggedExec(createCacheTable); if (!success) - { + { return false; - } + } d->TagCacheVerified = true; return true; @@ -1843,12 +2220,12 @@ QString ctkDICOMDatabase::cachedTag(const QString sopInstanceUID, const QString { Q_D(ctkDICOMDatabase); if ( !this->tagCacheExists() ) - { + { if ( !this->initializeTagCache() ) - { + { return( "" ); - } } + } QSqlQuery selectValue( d->TagCacheDatabase ); selectValue.prepare( "SELECT Value FROM TagCache WHERE SOPInstanceUID = :sopInstanceUID AND Tag = :tag" ); selectValue.bindValue(":sopInstanceUID",sopInstanceUID); @@ -1856,16 +2233,47 @@ QString ctkDICOMDatabase::cachedTag(const QString sopInstanceUID, const QString d->loggedExec(selectValue); QString result(""); if (selectValue.next()) - { + { result = selectValue.value(0).toString(); if (result == QString("")) - { + { result = ValueIsEmptyString; - } } + } return( result ); } +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::getCachedTags(const QString sopInstanceUID, QMap &cachedTags) +{ + Q_D(ctkDICOMDatabase); + cachedTags.clear(); + if ( !this->tagCacheExists() ) + { + if ( !this->initializeTagCache() ) + { + // cache is empty + return; + } + } + QSqlQuery selectValue( d->TagCacheDatabase ); + selectValue.prepare( "SELECT Tag, Value FROM TagCache WHERE SOPInstanceUID = :sopInstanceUID" ); + selectValue.bindValue(":sopInstanceUID",sopInstanceUID); + d->loggedExec(selectValue); + QString tag; + QString value; + while (selectValue.next()) + { + tag = selectValue.value(0).toString(); + value = selectValue.value(1).toString(); + if (value == TagNotInInstance || value == ValueIsEmptyString) + { + value = QString(""); + } + cachedTags.insert(tag, value); + } +} + //------------------------------------------------------------------------------ bool ctkDICOMDatabase::cacheTag(const QString sopInstanceUID, const QString tag, const QString value) { @@ -1891,12 +2299,12 @@ 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) - { + { if (*i == "") - { + { *i = TagNotInInstance; - } } + } QSqlQuery insertTags( d->TagCacheDatabase ); insertTags.prepare( "INSERT OR REPLACE INTO TagCache VALUES(?,?,?)" ); @@ -1905,3 +2313,280 @@ bool ctkDICOMDatabase::cacheTags(const QStringList sopInstanceUIDs, const QStrin insertTags.addBindValue(values); return d->loggedExecBatch(insertTags); } + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::updateDisplayedFields() +{ + Q_D(ctkDICOMDatabase); + + // Get the files for which the displayed fields have not been created yet (DisplayedFieldsUpdatedTimestamp is NULL) + //TODO: handle cases when the values actually changed; now we only cover insertion and schema update + QSqlQuery newFilesQuery(d->Database); + d->loggedExec(newFilesQuery,QString("SELECT SOPInstanceUID, SeriesInstanceUID FROM Images WHERE DisplayedFieldsUpdatedTimestamp IS NULL;")); + + // 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 + + d->DisplayedFieldGenerator.setDatabase(this); + + int progressValue = 0; + emit displayedFieldsUpdateProgress(++progressValue); + + // Get display names for newly added files and add them into the display tables + while (newFilesQuery.next()) + { + QString sopInstanceUID = newFilesQuery.value(0).toString(); + QString seriesInstanceUID = newFilesQuery.value(1).toString(); + QMap cachedTags; + this->getCachedTags(sopInstanceUID, cachedTags); + + // Patient + int displayedFieldsIndexForCurrentPatient = d->getDisplayPatientFieldsIndex( + cachedTags[ctkDICOMItem::TagKeyStripped(DCM_PatientName)], + cachedTags[ctkDICOMItem::TagKeyStripped(DCM_PatientID)], + displayedFieldsVectorPatient ); + if (displayedFieldsIndexForCurrentPatient < 0) + { + logger.error("Failed to find patient for SOP Instance UID = " + sopInstanceUID); + continue; + } + QMap displayedFieldsForCurrentPatient = displayedFieldsVectorPatient[ displayedFieldsIndexForCurrentPatient ]; + + // Study + QString displayedFieldsKeyForCurrentStudy = d->getDisplayStudyFieldsKey( + cachedTags[ctkDICOMItem::TagKeyStripped(DCM_StudyInstanceUID)], + displayedFieldsMapStudy ); + if (displayedFieldsKeyForCurrentStudy.isEmpty()) + { + logger.error("Failed to find study for SOP Instance UID = " + sopInstanceUID); + continue; + } + QMap displayedFieldsForCurrentStudy = displayedFieldsMapStudy[ displayedFieldsKeyForCurrentStudy ]; + displayedFieldsForCurrentStudy["PatientIndex"] = QString::number(displayedFieldsIndexForCurrentPatient); + + // Series + QString displayedFieldsKeyForCurrentSeries = d->getDisplaySeriesFieldsKey( + seriesInstanceUID, + displayedFieldsMapSeries ); + if (displayedFieldsKeyForCurrentSeries.isEmpty()) + { + logger.error("Failed to find series for SOP Instance UID = " + sopInstanceUID); + continue; + } + QMap displayedFieldsForCurrentSeries = displayedFieldsMapSeries[ displayedFieldsKeyForCurrentSeries ]; + + // Do the update of the displayed fields using the roles + d->DisplayedFieldGenerator.updateDisplayedFieldsForInstance(sopInstanceUID, + displayedFieldsForCurrentSeries, displayedFieldsForCurrentStudy, displayedFieldsForCurrentPatient); + + // Set updated fields to the series / study / patient displayed fields maps + displayedFieldsMapSeries[ displayedFieldsKeyForCurrentSeries ] = displayedFieldsForCurrentSeries; + displayedFieldsMapStudy[ displayedFieldsKeyForCurrentStudy ] = displayedFieldsForCurrentStudy; + displayedFieldsVectorPatient[ displayedFieldsIndexForCurrentPatient ] = displayedFieldsForCurrentPatient; + } // For each instance + + emit displayedFieldsUpdateProgress(++progressValue); + + // Calculate number of images in each updated series + d->setCountToSeriesDisplayedFields(displayedFieldsMapSeries); + + emit displayedFieldsUpdateProgress(++progressValue); + + // Calculate number of series in each updated study + d->setNumberOfSeriesToStudyDisplayedFields(displayedFieldsMapStudy); + // Calculate number of studies in each updated patient + d->setNumberOfStudiesToPatientDisplayedFields(displayedFieldsVectorPatient); + + emit displayedFieldsUpdateProgress(++progressValue); + + // Update/insert the display values + if (displayedFieldsMapSeries.count() > 0) + { + QSqlQuery transaction(d->Database); + transaction.prepare("BEGIN TRANSACTION"); + transaction.exec(); + + if (d->applyDisplayedFieldsChanges(displayedFieldsMapSeries, displayedFieldsMapStudy, displayedFieldsVectorPatient)) + { + // Update image timestamp + newFilesQuery.first(); + newFilesQuery.previous(); // Need to go one before the first record + while (newFilesQuery.next()) + { + QSqlQuery updateDisplayedFieldsUpdatedTimestampStatement(d->Database); + QString updateDisplayedFieldsUpdatedTimestampStatementString = + QString("UPDATE IMAGES SET DisplayedFieldsUpdatedTimestamp=CURRENT_TIMESTAMP WHERE SOPInstanceUID='%1';").arg(newFilesQuery.value(0).toString()); + d->loggedExec(updateDisplayedFieldsUpdatedTimestampStatement, updateDisplayedFieldsUpdatedTimestampStatementString); + } + } + + transaction = QSqlQuery(d->Database); + transaction.prepare("END TRANSACTION"); + transaction.exec(); + } + + emit displayedFieldsUpdated(); + emit databaseChanged(); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::displayedNameForField(QString table, QString field) const +{ + Q_D(const ctkDICOMDatabase); + + QSqlQuery query( QString("SELECT DisplayedName FROM ColumnDisplayProperties WHERE TableName='%1' AND FieldName='%2';") + .arg(table).arg(field), d->Database ); + if (!query.exec()) + { + logger.error("SQLITE ERROR: " + query.lastError().driverText()); + return QString(); + } + + query.first(); + return query.value(0).toString(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::setDisplayedNameForField(QString table, QString field, QString displayedName) +{ + Q_D(ctkDICOMDatabase); + + if (!this->isOpen()) + { + logger.warn("Database needs to be open to set column display properties"); + return; + } + + QSqlQuery query(d->Database); + QString statement = QString("UPDATE ColumnDisplayProperties SET DisplayedName='%1' WHERE TableName='%2' AND FieldName='%3';") + .arg(displayedName).arg(table).arg(field); + if (!d->loggedExec(query, statement)) + { + logger.error("SQLITE ERROR: " + query.lastError().driverText()); + return; + } + + emit databaseChanged(); +} + +//------------------------------------------------------------------------------ +bool ctkDICOMDatabase::visibilityForField(QString table, QString field) const +{ + Q_D(const ctkDICOMDatabase); + + QSqlQuery query( QString("SELECT Visibility FROM ColumnDisplayProperties WHERE TableName='%1' AND FieldName='%2';") + .arg(table).arg(field), d->Database ); + if (!query.exec()) + { + logger.error("SQLITE ERROR: " + query.lastError().driverText()); + return false; + } + + query.first(); + return (query.value(0).toInt() != 0); +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::setVisibilityForField(QString table, QString field, bool visibility) +{ + Q_D(ctkDICOMDatabase); + + if (!this->isOpen()) + { + logger.warn("Database needs to be open to set column display properties"); + return; + } + + QSqlQuery query(d->Database); + QString statement = QString("UPDATE ColumnDisplayProperties SET Visibility=%1 WHERE TableName='%2' AND FieldName='%3';") + .arg(QString::number((int)visibility)).arg(table).arg(field); + if (!d->loggedExec(query, statement)) + { + logger.error("SQLITE ERROR: " + query.lastError().driverText()); + return; + } + + emit databaseChanged(); +} + +//------------------------------------------------------------------------------ +int ctkDICOMDatabase::weightForField(QString table, QString field) const +{ + Q_D(const ctkDICOMDatabase); + + QSqlQuery query( QString("SELECT Weight FROM ColumnDisplayProperties WHERE TableName='%1' AND FieldName='%2';") + .arg(table).arg(field), d->Database ); + if (!query.exec()) + { + logger.error("SQLITE ERROR: " + query.lastError().driverText()); + return INT_MAX; + } + + query.first(); + return query.value(0).toInt(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::setWeightForField(QString table, QString field, int weight) +{ + Q_D(ctkDICOMDatabase); + + if (!this->isOpen()) + { + logger.warn("Database needs to be open to set column display properties"); + return; + } + + QSqlQuery query(d->Database); + QString statement = QString("UPDATE ColumnDisplayProperties SET Weight=%1 WHERE TableName='%2' AND FieldName='%3';") + .arg(QString::number(weight)).arg(table).arg(field); + if (!d->loggedExec(query, statement)) + { + logger.error("SQLITE ERROR: " + query.lastError().driverText()); + return; + } + + emit databaseChanged(); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::formatForField(QString table, QString field) const +{ + Q_D(const ctkDICOMDatabase); + + QSqlQuery query( QString("SELECT Format FROM ColumnDisplayProperties WHERE TableName='%1' AND FieldName='%2';") + .arg(table).arg(field), d->Database ); + if (!query.exec()) + { + logger.error("SQLITE ERROR: " + query.lastError().driverText()); + return QString(); + } + + query.first(); + return query.value(0).toString(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMDatabase::setFormatForField(QString table, QString field, QString format) +{ + Q_D(ctkDICOMDatabase); + + if (!this->isOpen()) + { + logger.warn("Database needs to be open to set column display properties"); + return; + } + + QSqlQuery query(d->Database); + QString statement = QString("UPDATE ColumnDisplayProperties SET Format='%1' WHERE TableName='%2' AND FieldName='%3';") + .arg(format).arg(table).arg(field); + if (!d->loggedExec(query, statement)) + { + logger.error("SQLITE ERROR: " + query.lastError().driverText()); + return; + } + + emit databaseChanged(); +} diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.h b/Libs/DICOM/Core/ctkDICOMDatabase.h index a69566fd31..8398c84f81 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase.h @@ -175,10 +175,10 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject void setTagsToPrecache(const QStringList tags); const QStringList tagsToPrecache(); - /// Insert into the database if not already exsting. + /// Insert into the database if not already existing. /// @param dataset The dataset to store into the database. Usually, this is /// is a complete DICOM object, like a complete image. However - /// the database also inserts partial objects, like studyl + /// the database also inserts partial objects, like study /// information to the database, even if no image data is /// contained. This can be helpful to store results from /// querying the PACS for patient/study/series or image @@ -199,6 +199,13 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject bool createHierarchy = true, const QString& destinationDirectoryName = QString() ); + /// 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 + /// when we want to show a derived piece of information (such as image size or + /// number of studies in a patient). + Q_INVOKABLE void updateDisplayedFields(); + /// Reset cached item IDs to make sure previous /// inserts do not interfere with upcoming insert operations. /// Typically, it should be call just before a batch of files @@ -253,11 +260,31 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject Q_INVOKABLE bool initializeTagCache (); /// Return the value of a cached tag Q_INVOKABLE QString cachedTag (const QString sopInstanceUID, const QString tag); + /// Return the list of all cached tags and values for the specified sopInstanceUID. Returns with empty string if the tag is not present in the cache. + Q_INVOKABLE void getCachedTags(const QString sopInstanceUID, QMap &cachedTags); /// Insert an instance tag's value into to the cache 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); + /// Get displayed name of a given field + Q_INVOKABLE QString displayedNameForField(QString table, QString field) const; + /// Set displayed name of a given field + Q_INVOKABLE void setDisplayedNameForField(QString table, QString field, QString displayedName); + /// Get visibility of a given field + Q_INVOKABLE bool visibilityForField(QString table, QString field) const; + /// Set visibility of a given field + Q_INVOKABLE void setVisibilityForField(QString table, QString field, bool visibility); + /// Get weight of a given field. + /// Weight specifies the order of the field columns in the table. Smaller values are positioned towards the left ("heaviest sinks down") + Q_INVOKABLE int weightForField(QString table, QString field) const; + /// Set weight of a given field + /// Weight specifies the order of the field columns in the table. Smaller values are positioned towards the left ("heaviest sinks down") + Q_INVOKABLE void setWeightForField(QString table, QString field, int weight); + /// Get format of a given field + 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. @@ -277,21 +304,28 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// instanceAdded arguments: /// - instanceUID (unique) void instanceAdded(QString); - /// Indicates that an in-memory database has been updated + + /// Indicate that an in-memory database has been updated void databaseChanged(); - /// Indicates that the schema is about to be updated and how many files will be processed + + /// Indicate that the schema is about to be updated and how many files will be processed void schemaUpdateStarted(int); - /// Indicates progress in updating schema (int is file number, string is file name) + /// Indicate progress in updating schema (int is file number, string is file name) void schemaUpdateProgress(int); void schemaUpdateProgress(QString); - /// Indicates schema update finished + /// Indicate schema update finished void schemaUpdated(); + /// Trigger showing progress dialog for displayed fields update + void displayedFieldsUpdateStarted(); + /// Indicate progress in updating displayed fields (int is step number) + void displayedFieldsUpdateProgress(int); + /// Indicate displayed fields update finished + void displayedFieldsUpdated(); + protected: QScopedPointer d_ptr; - - private: Q_DECLARE_PRIVATE(ctkDICOMDatabase); Q_DISABLE_COPY(ctkDICOMDatabase); diff --git a/Libs/DICOM/Core/ctkDICOMDataset.h b/Libs/DICOM/Core/ctkDICOMDataset.h index 85c3749fe9..a4650237fe 100644 --- a/Libs/DICOM/Core/ctkDICOMDataset.h +++ b/Libs/DICOM/Core/ctkDICOMDataset.h @@ -1,10 +1,10 @@ -#ifndef CTK_DICOM_DATASET_H -#define CTK_DICOM_DATASET_H -#if defined _MSC_VER - #pragma message ( "Deprecated header ctkDICOMItem.h included. Please use ctkDICOMItem.h!" ) -#elif defined __GNUC__ - #warning "Deprecated header ctkDICOMItem.h included. Please use ctkDICOMItem.h!" -#endif -#include "ctkDICOMItem.h" -#endif - +#ifndef CTK_DICOM_DATASET_H +#define CTK_DICOM_DATASET_H +#if defined _MSC_VER + #pragma message ( "Deprecated header ctkDICOMItem.h included. Please use ctkDICOMItem.h!" ) +#elif defined __GNUC__ + #warning "Deprecated header ctkDICOMItem.h included. Please use ctkDICOMItem.h!" +#endif +#include "ctkDICOMItem.h" +#endif + diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.cpp b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.cpp new file mode 100644 index 0000000000..cedcab3c15 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.cpp @@ -0,0 +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) + { + rule->getDisplayedFieldsForInstance(cachedTagsForInstance, newFieldsSeries, newFieldsStudy, newFieldsPatient); + } + QMap initialFieldsSeries=displayedFieldsForCurrentSeries; + QMap initialFieldsStudy=displayedFieldsForCurrentStudy; + QMap initialFieldsPatient=displayedFieldsForCurrentPatient; + foreach(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule, d->AllRules) + { + 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 new file mode 100644 index 0000000000..4da4092803 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator.h @@ -0,0 +1,80 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef __ctkDICOMDisplayedFieldGenerator_h +#define __ctkDICOMDisplayedFieldGenerator_h + +// Qt includes +#include +#include + +#include "ctkDICOMCoreExport.h" + +class ctkDICOMDisplayedFieldGeneratorPrivate; +class ctkDICOMDisplayedFieldGeneratorAbstractRule; +class ctkDICOMDatabase; + +/// \ingroup DICOM_Core +/// +/// \brief Generates displayable data fields from DICOM tags +/// +/// The \sa updateDisplayedFieldsForInstance function is called from the DICOM database when update of the +/// displayed fields is needed. +/// +/// Displayed fields are determined by the rules, subclasses of ctkDICOMDisplayedFieldGeneratorAbstractRule. +/// The rules need to be registered to take part of the generation. When updating the displayed fields, +/// every rule defines the fields it is responsible for using the cached DICOM tags in the database. +/// Tags can be requested to be cached in the rules from the getRequiredDICOMTags function. After the fields +/// are defined in each rule, the results are merged together. The merging rules are also defined in the +/// rule classes. Each field can requested to be merged with "expect same value", which uses the only +/// non-empty value and throws a warning if conflicting values are encountered, or with "concatenate", +/// which simply concatenates the displayed field values together. +/// +class CTK_DICOM_CORE_EXPORT ctkDICOMDisplayedFieldGenerator : public QObject +{ + Q_OBJECT +public: + explicit ctkDICOMDisplayedFieldGenerator(QObject *parent = 0); + virtual ~ctkDICOMDisplayedFieldGenerator(); + + /// Set DICOM database + Q_INVOKABLE void setDatabase(ctkDICOMDatabase* database); + + /// Collect the DICOM tags required by all the registered rules + Q_INVOKABLE QStringList getRequiredTags(); + + /// Update displayed fields for an instance, invoking all registered rules + Q_INVOKABLE void updateDisplayedFieldsForInstance(QString sopInstanceUID, + QMap &displayedFieldsForCurrentSeries, + QMap &displayedFieldsForCurrentStudy, + QMap &displayedFieldsForCurrentPatient); + + /// Register new displayed field generator rule + void registerDisplayedFieldGeneratorRule(ctkDICOMDisplayedFieldGeneratorAbstractRule* rule); + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(ctkDICOMDisplayedFieldGenerator); + Q_DISABLE_COPY(ctkDICOMDisplayedFieldGenerator); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorAbstractRule.h b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorAbstractRule.h new file mode 100644 index 0000000000..1e802c8012 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorAbstractRule.h @@ -0,0 +1,164 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef __ctkDICOMDisplayedFieldGeneratorAbstractRule_h +#define __ctkDICOMDisplayedFieldGeneratorAbstractRule_h + +// Qt includes +#include +#include + +// DCMTK includes +#include + +#include "ctkDICOMCoreExport.h" + +class ctkDICOMDatabase; + +/// \ingroup DICOM_Core +/// +/// Abstract base class for generating displayed fields from DICOM fields. +/// +/// The displayed field generator classes need to be registered in \sa ctkDICOMDisplayedFieldGenerator +/// so that the rules it defines are taken into account when generating the displayed fields. +/// +class CTK_DICOM_CORE_EXPORT ctkDICOMDisplayedFieldGeneratorAbstractRule +{ +public: + /// Generate displayed fields for a certain instance based on its cached tags + /// Each rule plugin has the chance to fill any field in the series, study, and patient fields. + /// The way these generated fields will be used is defined by \sa mergeDisplayedFieldsForInstance + virtual void getDisplayedFieldsForInstance(const QMap &cachedTagsForInstance, QMap &displayedFieldsForCurrentSeries, + QMap &displayedFieldsForCurrentStudy, QMap &displayedFieldsForCurrentPatient)=0; + + /// Define rules of merging initial database values with new values generated by the rule plugins + /// Currently available options: + /// - \sa mergeExpectSameValue Use the non-empty field value. If both initial and new are non-empty and different, then use the initial value + /// - \sa mergeConcatenate Use the non-empty field value. If both initial and new are non-empty, then concatenate them using comma + virtual void mergeDisplayedFieldsForInstance( + const QMap &initialFieldsSeries, const QMap &initialFieldsStudy, const QMap &initialFieldsPatient, + const QMap &newFieldsSeries, const QMap &newFieldsStudy, const QMap &newFieldsPatient, + QMap &mergedFieldsSeries, QMap &mergedFieldsStudy, QMap &mergedFieldsPatient, + const QMap &emptyFieldsSeries, const QMap &emptyFieldsStudy, const QMap &emptyFieldsPatient + )=0; + + /// Specify list of DICOM tags required by the rule. These tags will be included in the tag cache + virtual QStringList getRequiredDICOMTags()=0; + + /// Utility function to convert a DICOM tag enum to string + static QString dicomTagToString(DcmTagKey& tag) + { + return QString("%1,%2").arg(tag.getGroup(),4,16,QLatin1Char('0')).arg(tag.getElement(),4,16,QLatin1Char('0')); + } + + /// Register placeholder strings that still mean that a given field can be considered empty. + /// Used when merging the original database content with the displayed fields generated by the rules. + /// Example: SeriesDescription -> Unnamed Series + virtual void registerEmptyFieldNames( + QMap emptyFieldsSeries, QMap emptyFieldsStudies, QMap emptyFieldsPatients)=0; + + /// Utility function determining whether a given field is considered empty + static bool isFieldEmpty(const QString &fieldName, const QMap &fields, const QMap &emptyValuesForEachField) + { + if (!fields.contains(fieldName)) + { + // the field is not present + return true; + } + if (fields[fieldName].isEmpty()) + { + // the field is present, but empty + return true; + } + if (emptyValuesForEachField[fieldName].contains(fields[fieldName])) + { + // the field is not empty, but contain a placeholder string (example: "No description") that means that the field is undefined + return true; + } + // this field is non-empty + return false; + } + + /// Merge function that only uses the new value if the initial value is empty and vice versa + static void mergeExpectSameValue( + const QString &fieldName, const QMap &initialFields, const QMap &newFields, + QMap &mergedFields, const QMap &emptyValuesForEachField ) + { + if (isFieldEmpty(fieldName, newFields, emptyValuesForEachField)) + { + // no new value is defined for this value, keep the initial value (if exists) + if (!isFieldEmpty(fieldName, initialFields, emptyValuesForEachField)) + { + mergedFields[fieldName]=initialFields[fieldName]; + } + return; + } + if (isFieldEmpty(fieldName, initialFields, emptyValuesForEachField)) + { + // no initial value is defined for this value, use just the new value (if exists) + if (!isFieldEmpty(fieldName, newFields, emptyValuesForEachField)) + { + mergedFields[fieldName]=newFields[fieldName]; + } + return; + } + // both initial and new value are defined and they are different => just keep using the old value + // TODO: log warning here, as this is not expected + mergedFields[fieldName]=initialFields[fieldName]; + } + + /// Merge function that sets merged value as a concatenation of the initial and new values + /// Individual values in the concatenation are separated by comma + static void mergeConcatenate( + const QString &fieldName, const QMap &initialFields, const QMap &newFields, + QMap &mergedFields, const QMap &emptyValuesForEachField ) + { + if (isFieldEmpty(fieldName, newFields, emptyValuesForEachField)) + { + // no new value is defined for this value, keep the initial value (if exists) + if (!isFieldEmpty(fieldName, initialFields, emptyValuesForEachField)) + { + mergedFields[fieldName]=initialFields[fieldName]; + } + return; + } + if (isFieldEmpty(fieldName, initialFields, emptyValuesForEachField)) + { + // no initial value is defined for this value, use just the new value (if exists) + if (!isFieldEmpty(fieldName, newFields, emptyValuesForEachField)) + { + mergedFields[fieldName]=newFields[fieldName]; + } + return; + } + QStringList initialValueSplit=initialFields[fieldName].split(","); + if (initialValueSplit.contains(newFields[fieldName])) + { + // the field is already contained in the list, so no need to add it + mergedFields[fieldName]=initialFields[fieldName]; + return; + } + // need to concatenate the new value to the initial + mergedFields[fieldName]=initialFields[fieldName]+", "+newFields[fieldName]; + } + +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorDefaultRule.cpp b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorDefaultRule.cpp new file mode 100644 index 0000000000..3176046b40 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorDefaultRule.cpp @@ -0,0 +1,181 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) PerkLab 2018 + + 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. + +=========================================================================*/ + +#include "ctkDICOMDisplayedFieldGeneratorDefaultRule.h" + +// dcmtk includes +#include "dcmtk/dcmdata/dcvrpn.h" + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDisplayedFieldGeneratorDefaultRule::getRequiredDICOMTags() +{ + QStringList requiredTags; + + requiredTags << dicomTagToString(DCM_SOPInstanceUID); + + requiredTags << dicomTagToString(DCM_PatientID); + requiredTags << dicomTagToString(DCM_PatientName); + requiredTags << dicomTagToString(DCM_PatientBirthDate); + requiredTags << dicomTagToString(DCM_PatientBirthTime); + requiredTags << dicomTagToString(DCM_PatientSex); + requiredTags << dicomTagToString(DCM_PatientAge); + requiredTags << dicomTagToString(DCM_PatientComments); + + requiredTags << dicomTagToString(DCM_StudyInstanceUID); + requiredTags << dicomTagToString(DCM_StudyID); + requiredTags << dicomTagToString(DCM_StudyDate); + requiredTags << dicomTagToString(DCM_StudyTime); + requiredTags << dicomTagToString(DCM_AccessionNumber); + requiredTags << dicomTagToString(DCM_ModalitiesInStudy); + requiredTags << dicomTagToString(DCM_InstitutionName); + requiredTags << dicomTagToString(DCM_PerformingPhysicianName); + requiredTags << dicomTagToString(DCM_ReferringPhysicianName); + requiredTags << dicomTagToString(DCM_StudyDescription); + + requiredTags << dicomTagToString(DCM_SeriesInstanceUID); + requiredTags << dicomTagToString(DCM_SeriesDate); + requiredTags << dicomTagToString(DCM_SeriesTime); + requiredTags << dicomTagToString(DCM_SeriesDescription); + requiredTags << dicomTagToString(DCM_Modality); + requiredTags << dicomTagToString(DCM_BodyPartExamined); + requiredTags << dicomTagToString(DCM_FrameOfReferenceUID); + requiredTags << dicomTagToString(DCM_ContrastBolusAgent); + requiredTags << dicomTagToString(DCM_ScanningSequence); + requiredTags << dicomTagToString(DCM_SeriesNumber); + requiredTags << dicomTagToString(DCM_AcquisitionNumber); + requiredTags << dicomTagToString(DCM_EchoNumbers); + requiredTags << dicomTagToString(DCM_TemporalPositionIdentifier); + + return requiredTags; +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGeneratorDefaultRule::registerEmptyFieldNames( + QMap emptyFieldsDisplaySeries, + QMap emptyFieldsDisplayStudies, + QMap emptyFieldsDisplayPatients ) +{ + emptyFieldsDisplaySeries.insertMulti("SeriesDescription", EMPTY_SERIES_DESCRIPTION); +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGeneratorDefaultRule::getDisplayedFieldsForInstance( + const QMap &cachedTagsForInstance, QMap &displayedFieldsForCurrentSeries, + QMap &displayedFieldsForCurrentStudy, QMap &displayedFieldsForCurrentPatient ) +{ + displayedFieldsForCurrentPatient["PatientsName"] = cachedTagsForInstance[dicomTagToString(DCM_PatientName)]; + displayedFieldsForCurrentPatient["PatientID"] = cachedTagsForInstance[dicomTagToString(DCM_PatientID)]; + displayedFieldsForCurrentPatient["DisplayedPatientsName"] = this->humanReadablePatientName(cachedTagsForInstance[dicomTagToString(DCM_PatientName)]); + + displayedFieldsForCurrentStudy["StudyInstanceUID"] = cachedTagsForInstance[dicomTagToString(DCM_StudyInstanceUID)]; + displayedFieldsForCurrentStudy["PatientIndex"] = displayedFieldsForCurrentPatient["PatientIndex"]; + displayedFieldsForCurrentStudy["StudyDescription"] = cachedTagsForInstance[dicomTagToString(DCM_StudyDescription)]; + displayedFieldsForCurrentStudy["StudyDate"] = cachedTagsForInstance[dicomTagToString(DCM_StudyDate)]; + displayedFieldsForCurrentStudy["ModalitiesInStudy"] = cachedTagsForInstance[dicomTagToString(DCM_ModalitiesInStudy)]; + displayedFieldsForCurrentStudy["InstitutionName"] = cachedTagsForInstance[dicomTagToString(DCM_InstitutionName)]; + displayedFieldsForCurrentStudy["ReferringPhysician"] = cachedTagsForInstance[dicomTagToString(DCM_ReferringPhysicianName)]; + + displayedFieldsForCurrentSeries["SeriesInstanceUID"] = cachedTagsForInstance[dicomTagToString(DCM_SeriesInstanceUID)]; + displayedFieldsForCurrentSeries["StudyInstanceUID"] = cachedTagsForInstance[dicomTagToString(DCM_StudyInstanceUID)]; + displayedFieldsForCurrentSeries["SeriesNumber"] = cachedTagsForInstance[dicomTagToString(DCM_SeriesNumber)]; + displayedFieldsForCurrentSeries["Modality"] = cachedTagsForInstance[dicomTagToString(DCM_Modality)]; + displayedFieldsForCurrentPatient["SeriesDescription"] = cachedTagsForInstance[dicomTagToString(DCM_SeriesDescription)]; + if ( cachedTagsForInstance.contains(dicomTagToString(DCM_Rows)) && !cachedTagsForInstance[dicomTagToString(DCM_Rows)].isEmpty() + && cachedTagsForInstance.contains(dicomTagToString(DCM_Columns)) && !cachedTagsForInstance[dicomTagToString(DCM_Columns)].isEmpty() ) + { + QString rows = cachedTagsForInstance[dicomTagToString(DCM_Rows)]; + QString columns = cachedTagsForInstance[dicomTagToString(DCM_Columns)]; + displayedFieldsForCurrentSeries["DisplayedSize"] = QString("%1x%2").arg(columns).arg(rows); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGeneratorDefaultRule::mergeDisplayedFieldsForInstance( + const QMap &initialFieldsSeries, const QMap &initialFieldsStudy, const QMap &initialFieldsPatient, + const QMap &newFieldsSeries, const QMap &newFieldsStudy, const QMap &newFieldsPatient, + QMap &mergedFieldsSeries, QMap &mergedFieldsStudy, QMap &mergedFieldsPatient, + const QMap &emptyFieldsSeries, const QMap &emptyFieldsStudy, const QMap &emptyFieldsPatient + ) +{ + mergeExpectSameValue("PatientIndex", initialFieldsPatient, newFieldsPatient, mergedFieldsPatient, emptyFieldsPatient); + mergeExpectSameValue("PatientsName", initialFieldsPatient, newFieldsPatient, mergedFieldsPatient, emptyFieldsPatient); + mergeExpectSameValue("PatientID", initialFieldsPatient, newFieldsPatient, mergedFieldsPatient, emptyFieldsPatient); + mergeExpectSameValue("DisplayedPatientsName", initialFieldsPatient, newFieldsPatient, mergedFieldsPatient, emptyFieldsPatient); + + mergeExpectSameValue("StudyInstanceUID", initialFieldsStudy, newFieldsStudy, mergedFieldsStudy, emptyFieldsStudy); + mergeExpectSameValue("PatientIndex", initialFieldsStudy, newFieldsStudy, mergedFieldsStudy, emptyFieldsStudy); + mergeConcatenate ("StudyDescription", initialFieldsStudy, newFieldsStudy, mergedFieldsStudy, emptyFieldsStudy); + mergeExpectSameValue("StudyDate", initialFieldsStudy, newFieldsStudy, mergedFieldsStudy, emptyFieldsStudy); + mergeConcatenate ("ModalitiesInStudy", initialFieldsStudy, newFieldsStudy, mergedFieldsStudy, emptyFieldsStudy); + mergeExpectSameValue("InstitutionName", initialFieldsStudy, newFieldsStudy, mergedFieldsStudy, emptyFieldsStudy); + mergeConcatenate ("ReferringPhysician", initialFieldsStudy, newFieldsStudy, mergedFieldsStudy, emptyFieldsStudy); + + mergeExpectSameValue("SeriesInstanceUID", initialFieldsSeries, newFieldsSeries, mergedFieldsSeries, emptyFieldsSeries); + mergeExpectSameValue("StudyInstanceUID", initialFieldsSeries, newFieldsSeries, mergedFieldsSeries, emptyFieldsSeries); + mergeExpectSameValue("SeriesNumber", initialFieldsSeries, newFieldsSeries, mergedFieldsSeries, emptyFieldsSeries); + mergeExpectSameValue("Modality", initialFieldsSeries, newFieldsSeries, mergedFieldsSeries, emptyFieldsSeries); + mergeConcatenate ("SeriesDescription", initialFieldsSeries, newFieldsSeries, mergedFieldsSeries, emptyFieldsSeries); + mergeExpectSameValue("DisplayedSize", initialFieldsSeries, newFieldsSeries, mergedFieldsSeries, emptyFieldsSeries); +} + +//------------------------------------------------------------------------------ +QString ctkDICOMDisplayedFieldGeneratorDefaultRule::humanReadablePatientName(QString dicomPatientName) +{ + OFString dicomName(dicomPatientName.toLatin1().constData()); + OFString formattedName; + OFString lastName, firstName, middleName, namePrefix, nameSuffix; + OFCondition l_error = DcmPersonName::getNameComponentsFromString( + dicomName, lastName, firstName, middleName, namePrefix, nameSuffix); + if (l_error.good()) + { + formattedName.clear(); + // concatenate name components per this convention Last, First Middle, Suffix (Prefix) + if (!lastName.empty()) + { + formattedName += lastName; + if (!(firstName.empty() && middleName.empty())) + { + formattedName += ","; + } + } + if (!firstName.empty()) + { + formattedName += " "; + formattedName += firstName; + } + if (!middleName.empty()) + { + formattedName += " "; + formattedName += middleName; + } + if (!nameSuffix.empty()) + { + formattedName += ", "; + formattedName += nameSuffix; + } + if (!namePrefix.empty()) + { + formattedName += " ("; + formattedName += namePrefix; + formattedName += ")"; + } + } + return QString(formattedName.c_str()); +} diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorDefaultRule.h b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorDefaultRule.h new file mode 100644 index 0000000000..d5f24d129a --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorDefaultRule.h @@ -0,0 +1,62 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef __ctkDICOMDisplayedFieldGeneratorDefaultRule_h +#define __ctkDICOMDisplayedFieldGeneratorDefaultRule_h + +// Qt includes +#include + +#include "ctkDICOMDisplayedFieldGeneratorAbstractRule.h" + +#define EMPTY_SERIES_DESCRIPTION "Unnamed Series" + +/// \ingroup DICOM_Core +/// +/// Default rule for generating displayed fields from DICOM fields +class CTK_DICOM_CORE_EXPORT ctkDICOMDisplayedFieldGeneratorDefaultRule : public ctkDICOMDisplayedFieldGeneratorAbstractRule +{ +public: + /// Specify list of DICOM tags required by the rule. These tags will be included in the tag cache + virtual QStringList getRequiredDICOMTags(); + + /// Register placeholder strings that still mean that a given field can be considered empty. + /// Used when merging the original database content with the displayed fields generated by the rules. + /// Example: SeriesDescription -> Unnamed Series + virtual void registerEmptyFieldNames(QMap emptyFieldsSeries, QMap emptyFieldstudies, QMap emptyFieldsPatients); + + /// Generate displayed fields for a certain instance based on its cached tags + /// The way these generated fields will be used is defined by \sa mergeDisplayedFieldsForInstance + virtual void getDisplayedFieldsForInstance( + const QMap &cachedTagsForInstance, QMap &displayedFieldsForCurrentSeries, + QMap &displayedFieldsForCurrentStudy, QMap &displayedFieldsForCurrentPatient ); + + /// Define rules of merging initial database values with new values generated by the rule plugins + virtual void mergeDisplayedFieldsForInstance( + const QMap &initialFieldsSeries, const QMap &initialFieldsStudy, const QMap &initialFieldsPatient, + const QMap &newFieldsSeries, const QMap &newFieldsStudy, const QMap &newFieldsPatient, + QMap &mergedFieldsSeries, QMap &mergedFieldsStudy, QMap &mergedFieldsPatient, + const QMap &emptyFieldsSeries, const QMap &emptyFieldsStudy, const QMap &emptyFieldsPatient ); + +protected: + QString humanReadablePatientName(QString dicomPatientName); +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.cpp b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.cpp new file mode 100644 index 0000000000..6e3fccdb72 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.cpp @@ -0,0 +1,130 @@ +/*========================================================================= + + Library: CTK + + Copyright (c) PerkLab 2018 + + 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. + +=========================================================================*/ + +#include "ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.h" + +//------------------------------------------------------------------------------ +ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule::ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule() + : EmptySeriesDescriptionRtPlan("Unnamed RT Plan") + , EmptySeriesDescriptionRtStruct("Unnamed RT Structure Set") + , EmptySeriesDescriptionRtImage("Unnamed RT Image") +{ +} + +//------------------------------------------------------------------------------ +QStringList ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule::getRequiredDICOMTags() +{ + QStringList requiredTags; + + requiredTags << dicomTagToString(DCM_Modality); + + requiredTags << dicomTagToString(DCM_RTPlanName); + requiredTags << dicomTagToString(DCM_RTPlanLabel); + //requiredTags << dicomTagToString(DCM_RTPlanDate); + //requiredTags << dicomTagToString(DCM_RTPlanTime); + //requiredTags << dicomTagToString(DCM_RTPlanDescription); + + requiredTags << dicomTagToString(DCM_StructureSetName); + requiredTags << dicomTagToString(DCM_StructureSetLabel); + //requiredTags << dicomTagToString(DCM_StructureSetDescription); + //requiredTags << dicomTagToString(DCM_StructureSetDate); + //requiredTags << dicomTagToString(DCM_StructureSetTime); + + requiredTags << dicomTagToString(DCM_RTImageName); + requiredTags << dicomTagToString(DCM_RTImageLabel); + requiredTags << dicomTagToString(DCM_RTImageDescription); + + return requiredTags; +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule::registerEmptyFieldNames( + QMap emptyFieldsDisplaySeries, + QMap emptyFieldsDisplayStudies, + QMap emptyFieldsDisplayPatients ) +{ + emptyFieldsDisplaySeries.insertMulti("SeriesDescription", this->EmptySeriesDescriptionRtPlan); + emptyFieldsDisplaySeries.insertMulti("SeriesDescription", this->EmptySeriesDescriptionRtStruct); + emptyFieldsDisplaySeries.insertMulti("SeriesDescription", this->EmptySeriesDescriptionRtImage); +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule::getDisplayedFieldsForInstance( + const QMap &cachedTagsForInstance, QMap &displayedFieldsForCurrentSeries, + QMap &displayedFieldsForCurrentStudy, QMap &displayedFieldsForCurrentPatient ) +{ + QString modality = cachedTagsForInstance[dicomTagToString(DCM_Modality)]; + if (!modality.compare("RTPLAN")) + { + if (!cachedTagsForInstance[dicomTagToString(DCM_RTPlanName)].isEmpty()) + { + displayedFieldsForCurrentSeries["SeriesDescription"] = cachedTagsForInstance[dicomTagToString(DCM_RTPlanName)]; + } + else if (!cachedTagsForInstance[dicomTagToString(DCM_RTPlanLabel)].isEmpty()) + { + displayedFieldsForCurrentSeries["SeriesDescription"] = cachedTagsForInstance[dicomTagToString(DCM_RTPlanLabel)]; + } + else + { + displayedFieldsForCurrentSeries["SeriesDescription"] = QString(this->EmptySeriesDescriptionRtPlan); + } + } + else if (!modality.compare("RTSTRUCT")) + { + if (!cachedTagsForInstance[dicomTagToString(DCM_StructureSetName)].isEmpty()) + { + displayedFieldsForCurrentSeries["SeriesDescription"] = cachedTagsForInstance[dicomTagToString(DCM_StructureSetName)]; + } + else if (!cachedTagsForInstance[dicomTagToString(DCM_StructureSetLabel)].isEmpty()) + { + displayedFieldsForCurrentSeries["SeriesDescription"] = cachedTagsForInstance[dicomTagToString(DCM_StructureSetLabel)]; + } + else + { + displayedFieldsForCurrentSeries["SeriesDescription"] = QString(this->EmptySeriesDescriptionRtStruct); + } + } + else if (!modality.compare("RTIMAGE")) + { + if (!cachedTagsForInstance[dicomTagToString(DCM_RTImageName)].isEmpty()) + { + displayedFieldsForCurrentSeries["SeriesDescription"] = cachedTagsForInstance[dicomTagToString(DCM_RTImageName)]; + } + else if (!cachedTagsForInstance[dicomTagToString(DCM_RTImageLabel)].isEmpty()) + { + displayedFieldsForCurrentSeries["SeriesDescription"] = cachedTagsForInstance[dicomTagToString(DCM_RTImageLabel)]; + } + else + { + displayedFieldsForCurrentSeries["SeriesDescription"] = QString(this->EmptySeriesDescriptionRtImage); + } + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule::mergeDisplayedFieldsForInstance( + const QMap &initialFieldsSeries, const QMap &initialFieldsStudy, const QMap &initialFieldsPatient, + const QMap &newFieldsSeries, const QMap &newFieldsStudy, const QMap &newFieldsPatient, + QMap &mergedFieldsSeries, QMap &mergedFieldsStudy, QMap &mergedFieldsPatient, + const QMap &emptyFieldsSeries, const QMap &emptyFieldsStudy, const QMap &emptyFieldsPatient + ) +{ + mergeConcatenate("SeriesDescription", initialFieldsSeries, newFieldsSeries, mergedFieldsSeries, emptyFieldsSeries); +} diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.h b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.h new file mode 100644 index 0000000000..5f432df634 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule.h @@ -0,0 +1,65 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef __ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule_h +#define __ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule_h + +// Qt includes +#include + +#include "ctkDICOMDisplayedFieldGeneratorAbstractRule.h" + +/// \ingroup DICOM_Core +/// +/// Default rule for generating displayed fields from DICOM fields +class CTK_DICOM_CORE_EXPORT ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule : public ctkDICOMDisplayedFieldGeneratorAbstractRule +{ +public: + /// Constructor + explicit ctkDICOMDisplayedFieldGeneratorRadiotherapySeriesDescriptionRule(); + + /// Specify list of DICOM tags required by the rule. These tags will be included in the tag cache + virtual QStringList getRequiredDICOMTags(); + + /// Register placeholder strings that still mean that a given field can be considered empty. + /// Used when merging the original database content with the displayed fields generated by the rules. + /// Example: SeriesDescription -> Unnamed Series + virtual void registerEmptyFieldNames(QMap emptyFieldsSeries, QMap emptyFieldsStudies, QMap emptyFieldsPatients); + + /// Generate displayed fields for a certain instance based on its cached tags + /// The way these generated fields will be used is defined by \sa mergeDisplayedFieldsForInstance + virtual void getDisplayedFieldsForInstance( + const QMap &cachedTagsForInstance, QMap &displayedFieldsForCurrentSeries, + QMap &displayedFieldsForCurrentStudy, QMap &displayedFieldsForCurrentPatient ); + + /// Define rules of merging initial database values with new values generated by the rule plugins + virtual void mergeDisplayedFieldsForInstance( + const QMap &initialFieldsSeries, const QMap &initialFieldsStudy, const QMap &initialFieldsPatient, + const QMap &newFieldsSeries, const QMap &newFieldsStudy, const QMap &newFieldsPatient, + QMap &mergedFieldsSeries, QMap &mergedFieldsStudy, QMap &mergedFieldsPatient, + const QMap &emptyFieldsSeries, const QMap &emptyFieldsStudy, const QMap &emptyFieldsPatient ); + +protected: + const QString EmptySeriesDescriptionRtPlan; + const QString EmptySeriesDescriptionRtStruct; + const QString EmptySeriesDescriptionRtImage; +}; + +#endif diff --git a/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator_p.h b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator_p.h new file mode 100644 index 0000000000..77a14ca1d1 --- /dev/null +++ b/Libs/DICOM/Core/ctkDICOMDisplayedFieldGenerator_p.h @@ -0,0 +1,58 @@ +/*========================================================================= + + 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. + +=========================================================================*/ + +#ifndef CTKDICOMDisplayedFieldGeneratorPrivate_H +#define CTKDICOMDisplayedFieldGeneratorPrivate_H + +#include +#include +#include +#include + +#include "ctkDICOMDisplayedFieldGenerator.h" + +class ctkDICOMDatabase; +class ctkDICOMDisplayedFieldGeneratorAbstractRule; + +//------------------------------------------------------------------------------ +class ctkDICOMDisplayedFieldGeneratorPrivate : public QObject +{ + Q_OBJECT + + Q_DECLARE_PUBLIC(ctkDICOMDisplayedFieldGenerator); + +protected: + ctkDICOMDisplayedFieldGenerator* const q_ptr; + +public: + ctkDICOMDisplayedFieldGeneratorPrivate(ctkDICOMDisplayedFieldGenerator&); + ~ctkDICOMDisplayedFieldGeneratorPrivate(); + +public: + QList AllRules; + ctkDICOMDatabase* Database; + + QMap EmptyFieldNamesPatients; + QMap EmptyFieldNamesStudies; + QMap EmptyFieldNamesSeries; +}; + + +#endif // CTKDICOMDisplayedFieldGeneratorPrivate_H diff --git a/Libs/DICOM/Core/ctkDICOMIndexer.cpp b/Libs/DICOM/Core/ctkDICOMIndexer.cpp index d276e494f1..7ba39ce704 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer.cpp +++ b/Libs/DICOM/Core/ctkDICOMIndexer.cpp @@ -140,31 +140,34 @@ void ctkDICOMIndexer::addListOfFiles(ctkDICOMDatabase& database, QTime timeProbe; timeProbe.start(); d->Canceled = false; - int CurrentFileIndex = 0; + int currentFileIndex = 0; int lastReportedPercent = 0; foreach(QString filePath, listOfFiles) { - int percent = ( 100 * CurrentFileIndex ) / listOfFiles.size(); + 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++; + currentFileIndex++; - if( d->Canceled ) - { + if (d->Canceled) + { break; - } + } } + + // 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)); + qDebug() << QString("DICOM indexer has successfully processed %1 files [%2s]") + .arg(currentFileIndex).arg(QString::number(elapsedTimeInSeconds,'f', 2)); } //------------------------------------------------------------------------------ diff --git a/Libs/DICOM/Core/ctkDICOMIndexer.h b/Libs/DICOM/Core/ctkDICOMIndexer.h index f055f07a85..1b8ff18336 100644 --- a/Libs/DICOM/Core/ctkDICOMIndexer.h +++ b/Libs/DICOM/Core/ctkDICOMIndexer.h @@ -148,6 +148,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMIndexer : public QObject void progress(int); void indexingComplete(); + /// Trigger showing progress dialog for displayed fields update + void displayedFieldsUpdateStarted(); + public Q_SLOTS: void cancel(); diff --git a/Libs/DICOM/Core/ctkDICOMItem.cpp b/Libs/DICOM/Core/ctkDICOMItem.cpp index be33e90547..989f4bc3e6 100644 --- a/Libs/DICOM/Core/ctkDICOMItem.cpp +++ b/Libs/DICOM/Core/ctkDICOMItem.cpp @@ -940,6 +940,11 @@ QString ctkDICOMItem::TagKey( const DcmTag& tag ) return QString("(%1,%2)").arg( tag.getGroup(), 4, 16, QLatin1Char('0')).arg( tag.getElement(), 4, 16, QLatin1Char('0') ); } +QString ctkDICOMItem::TagKeyStripped( const DcmTag& tag ) +{ + return QString("%1,%2").arg( tag.getGroup(), 4, 16, QLatin1Char('0')).arg( tag.getElement(), 4, 16, QLatin1Char('0') ); +} + QString ctkDICOMItem::TagDescription( const DcmTag& tag ) { if (!dcmDataDict.isDictionaryLoaded()) diff --git a/Libs/DICOM/Core/ctkDICOMItem.h b/Libs/DICOM/Core/ctkDICOMItem.h index b76bf113f2..88cd96e211 100644 --- a/Libs/DICOM/Core/ctkDICOMItem.h +++ b/Libs/DICOM/Core/ctkDICOMItem.h @@ -223,6 +223,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMItem /// \brief Nicely formatted (group,element) version of a tag /// static QString TagKey( const DcmTag& tag ); + /// \brief Formatted group,element version of a tag. + /// The difference from \sa TagKey is the lack of parentheses + static QString TagKeyStripped( const DcmTag& tag ); /// /// \brief Description (name) of the tag diff --git a/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp b/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp index 0a3fd28fd5..567560e83a 100644 --- a/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp @@ -18,9 +18,6 @@ =========================================================================*/ -// std includes -#include - // Qt includes #include #include @@ -30,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -84,6 +82,7 @@ class ctkDICOMBrowserPrivate: public Ui_ctkDICOMBrowser QSharedPointer DICOMIndexer; QProgressDialog *IndexerProgress; QProgressDialog *UpdateSchemaProgress; + QProgressDialog *UpdateDisplayedFieldsProgress; QProgressDialog *ExportProgress; void showIndexerDialog(); @@ -92,9 +91,11 @@ class ctkDICOMBrowserPrivate: public Ui_ctkDICOMBrowser // used when suspending the ctkDICOMModel QSqlDatabase EmptyDatabase; + bool DisplayImportSummary; + bool ConfirmRemove; + // local count variables to keep track of the number of items // added to the database during an import operation - bool DisplayImportSummary; int PatientsAddedDuringImport; int StudiesAddedDuringImport; int SeriesAddedDuringImport; @@ -142,8 +143,10 @@ ctkDICOMBrowserPrivate::ctkDICOMBrowserPrivate(ctkDICOMBrowser* parent): q_ptr(p DICOMIndexer = QSharedPointer (new ctkDICOMIndexer); IndexerProgress = 0; UpdateSchemaProgress = 0; + UpdateDisplayedFieldsProgress = 0; ExportProgress = 0; DisplayImportSummary = true; + ConfirmRemove = false; PatientsAddedDuringImport = 0; StudiesAddedDuringImport = 0; SeriesAddedDuringImport = 0; @@ -154,17 +157,21 @@ ctkDICOMBrowserPrivate::ctkDICOMBrowserPrivate(ctkDICOMBrowser* parent): q_ptr(p ctkDICOMBrowserPrivate::~ctkDICOMBrowserPrivate() { if ( IndexerProgress ) - { + { delete IndexerProgress; - } + } if ( UpdateSchemaProgress ) - { + { delete UpdateSchemaProgress; - } + } + if ( UpdateDisplayedFieldsProgress ) + { + delete UpdateDisplayedFieldsProgress; + } if ( ExportProgress ) - { + { delete ExportProgress; - } + } } //---------------------------------------------------------------------------- @@ -172,33 +179,27 @@ void ctkDICOMBrowserPrivate::showUpdateSchemaDialog() { Q_Q(ctkDICOMBrowser); if (UpdateSchemaProgress == 0) - { + { // // Set up the Update Schema Progress Dialog // UpdateSchemaProgress = new QProgressDialog( - q->tr("DICOM Schema Update"), "Cancel", 0, 100, q, - Qt::WindowTitleHint | Qt::WindowSystemMenuHint); + q->tr("DICOM Schema Update"), "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 + // 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...")); UpdateSchemaProgress->setLabel(progressLabel); UpdateSchemaProgress->setWindowModality(Qt::ApplicationModal); UpdateSchemaProgress->setMinimumDuration(0); UpdateSchemaProgress->setValue(0); - q->connect(DICOMDatabase.data(), SIGNAL(schemaUpdateStarted(int)), - UpdateSchemaProgress, SLOT(setMaximum(int))); - q->connect(DICOMDatabase.data(), SIGNAL(schemaUpdateProgress(int)), - UpdateSchemaProgress, SLOT(setValue(int))); - q->connect(DICOMDatabase.data(), SIGNAL(schemaUpdateProgress(QString)), - progressLabel, SLOT(setText(QString))); + q->connect(DICOMDatabase.data(), SIGNAL(schemaUpdateStarted(int)), UpdateSchemaProgress, SLOT(setMaximum(int))); + q->connect(DICOMDatabase.data(), SIGNAL(schemaUpdateProgress(int)), UpdateSchemaProgress, SLOT(setValue(int))); + q->connect(DICOMDatabase.data(), SIGNAL(schemaUpdateProgress(QString)), progressLabel, SLOT(setText(QString))); // close the dialog - q->connect(DICOMDatabase.data(), SIGNAL(schemaUpdated()), - UpdateSchemaProgress, SLOT(close())); - } + q->connect(DICOMDatabase.data(), SIGNAL(schemaUpdated()), UpdateSchemaProgress, SLOT(close())); + } UpdateSchemaProgress->show(); } @@ -207,7 +208,7 @@ void ctkDICOMBrowserPrivate::showIndexerDialog() { Q_Q(ctkDICOMBrowser); if (IndexerProgress == 0) - { + { // // Set up the Indexer Progress Dialog // @@ -222,29 +223,21 @@ void ctkDICOMBrowserPrivate::showIndexerDialog() IndexerProgress->setMinimumDuration(0); IndexerProgress->setValue(0); - q->connect(IndexerProgress, SIGNAL(canceled()), - DICOMIndexer.data(), SLOT(cancel())); + q->connect(IndexerProgress, SIGNAL(canceled()), DICOMIndexer.data(), SLOT(cancel())); - 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))); + 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))); // close the dialog - q->connect(DICOMIndexer.data(), SIGNAL(indexingComplete()), - IndexerProgress, SLOT(close())); + 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())); + 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())); - } + q->connect(IndexerProgress, SIGNAL(canceled()), q, SIGNAL(directoryImported())); + q->connect(DICOMIndexer.data(), SIGNAL(indexingComplete()), q, SIGNAL(directoryImported())); + } IndexerProgress->show(); } @@ -261,9 +254,8 @@ ctkDICOMBrowser::ctkDICOMBrowser(QWidget* _parent):Superclass(_parent), d->setupUi(this); - // signals related to tracking inserts - connect(d->DICOMDatabase.data(), SIGNAL(patientAdded(int,QString,QString,QString)), this, - SLOT(onPatientAdded(int,QString,QString,QString))); + // Signals related to tracking inserts + connect(d->DICOMDatabase.data(), SIGNAL(patientAdded(int,QString,QString,QString)), this, SLOT(onPatientAdded(int,QString,QString,QString))); connect(d->DICOMDatabase.data(), SIGNAL(studyAdded(QString)), this, SLOT(onStudyAdded(QString))); connect(d->DICOMDatabase.data(), SIGNAL(seriesAdded(QString)), this, SLOT(onSeriesAdded(QString))); connect(d->DICOMDatabase.data(), SIGNAL(instanceAdded(QString)), this, SLOT(onInstanceAdded(QString))); @@ -271,22 +263,29 @@ ctkDICOMBrowser::ctkDICOMBrowser(QWidget* _parent):Superclass(_parent), connect(d->tableDensityComboBox ,SIGNAL(currentIndexChanged (const QString&)), this, SLOT(onTablesDensityComboBox(QString))); - //Set ToolBar button style + connect(d->DirectoryButton, SIGNAL(directoryChanged(QString)), this, SLOT(setDatabaseDirectory(QString))); + + // Signal for displayed fields update + connect(d->DICOMDatabase.data(), SIGNAL(displayedFieldsUpdateStarted()), this, SLOT(showUpdateDisplayedFieldsDialog())); + connect(d->DICOMIndexer.data(), SIGNAL(displayedFieldsUpdateStarted()), this, SLOT(showUpdateDisplayedFieldsDialog())); + + // Set ToolBar button style d->ToolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); - //Initialize Q/R widget + // Initialize Q/R widget d->QueryRetrieveWidget = new ctkDICOMQueryRetrieveWidget(); d->QueryRetrieveWidget->setWindowModality ( Qt::ApplicationModal ); - //initialize directory from settings, then listen for changes + // initialize directory from settings, then listen for changes QSettings settings; if ( settings.value(Self::databaseDirectorySettingsKey(), "") == "" ) - { + { settings.setValue(Self::databaseDirectorySettingsKey(), QString("./ctkDICOM-Database")); settings.sync(); - } + } QString databaseDirectory = this->databaseDirectory(); this->setDatabaseDirectory(databaseDirectory); + databaseDirectory = this->databaseDirectory(); // In case a new database has been created instead of updating schema d->DirectoryButton->setDirectory(databaseDirectory); d->dicomTableManager->setDICOMDatabase(d->DICOMDatabase.data()); @@ -307,8 +306,6 @@ ctkDICOMBrowser::ctkDICOMBrowser(QWidget* _parent):Superclass(_parent), connect(d->dicomTableManager, SIGNAL(seriesRightClicked(const QPoint&)), this, SLOT(onSeriesRightClicked(const QPoint&))); - connect(d->DirectoryButton, SIGNAL(directoryChanged(QString)), this, SLOT(setDatabaseDirectory(QString))); - // Initialize directoryMode widget QFormLayout *layout = new QFormLayout; QComboBox* importDirectoryModeComboBox = new QComboBox(); @@ -372,6 +369,22 @@ void ctkDICOMBrowser::setDisplayImportSummary(bool onOff) d->DisplayImportSummary = onOff; } +//---------------------------------------------------------------------------- +bool ctkDICOMBrowser::confirmRemove() +{ + Q_D(ctkDICOMBrowser); + + return d->ConfirmRemove; +} + +//---------------------------------------------------------------------------- +void ctkDICOMBrowser::setConfirmRemove(bool onOff) +{ + Q_D(ctkDICOMBrowser); + + d->ConfirmRemove = onOff; +} + //---------------------------------------------------------------------------- int ctkDICOMBrowser::patientsAddedDuringImport() { @@ -405,13 +418,98 @@ int ctkDICOMBrowser::instancesAddedDuringImport() } //---------------------------------------------------------------------------- -void ctkDICOMBrowser::updateDatabaseSchemaIfNeeded() +bool ctkDICOMBrowser::updateDatabaseSchemaIfNeeded() { - Q_D(ctkDICOMBrowser); - d->showUpdateSchemaDialog(); - d->DICOMDatabase->updateSchemaIfNeeded(); + if (d->DICOMDatabase->schemaVersionLoaded() != d->DICOMDatabase->schemaVersion()) + { + 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(" Create new database "), QMessageBox::RejectRole); + schemaUpdateMsgBox.setDefaultButton(updateButton); + schemaUpdateMsgBox.exec(); + if (schemaUpdateMsgBox.clickedButton() == updateButton) + { + updateSchema = true; + } + else + { + // Have user select new database folder + // (cannot simply call d->DirectoryButton->browse() because it will cause circular calls) + + // See https://bugreports.qt-project.org/browse/QTBUG-10244 + class ExcludeReadOnlyFilterProxyModel : public QSortFilterProxyModel + { + public: + ExcludeReadOnlyFilterProxyModel(QPalette palette, QObject *parent) + : QSortFilterProxyModel(parent) + , Palette(palette) + { + } + virtual Qt::ItemFlags flags(const QModelIndex& index)const + { + 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); + } + QPalette Palette; + }; + + QScopedPointer fileDialog( + new ctkFileDialog(this, "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 are still openable 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())); + + QString dir; + 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()) + { + qCritical() << Q_FUNC_INFO << ": Either user canceled database folder dialog or the selected directory is readonly"; + return false; + } + + this->setDatabaseDirectory(dir); + return true; + } + } + + if (updateSchema) + { + d->showUpdateSchemaDialog(); + d->DICOMDatabase->updateSchema(); + } + } + + return false; } //---------------------------------------------------------------------------- @@ -421,34 +519,38 @@ void ctkDICOMBrowser::setDatabaseDirectory(const QString& directory) // If needed, create database directory if (!QDir(directory).exists()) - { + { QDir().mkdir(directory); - } + } QSettings settings; settings.setValue(Self::databaseDirectorySettingsKey(), directory); settings.sync(); - //close the active DICOM database + // close the active DICOM database d->DICOMDatabase->closeDatabase(); - //open DICOM database on the directory + // open DICOM database on the directory QString databaseFileName = directory + QString("/ctkDICOM.sql"); try - { + { d->DICOMDatabase->openDatabase( databaseFileName ); - } + } catch (std::exception e) - { + { std::cerr << "Database error: " << qPrintable(d->DICOMDatabase->lastError()) << "\n"; d->DICOMDatabase->closeDatabase(); return; - } + } // update the database schema if needed and provide progress - this->updateDatabaseSchemaIfNeeded(); + if (this->updateDatabaseSchemaIfNeeded()) + { + // If new database is selected then do not make the calls below here to prevent circular calls + return; + } - //pass DICOM database instance to Import widget + // pass DICOM database instance to Import widget d->QueryRetrieveWidget->setRetrieveDatabase(d->DICOMDatabase); // update the button and let any connected slots know about the change @@ -524,7 +626,6 @@ void ctkDICOMBrowser::openQueryDialog() d->QueryRetrieveWidget->show(); d->QueryRetrieveWidget->raise(); - } //---------------------------------------------------------------------------- @@ -537,22 +638,29 @@ void ctkDICOMBrowser::onQueryRetrieveFinished() void ctkDICOMBrowser::onRemoveAction() { Q_D(ctkDICOMBrowser); - QStringList selectedSeriesUIDs = d->dicomTableManager->currentSeriesSelection(); + 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); - } + { + d->DICOMDatabase->removeSeries(uid); + } QStringList selectedStudiesUIDs = d->dicomTableManager->currentStudiesSelection(); foreach (const QString& uid, selectedStudiesUIDs) - { - d->DICOMDatabase->removeStudy(uid); - } - QStringList selectedPatientUIDs = d->dicomTableManager->currentPatientsSelection(); + { + d->DICOMDatabase->removeStudy(uid); + } foreach (const QString& uid, selectedPatientUIDs) - { - d->DICOMDatabase->removePatient(uid); - } + { + d->DICOMDatabase->removePatient(uid); + } // Update the table views d->dicomTableManager->updateTableViews(); } @@ -589,7 +697,6 @@ void ctkDICOMBrowser::onRepairAction() repairMessageBox->addButton(QMessageBox::Ok); repairMessageBox->exec(); } - else { repairMessageBox->addButton(QMessageBox::Yes); @@ -707,23 +814,23 @@ void ctkDICOMBrowser::importDirectories(QStringList directories, 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()); - } + } } //---------------------------------------------------------------------------- @@ -733,9 +840,9 @@ void ctkDICOMBrowser::importDirectory(QString directory, ctkDICOMBrowser::Import ctkDICOMImportStats stats(d); d->importDirectory(directory, mode); if (d->DisplayImportSummary) - { + { QMessageBox::information(d->ImportDialog,"DICOM Directory Import", stats.summary()); - } + } } //---------------------------------------------------------------------------- @@ -748,15 +855,15 @@ void ctkDICOMBrowser::onImportDirectory(QString directory, ctkDICOMBrowser::Impo void ctkDICOMBrowserPrivate::importDirectory(QString directory, ctkDICOMBrowser::ImportDirectoryMode mode) { if (!QDir(directory).exists()) - { + { return; - } + } QString targetDirectory; if (mode == ctkDICOMBrowser::ImportDirectoryCopy) - { + { targetDirectory = this->DICOMDatabase->databaseDirectory(); - } + } // show progress dialog and perform indexing this->showIndexerDialog(); @@ -770,9 +877,9 @@ void ctkDICOMBrowserPrivate::importOldSettings() QSettings settings; int dontConfirmCopyOnImport = settings.value("MainWindow/DontConfirmCopyOnImport", static_cast(QMessageBox::InvalidRole)).toInt(); if (dontConfirmCopyOnImport == QMessageBox::AcceptRole) - { + { settings.setValue("DICOM/ImportDirectoryMode", static_cast(ctkDICOMBrowser::ImportDirectoryCopy)); - } + } settings.remove("MainWindow/DontConfirmCopyOnImport"); } @@ -787,12 +894,11 @@ ctkFileDialog* ctkDICOMBrowser::importDialog() const ctkDICOMBrowser::ImportDirectoryMode ctkDICOMBrowser::importDirectoryMode()const { Q_D(const ctkDICOMBrowser); - ctkDICOMBrowserPrivate* mutable_d = - const_cast(d); + ctkDICOMBrowserPrivate* mutable_d = const_cast(d); mutable_d->importOldSettings(); QSettings settings; return static_cast(settings.value( - "DICOM/ImportDirectoryMode", static_cast(ctkDICOMBrowser::ImportDirectoryAddLink)).toInt()); + "DICOM/ImportDirectoryMode", static_cast(ctkDICOMBrowser::ImportDirectoryAddLink)).toInt() ); } //---------------------------------------------------------------------------- @@ -803,13 +909,61 @@ void ctkDICOMBrowser::setImportDirectoryMode(ctkDICOMBrowser::ImportDirectoryMod QSettings settings; settings.setValue("DICOM/ImportDirectoryMode", static_cast(mode)); if (!d->ImportDialog) - { + { return; - } + } QComboBox* comboBox = d->ImportDialog->bottomWidget()->findChild(); 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) { @@ -825,9 +979,9 @@ bool ctkDICOMBrowser::confirmDeleteSelectedUIDs(QStringList uids) Q_D(ctkDICOMBrowser); if (uids.isEmpty()) - { + { return false; - } + } ctkMessageBox confirmDeleteDialog; QString message("Do you want to delete the following selected items?"); @@ -835,7 +989,7 @@ bool ctkDICOMBrowser::confirmDeleteSelectedUIDs(QStringList uids) // add the information about the selected UIDs int numUIDs = uids.size(); for (int i = 0; i < numUIDs; ++i) - { + { QString uid = uids.at(i); // try using the given UID to find a descriptive string @@ -844,24 +998,23 @@ bool ctkDICOMBrowser::confirmDeleteSelectedUIDs(QStringList uids) QString seriesDescription = d->DICOMDatabase->descriptionForSeries(uid); if (!patientName.isEmpty()) - { + { message += QString("\n") + patientName; - } + } else if (!studyDescription.isEmpty()) - { + { message += QString("\n") + studyDescription; - } + } else if (!seriesDescription.isEmpty()) - { + { message += QString("\n") + seriesDescription; - } + } else - { + { // if all other descriptors are empty, use the UID message += QString("\n") + uid; - } - } + } confirmDeleteDialog.setText(message); confirmDeleteDialog.setIcon(QMessageBox::Question); @@ -872,13 +1025,13 @@ bool ctkDICOMBrowser::confirmDeleteSelectedUIDs(QStringList uids) int response = confirmDeleteDialog.exec(); if (response == QMessageBox::AcceptRole) - { + { return true; - } + } else - { + { return false; - } + } } //---------------------------------------------------------------------------- @@ -890,10 +1043,10 @@ void ctkDICOMBrowser::onPatientsRightClicked(const QPoint &point) QStringList selectedPatientsUIDs = d->dicomTableManager->currentPatientsSelection(); int numPatients = selectedPatientsUIDs.size(); if (numPatients == 0) - { + { qDebug() << "No patients selected!"; return; - } + } QMenu *patientsMenu = new QMenu(d->dicomTableManager); @@ -917,29 +1070,29 @@ void ctkDICOMBrowser::onPatientsRightClicked(const QPoint &point) if (selectedAction == deleteAction && this->confirmDeleteSelectedUIDs(selectedPatientsUIDs)) - { + { qDebug() << "Deleting " << numPatients << " patients"; foreach (const QString& uid, selectedPatientsUIDs) - { + { d->DICOMDatabase->removePatient(uid); d->dicomTableManager->updateTableViews(); - } } + } 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; } + delete directoryDialog; + } } //---------------------------------------------------------------------------- @@ -951,10 +1104,10 @@ void ctkDICOMBrowser::onStudiesRightClicked(const QPoint &point) QStringList selectedStudiesUIDs = d->dicomTableManager->currentStudiesSelection(); int numStudies = selectedStudiesUIDs.size(); if (numStudies == 0) - { + { qDebug() << "No studies selected!"; return; - } + } QMenu *studiesMenu = new QMenu(d->dicomTableManager); @@ -978,28 +1131,28 @@ void ctkDICOMBrowser::onStudiesRightClicked(const QPoint &point) if (selectedAction == deleteAction && this->confirmDeleteSelectedUIDs(selectedStudiesUIDs)) - { + { foreach (const QString& uid, selectedStudiesUIDs) - { + { d->DICOMDatabase->removeStudy(uid); d->dicomTableManager->updateTableViews(); - } } + } 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; } + delete directoryDialog; + } } //---------------------------------------------------------------------------- @@ -1011,10 +1164,10 @@ void ctkDICOMBrowser::onSeriesRightClicked(const QPoint &point) QStringList selectedSeriesUIDs = d->dicomTableManager->currentSeriesSelection(); int numSeries = selectedSeriesUIDs.size(); if (numSeries == 0) - { + { qDebug() << "No series selected!"; return; - } + } QMenu *seriesMenu = new QMenu(d->dicomTableManager); @@ -1037,28 +1190,28 @@ void ctkDICOMBrowser::onSeriesRightClicked(const QPoint &point) if (selectedAction == deleteAction && this->confirmDeleteSelectedUIDs(selectedSeriesUIDs)) - { + { foreach (const QString& uid, selectedSeriesUIDs) - { + { d->DICOMDatabase->removeSeries(uid); d->dicomTableManager->updateTableViews(); - } } + } 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; } + delete directoryDialog; + } } //---------------------------------------------------------------------------- @@ -1067,7 +1220,7 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) Q_D(ctkDICOMBrowser); foreach (const QString& uid, uids) - { + { QStringList filesForSeries = d->DICOMDatabase->filesForSeries(uid); // Use the first file to get the overall series information @@ -1087,19 +1240,19 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) QString nameSep = "-"; QString destinationDir = dirPath + sep + patientID; if (!patientName.isEmpty()) - { + { destinationDir += nameSep + patientName; - } + } destinationDir += sep + studyDate; if (!studyDescription.isEmpty()) - { + { destinationDir += nameSep + studyDescription; - } + } destinationDir += sep + seriesNumber; if (!seriesDescription.isEmpty()) - { + { destinationDir += nameSep + seriesDescription; - } + } destinationDir += sep; // make sure only ascii characters are in the directory path @@ -1110,9 +1263,9 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) // create the destination directory if necessary if (!QDir().exists(destinationDir)) - { + { if (!QDir().mkpath(destinationDir)) - { + { QString errorString = QString("Unable to create export destination directory:\n\n") + destinationDir @@ -1122,16 +1275,16 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) createDirectoryErrorMessageBox.setIcon(QMessageBox::Warning); createDirectoryErrorMessageBox.exec(); return; - } } + } // show progress if (d->ExportProgress == 0) - { + { d->ExportProgress = new QProgressDialog(this->tr("DICOM Export"), "Close", 0, 100, this, Qt::WindowTitleHint | Qt::WindowSystemMenuHint); d->ExportProgress->setWindowModality(Qt::ApplicationModal); d->ExportProgress->setMinimumDuration(0); - } + } QLabel *exportLabel = new QLabel(this->tr("Exporting series ") + seriesNumber); d->ExportProgress->setLabel(exportLabel); d->ExportProgress->setValue(0); @@ -1140,7 +1293,7 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) int numFiles = filesForSeries.size(); d->ExportProgress->setMaximum(numFiles); foreach (const QString& filePath, filesForSeries) - { + { QString destinationFileName = destinationDir; QString fileNumberString; @@ -1156,7 +1309,7 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) destinationFileName.replace("?", "_"); if (!QFile::exists(filePath)) - { + { d->ExportProgress->setValue(numFiles); QString errorString = QString("Export source file not found:\n\n") + filePath @@ -1168,7 +1321,7 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) return; } if (QFile::exists(destinationFileName)) - { + { d->ExportProgress->setValue(numFiles); QString errorString = QString("Export destination file already exists:\n\n") + destinationFileName @@ -1178,11 +1331,11 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) copyErrorMessageBox.setIcon(QMessageBox::Warning); copyErrorMessageBox.exec(); return; - } + } bool copyResult = QFile::copy(filePath, destinationFileName); if (!copyResult) - { + { d->ExportProgress->setValue(numFiles); QString errorString = QString("Failed to copy\n\n") + filePath @@ -1198,9 +1351,9 @@ void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids) fileNumber++; d->ExportProgress->setValue(fileNumber); - } - d->ExportProgress->setValue(numFiles); } + d->ExportProgress->setValue(numFiles); + } } //---------------------------------------------------------------------------- @@ -1209,10 +1362,10 @@ 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); - } + } } //---------------------------------------------------------------------------- @@ -1221,8 +1374,33 @@ 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() +{ + Q_D(ctkDICOMBrowser); + if (d->UpdateDisplayedFieldsProgress == 0) + { + // + // Set up the Update Schema Progress Dialog + // + d->UpdateDisplayedFieldsProgress = new QProgressDialog(this, Qt::WindowTitleHint | Qt::WindowSystemMenuHint); + + // We don't want the progress dialog to resize itself, so we bypass the label by creating our own + d->UpdateDisplayedFieldsProgress->setLabelText("Updating database displayed fields..."); + d->UpdateDisplayedFieldsProgress->setWindowModality(Qt::ApplicationModal); + d->UpdateDisplayedFieldsProgress->setMinimumDuration(0); + d->UpdateDisplayedFieldsProgress->setMaximum(5); + d->UpdateDisplayedFieldsProgress->setValue(0); + + connect(d->DICOMDatabase.data(), SIGNAL(displayedFieldsUpdateProgress(int)), d->UpdateDisplayedFieldsProgress, SLOT(setValue(int))); + connect(d->DICOMDatabase.data(), SIGNAL(displayedFieldsUpdated()), d->UpdateDisplayedFieldsProgress, SLOT(close())); + } + d->UpdateDisplayedFieldsProgress->show(); + QApplication::processEvents(); } diff --git a/Libs/DICOM/Widgets/ctkDICOMBrowser.h b/Libs/DICOM/Widgets/ctkDICOMBrowser.h index ac08c9a9e8..0938ad2713 100644 --- a/Libs/DICOM/Widgets/ctkDICOMBrowser.h +++ b/Libs/DICOM/Widgets/ctkDICOMBrowser.h @@ -64,6 +64,8 @@ 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 confirmRemove READ confirmRemove WRITE setConfirmRemove) public: typedef ctkDICOMBrowser Self; @@ -85,20 +87,23 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMBrowser : public QWidget void setTagsToPrecache(const QStringList tags); const QStringList tagsToPrecache(); - /// Updates schema of loaded database to match the one - /// coded by the current version of ctkDICOMDatabase. - /// Also provides a dialog box for progress - Q_INVOKABLE void updateDatabaseSchemaIfNeeded(); + /// 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. + /// \return Flag determining whether new database has been set. In that case prevent circular calls. + Q_INVOKABLE bool updateDatabaseSchemaIfNeeded(); Q_INVOKABLE ctkDICOMDatabase* database(); Q_INVOKABLE ctkDICOMTableManager* dicomTableManager(); /// Option to show or not import summary dialog. - /// Since the summary dialog is modal, we give the option - /// of disabling it for batch modes or testing. + /// Since the summary dialog is modal, we give the option of disabling it for batch modes or testing. void setDisplayImportSummary(bool); bool displayImportSummary(); + /// Option to show dialog to confirm removal from the database (Remove action). + void setConfirmRemove(bool); + bool confirmRemove(); /// Accessors to status of last directory import operation int patientsAddedDuringImport(); int studiesAddedDuringImport(); @@ -116,6 +121,22 @@ 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 @@ -131,13 +152,17 @@ 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. /// - /// The dialog is extented with two additional controls: + /// The dialog is extended with two additional controls: /// /// * **ImportDirectoryMode** combox: Allow user to select "Add Link" or "Copy" mode. /// Associated settings is stored using key `DICOM/ImportDirectoryMode`. @@ -176,12 +201,15 @@ public Q_SLOTS: void onSeriesAdded(QString); void onInstanceAdded(QString); + /// Show progress dialog for update displayed fields + void showUpdateDisplayedFieldsDialog(); + Q_SIGNALS: - /// Emited when directory is changed + /// Emitted when directory is changed void databaseDirectoryChanged(const QString&); - /// Emited when query/retrieve operation has happened + /// Emitted when query/retrieve operation has happened void queryRetrieveFinished(); - /// Emited when the directory import operation has completed + /// Emitted when the directory import operation has completed void directoryImported(); protected: diff --git a/Libs/DICOM/Widgets/ctkDICOMTableManager.cpp b/Libs/DICOM/Widgets/ctkDICOMTableManager.cpp index a03b6a4775..79e2dba6c8 100644 --- a/Libs/DICOM/Widgets/ctkDICOMTableManager.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMTableManager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include class ctkDICOMTableManagerPrivate : public Ui_ctkDICOMTableManager { @@ -46,6 +47,9 @@ class ctkDICOMTableManagerPrivate : public Ui_ctkDICOMTableManager ctkDICOMDatabase* dicomDatabase; bool m_DynamicTableLayout; + /// Flag storing state whether automatic selection of series is enabled. + /// It is only needed to be able to provide a read function for the property + bool m_AutoSelectSeries; }; //------------------------------------------------------------------------------ @@ -53,6 +57,7 @@ class ctkDICOMTableManagerPrivate : public Ui_ctkDICOMTableManager ctkDICOMTableManagerPrivate::ctkDICOMTableManagerPrivate(ctkDICOMTableManager &obj) : q_ptr(&obj) , m_DynamicTableLayout(false) + , m_AutoSelectSeries(true) { } @@ -219,13 +224,13 @@ void ctkDICOMTableManager::onPatientsSelectionChanged(const QStringList &uids) patientCondition.first = "Patients.UID"; Q_D(ctkDICOMTableManager); if (!uids.empty()) - { - patientCondition.second = uids; - } + { + patientCondition.second = uids; + } else - { - patientCondition.second = d->patientsTable->uidsForAllRows(); - } + { + patientCondition.second = d->patientsTable->uidsForAllRows(); + } d->studiesTable->addSqlWhereCondition(patientCondition); d->seriesTable->addSqlWhereCondition(patientCondition); } @@ -237,13 +242,13 @@ void ctkDICOMTableManager::onStudiesSelectionChanged(const QStringList &uids) studiesCondition.first = "Studies.StudyInstanceUID"; Q_D(ctkDICOMTableManager); if (!uids.empty()) - { - studiesCondition.second = uids; - } + { + studiesCondition.second = uids; + } else - { - studiesCondition.second = d->studiesTable->uidsForAllRows(); - } + { + studiesCondition.second = d->studiesTable->uidsForAllRows(); + } d->seriesTable->addSqlWhereCondition(studiesCondition); } @@ -254,12 +259,111 @@ void ctkDICOMTableManager::setDynamicTableLayout(bool dynamic) d->m_DynamicTableLayout = dynamic; } +//------------------------------------------------------------------------------ bool ctkDICOMTableManager::dynamicTableLayout() const { Q_D(const ctkDICOMTableManager); return d->m_DynamicTableLayout; } +//------------------------------------------------------------------------------ +void ctkDICOMTableManager::setAutoSelectSeries(bool autoSelect) +{ + Q_D(ctkDICOMTableManager); + + if (autoSelect == d->m_AutoSelectSeries) + { + return; + } + + if (autoSelect) + { + QAbstractItemView::SelectionMode selectionMode = static_cast(this->selectionMode()); + if (selectionMode == QAbstractItemView::SingleSelection) + { + QObject::connect( d->studiesTable, SIGNAL(selectionChanged(const QStringList&)), + d->seriesTable, SLOT(selectFirst()) ); + } + else + { + QObject::connect( d->studiesTable, SIGNAL(selectionChanged(const QStringList&)), + d->seriesTable, SLOT(selectAll()) ); + } + } + else + { + QObject::disconnect( d->studiesTable, SIGNAL(selectionChanged(const QStringList&)), + d->seriesTable, SLOT(selectAll()) ); + QObject::disconnect( d->studiesTable, SIGNAL(selectionChanged(const QStringList&)), + d->seriesTable, SLOT(selectFirst()) ); + // Remove selection to avoid loading any previously auto-selected series + d->seriesTable->clearSelection(); + } + + d->m_AutoSelectSeries = autoSelect; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMTableManager::autoSelectSeries() const +{ + Q_D(const ctkDICOMTableManager); + return d->m_AutoSelectSeries; +} + +//------------------------------------------------------------------------------ +void ctkDICOMTableManager::setSelectionMode(int mode) +{ + Q_D(ctkDICOMTableManager); + + if (mode == this->selectionMode()) + { + return; + } + + QAbstractItemView::SelectionMode selectionMode = static_cast(mode); + d->patientsTable->tableView()->setSelectionMode(selectionMode); + d->studiesTable->tableView()->setSelectionMode(selectionMode); + d->seriesTable->tableView()->setSelectionMode(selectionMode); + + // Re-connect the proper slots for studies + QObject::disconnect( d->patientsTable, SIGNAL(selectionChanged(const QStringList&)), + d->studiesTable, SLOT(selectAll()) ); + QObject::disconnect( d->patientsTable, SIGNAL(selectionChanged(const QStringList&)), + d->studiesTable, SLOT(selectFirst()) ); + if (selectionMode == QAbstractItemView::SingleSelection) + { + QObject::connect( d->patientsTable, SIGNAL(selectionChanged(const QStringList&)), + d->studiesTable, SLOT(selectFirst()) ); + } + else + { + QObject::connect( d->patientsTable, SIGNAL(selectionChanged(const QStringList&)), + d->studiesTable, SLOT(selectAll()) ); + } + + // Re-connect the proper slots for series + if (this->autoSelectSeries()) + { + this->setAutoSelectSeries(false); + this->setAutoSelectSeries(true); + } +} + +//------------------------------------------------------------------------------ +int ctkDICOMTableManager::selectionMode() const +{ + Q_D(const ctkDICOMTableManager); + QAbstractItemView::SelectionMode patientSelectionMode = d->patientsTable->tableView()->selectionMode(); + QAbstractItemView::SelectionMode studySelectionMode = d->studiesTable->tableView()->selectionMode(); + QAbstractItemView::SelectionMode seriesSelectionMode = d->seriesTable->tableView()->selectionMode(); + if (patientSelectionMode != studySelectionMode || patientSelectionMode != seriesSelectionMode) + { + qWarning() << Q_FUNC_INFO << ": Inconsistent selection mode in the tables. Patient selection mode is returned"; + } + + return static_cast(patientSelectionMode); +} + //------------------------------------------------------------------------------ void ctkDICOMTableManager::updateTableViews() { diff --git a/Libs/DICOM/Widgets/ctkDICOMTableManager.h b/Libs/DICOM/Widgets/ctkDICOMTableManager.h index 150fb1da92..1b915e1cb2 100644 --- a/Libs/DICOM/Widgets/ctkDICOMTableManager.h +++ b/Libs/DICOM/Widgets/ctkDICOMTableManager.h @@ -53,12 +53,22 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableManager : public QWidget Q_ENUMS(DisplayDensity) /** - * This property holds the density of tables in the table Manager. There are three denisity + * This property holds the density of tables in the table Manager. There are three density * levels: Comfortable (least dense), Cozy and Compact (most dense). */ - Q_PROPERTY(ctkDICOMTableManager::DisplayDensity displayDensity READ displayDensity WRITE setDisplayDensity); + /** + * Property for automatic selection of series when a study is selected. On by default + */ + Q_PROPERTY(bool autoSelectSeries READ autoSelectSeries WRITE setAutoSelectSeries) + + /** + * Property for selection mode of the contained three table views. + * QAbstractItemView::SelectionMode values are cast to/from integer. ExtendedSelection by default + */ + Q_PROPERTY(int selectionMode READ selectionMode WRITE setSelectionMode); + public: typedef QWidget Superclass; @@ -86,6 +96,12 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableManager : public QWidget void setDynamicTableLayout(bool); bool dynamicTableLayout() const; + void setAutoSelectSeries(bool); + bool autoSelectSeries() const; + + void setSelectionMode(int mode); + int selectionMode() const; + Q_INVOKABLE void updateTableViews(); enum DisplayDensity @@ -102,9 +118,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableManager : public QWidget Q_INVOKABLE ctkDICOMTableView* studiesTable(); Q_INVOKABLE ctkDICOMTableView* seriesTable(); - public Q_SLOTS: - void onPatientsQueryChanged(const QStringList&); void onStudiesQueryChanged(const QStringList&); void onPatientsSelectionChanged(const QStringList&); @@ -130,15 +144,12 @@ public Q_SLOTS: void studiesRightClicked(const QPoint&); void seriesRightClicked(const QPoint&); - protected: - virtual void resizeEvent(QResizeEvent *); QScopedPointer d_ptr; private: - Q_DECLARE_PRIVATE(ctkDICOMTableManager) Q_DISABLE_COPY(ctkDICOMTableManager) }; diff --git a/Libs/DICOM/Widgets/ctkDICOMTableView.cpp b/Libs/DICOM/Widgets/ctkDICOMTableView.cpp index 880aca205b..bc63df0b0c 100644 --- a/Libs/DICOM/Widgets/ctkDICOMTableView.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMTableView.cpp @@ -39,23 +39,24 @@ class ctkDICOMTableViewPrivate : public Ui_ctkDICOMTableView ctkDICOMTableViewPrivate(ctkDICOMTableView& obj); ctkDICOMTableViewPrivate(ctkDICOMTableView& obj, ctkDICOMDatabase* db); ~ctkDICOMTableViewPrivate(); - // Initialize UI and tableview with tablemodel - void init(); - //Temporay solution to hide UID columns - void hideUIDColumns(); + /// Initialize UI and tableview with tablemodel + void init(); void showFilterActiveWarning(bool); QString queryTableName() const; + void applyColumnProperties(); + ctkDICOMDatabase* dicomDatabase; QSqlQueryModel dicomSQLModel; QSortFilterProxyModel* dicomSQLFilterModel; QString queryForeignKey; QStringList currentSelection; - //Key = QString for columns, Values = QStringList + + /// Key = QString for columns, Values = QStringList QHash sqlWhereConditions; }; @@ -96,7 +97,6 @@ void ctkDICOMTableViewPrivate::init() this->dicomSQLFilterModel->setFilterKeyColumn(-1); this->dicomSQLFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); this->tblDicomDatabaseView->setModel(this->dicomSQLFilterModel); - this->tblDicomDatabaseView->setColumnHidden(0, true); this->tblDicomDatabaseView->setSortingEnabled(true); #if QT_VERSION < QT_VERSION_CHECK(5,0,0) this->tblDicomDatabaseView->horizontalHeader()->setResizeMode(QHeaderView::Interactive); @@ -115,8 +115,7 @@ void ctkDICOMTableViewPrivate::init() QObject::connect(this->tblDicomDatabaseView, SIGNAL(doubleClicked(const QModelIndex&)), q, SIGNAL(doubleClicked(const QModelIndex&))); - // enable right click menu, with mapping to global position (for use within the DICOM - // table manager) + // enable right click menu, with mapping to global position (for use within the DICOM table manager) this->tblDicomDatabaseView->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(this->tblDicomDatabaseView, SIGNAL(customContextMenuRequested(const QPoint&)), @@ -128,21 +127,6 @@ void ctkDICOMTableViewPrivate::init() QObject::connect(this->leSearchBox, SIGNAL(textChanged(QString)), q, SLOT(onFilterChanged())); } -//------------------------------------------------------------------------------ -//Temporay solution to hide UID columns -void ctkDICOMTableViewPrivate::hideUIDColumns() -{ - const int numberOfColumns = this->tblDicomDatabaseView->model()->columnCount(); - for (int i = 0; i < numberOfColumns; ++i) - { - QString columnName = this->tblDicomDatabaseView->model()->headerData(i, Qt::Horizontal).toString(); - if (columnName.contains("UID")) - { - this->tblDicomDatabaseView->hideColumn(i); - } - } -} - //---------------------------------------------------------------------------- QString ctkDICOMTableViewPrivate::queryTableName() const { @@ -154,14 +138,94 @@ void ctkDICOMTableViewPrivate::showFilterActiveWarning(bool showWarning) { QPalette palette; if (showWarning) + { + palette.setColor(QPalette::Base,Qt::yellow); + } + else + { + palette.setColor(QPalette::Base,Qt::white); + } + this->leSearchBox->setPalette(palette); +} + +//---------------------------------------------------------------------------- +void ctkDICOMTableViewPrivate::applyColumnProperties() +{ + if (!this->dicomDatabase || !this->dicomDatabase->isOpen()) + { + qCritical() << Q_FUNC_INFO << ": Database not accessible"; + return; + } + + QHeaderView* header = this->tblDicomDatabaseView->horizontalHeader(); + int columnCount = this->dicomSQLModel.columnCount(); + QList columnWeights; + QMap visualIndexToColumnIndexMap; + for (int col=0; coldicomSQLModel.headerData(col, Qt::Horizontal).toString(); + QString originalColumnName = this->dicomSQLModel.headerData(col, Qt::Horizontal, Qt::WhatsThisRole).toString(); + if (originalColumnName.isEmpty()) { - palette.setColor(QPalette::Base,Qt::yellow); + // Save original column name for future referencing the database fields + this->dicomSQLModel.setHeaderData(col, Qt::Horizontal, columnName, Qt::WhatsThisRole); } - else + else { - palette.setColor(QPalette::Base,Qt::white); + columnName = originalColumnName; + visualIndexToColumnIndexMap[header->visualIndex(col)] = col; } - this->leSearchBox->setPalette(palette); + + // Apply displayed name + QString displayedName = this->dicomDatabase->displayedNameForField(this->queryTableName(), columnName); + this->dicomSQLModel.setHeaderData(col, Qt::Horizontal, displayedName, Qt::DisplayRole); + + // Apply visibility + bool visbility = this->dicomDatabase->visibilityForField(this->queryTableName(), columnName); + this->tblDicomDatabaseView->setColumnHidden(col, !visbility); + + // Save weight to apply later + int weight = this->dicomDatabase->weightForField(this->queryTableName(), columnName); + columnWeights << weight; + + QString format = this->dicomDatabase->formatForField(this->queryTableName(), columnName); + //TODO: Apply format + } + + // 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]) + { + columnIndicesByVisualIndex.swap(j, j+1); + header->swapSections(j, j+1); + } + } + } + } + // Change column order according to weights (use bubble sort) + for (int i=0; i columnWeights[j+1]) + { + columnWeights.swap(j, j+1); + header->swapSections(j, j+1); + } + } + } + header->blockSignals(wasBlocked); + header->updateGeometry(); } @@ -210,16 +274,19 @@ void ctkDICOMTableView::setDicomDataBase(ctkDICOMDatabase *dicomDatabase) //Do nothing if no database is set if (!dicomDatabase) + { return; + } d->dicomDatabase = dicomDatabase; + //Create connections for new database - QObject::connect(d->dicomDatabase, SIGNAL(instanceAdded(const QString&)), - this, SLOT(onInstanceAdded())); + QObject::connect(d->dicomDatabase, SIGNAL(instanceAdded(const QString&)), this, SLOT(onInstanceAdded())); QObject::connect(d->dicomDatabase, SIGNAL(databaseChanged()), this, SLOT(onDatabaseChanged())); this->setQuery(); - d->hideUIDColumns(); + + d->applyColumnProperties(); } //------------------------------------------------------------------------------ @@ -245,7 +312,11 @@ void ctkDICOMTableView::onSelectionChanged() //------------------------------------------------------------------------------ void ctkDICOMTableView::onDatabaseChanged() { - setQuery(); + Q_D(ctkDICOMTableView); + + this->setQuery(); + + d->applyColumnProperties(); } //------------------------------------------------------------------------------ @@ -253,7 +324,7 @@ void ctkDICOMTableView::onUpdateQuery(const QStringList& uids) { Q_D(ctkDICOMTableView); - setQuery(uids); + this->setQuery(uids); d->showFilterActiveWarning( d->dicomSQLFilterModel->rowCount() == 0 && d->leSearchBox->text().length() != 0 ); @@ -283,7 +354,7 @@ void ctkDICOMTableView::onInstanceAdded() d->sqlWhereConditions.clear(); d->tblDicomDatabaseView->clearSelection(); d->leSearchBox->clear(); - setQuery(); + this->setQuery(); } //------------------------------------------------------------------------------ @@ -293,23 +364,39 @@ void ctkDICOMTableView::selectAll() d->tblDicomDatabaseView->selectAll(); } +//------------------------------------------------------------------------------ +void ctkDICOMTableView::selectFirst() +{ + Q_D(ctkDICOMTableView); + QModelIndex firstIndex = d->tblDicomDatabaseView->model()->index(0,0); + QItemSelectionModel* selectionModel = d->tblDicomDatabaseView->selectionModel(); + selectionModel->setCurrentIndex(firstIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows); +} + +//------------------------------------------------------------------------------ +void ctkDICOMTableView::clearSelection() +{ + Q_D(ctkDICOMTableView); + d->tblDicomDatabaseView->clearSelection(); +} + //------------------------------------------------------------------------------ bool ctkDICOMTableView::eventFilter(QObject *obj, QEvent *event) { Q_D(ctkDICOMTableView); if (obj == d->tblDicomDatabaseView->viewport()) + { + if (event->type() == QEvent::MouseButtonPress || + event->type() == QEvent::MouseButtonDblClick) { - if (event->type() == QEvent::MouseButtonPress || - event->type() == QEvent::MouseButtonDblClick) - { - QMouseEvent* mouseEvent = static_cast(event); - QPoint pos = mouseEvent->pos(); - if (!d->tblDicomDatabaseView->indexAt(pos).isValid()) - { - return true; - } - } + QMouseEvent* mouseEvent = static_cast(event); + QPoint pos = mouseEvent->pos(); + if (!d->tblDicomDatabaseView->indexAt(pos).isValid()) + { + return true; + } } + } return QObject::eventFilter(obj, event); } @@ -321,27 +408,27 @@ void ctkDICOMTableView::setQuery(const QStringList &uids) "Patients.UID = Studies.PatientsUID and Studies.StudyInstanceUID = Series.StudyInstanceUID"); if (!uids.empty() && d->queryForeignKey.length() != 0) - { - query += " and %1."+d->queryForeignKey+" in ( '"; - query.append(uids.join("','")).append("')"); - } + { + query += " and %1."+d->queryForeignKey+" in ( '"; + query.append(uids.join("','")).append("')"); + } if (!d->sqlWhereConditions.empty()) + { + QHash::const_iterator i = d->sqlWhereConditions.begin(); + while (i != d->sqlWhereConditions.end()) { - QHash::const_iterator i = d->sqlWhereConditions.begin(); - while (i != d->sqlWhereConditions.end()) - { - if (!i.value().empty()) - { - query += " and "+i.key()+" in ( '"; - query.append(i.value().join("','")).append("')"); - } - ++i; - } + if (!i.value().empty()) + { + query += " and "+i.key()+" in ( '"; + query.append(i.value().join("','")).append("')"); + } + ++i; } + } if (d->dicomDatabase != 0 && d->dicomDatabase->isOpen()) - { + { d->dicomSQLModel.setQuery(query.arg(d->queryTableName()), d->dicomDatabase->database()); - } + } } void ctkDICOMTableView::addSqlWhereCondition(const std::pair &condition) @@ -358,17 +445,17 @@ QStringList ctkDICOMTableView::uidsForAllRows() const int numberOfRows = tableModel->rowCount(); QStringList uids; if (numberOfRows == 0) - { - //Return invalid UID if there are no rows - uids << QString("#"); - } + { + //Return invalid UID if there are no rows + uids << QString("#"); + } else + { + for(int i = 0; i < numberOfRows; ++i) { - for(int i = 0; i < numberOfRows; ++i) - { - uids << QString("%1").arg(tableModel->index(i,0).data().toString()); - } + uids << QString("%1").arg(tableModel->index(i,0).data().toString()); } + } return uids; } @@ -381,9 +468,9 @@ QStringList ctkDICOMTableView::currentSelection() const QStringList uids; foreach(QModelIndex i, currentSelection) - { - uids<< i.data().toString(); - } + { + uids << i.data().toString(); + } return uids; } diff --git a/Libs/DICOM/Widgets/ctkDICOMTableView.h b/Libs/DICOM/Widgets/ctkDICOMTableView.h index fe5bdf1c05..10fa68d1e9 100644 --- a/Libs/DICOM/Widgets/ctkDICOMTableView.h +++ b/Libs/DICOM/Widgets/ctkDICOMTableView.h @@ -84,7 +84,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableView : public QWidget void setQueryTableName(const QString &tableName); /** - * Setting the foreign key for the database query. This is usefull if e.g. you + * Setting the foreign key for the database query. This is useful if e.g. you * want to select the studies for a certain patient * @param foreignKey the foreign key which will be used for the query */ @@ -95,7 +95,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableView : public QWidget * entries with the according uids are selected * @param uids a list of uids which should be selected */ - void setQuery (const QStringList &uids = QStringList()); + void setQuery(const QStringList &uids = QStringList()); /** * @brief Add a where condition to the usual select statement @@ -124,10 +124,9 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMTableView : public QWidget * @brief Get the actual QTableView, for specific view settings * @return a pointer to QTableView* tblDicomDatabaseView */ - QTableView* tableView(); + Q_INVOKABLE QTableView* tableView(); public Q_SLOTS: - /** * @brief slot is called if the selection of the tableview is changed * Within this slot the signal signalSelectionChanged is emitted @@ -146,6 +145,21 @@ public Q_SLOTS: * Emits customContextMenuRequested with the global point */ void onCustomContextMenuRequested(const QPoint &point); + + /** + * @brief Select all items in the view + */ + void selectAll(); + + /** + * @brief Select first item in the view + */ + void selectFirst(); + + /** + * @brief Clear any selection in the view + */ + void clearSelection(); protected Q_SLOTS: /** @@ -163,8 +177,6 @@ protected Q_SLOTS: */ void onInstanceAdded(); - void selectAll(); - protected: virtual bool eventFilter(QObject *obj, QEvent *event);