File indexing completed on 2024-04-28 15:08:50

0001 /*
0002     SPDX-FileCopyrightText: 2021 Valentin Boettcher <hiro at protagon.space; @hiro98:tchncs.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #pragma once
0008 
0009 #include <QSqlDatabase>
0010 #include <QSqlError>
0011 #include <exception>
0012 #include <list>
0013 #include <QString>
0014 #include <QList>
0015 #include <catalogsdb_debug.h>
0016 #include <QSqlQuery>
0017 #include <QMutex>
0018 #include <QObject>
0019 #include <QThread>
0020 
0021 #include "polyfills/qstring_hash.h"
0022 #include <unordered_map>
0023 #include <queue>
0024 
0025 #include <unordered_set>
0026 #include <utility>
0027 #include "catalogobject.h"
0028 #include "nan.h"
0029 #include "typedef.h"
0030 
0031 namespace CatalogsDB
0032 {
0033 /**
0034  * A simple struct to hold information about catalogs.
0035  */
0036 struct Catalog
0037 {
0038     /**
0039      * The catalog id.
0040      */
0041     int id = -1;
0042 
0043     /**
0044      * The catalog mame.
0045      */
0046     QString name = "Unknown";
0047 
0048     /**
0049      * The precedence level of a catalog.
0050      *
0051      * If doublicate objects exist in the database, the one from the
0052      * catalog with the highest precedence winns.
0053      */
0054     double precedence = 0;
0055 
0056     /**
0057      * The author of the catalog.
0058      */
0059     QString author = "";
0060 
0061     /**
0062      * The catalog source.
0063      */
0064     QString source = "";
0065 
0066     /**
0067      * A (short) description for the catalog.
0068      * QT html is allowed.
0069      */
0070     QString description = "";
0071 
0072     /**
0073      * Wether the catalog is mutable.
0074      */
0075     bool mut = false;
0076 
0077     /**
0078      * Wether the catalog is enabled.
0079      */
0080     bool enabled = false;
0081 
0082     /**
0083      * The catalog version.
0084      */
0085     int version = -1;
0086 
0087     /**
0088      * The catalog color in the form `[default color];[scheme file
0089      * name];[color]...`.
0090      */
0091     QString color = "";
0092 
0093     /**
0094      * The catalog license.
0095      */
0096     QString license = "";
0097 
0098     /**
0099      * The catalog maintainer.
0100      */
0101     QString maintainer = "";
0102 
0103     /**
0104      * Build time of the catalog. Usually only catalogs with the same
0105      * timestamp can be considered dedublicated.
0106      *
0107      * A `null` timestamp indicates that the catalog has not been
0108      * built by the catalog repository.
0109      */
0110     QDateTime timestamp{};
0111 };
0112 
0113 const Catalog cat_defaults{};
0114 
0115 /**
0116  * Holds statistical information about the objects in a catalog.
0117  */
0118 struct CatalogStatistics
0119 {
0120     std::map<SkyObject::TYPE, int> object_counts;
0121     int total_count = 0;
0122 };
0123 
0124 const QString db_file_extension = "kscat";
0125 constexpr int application_id    = 0x4d515158;
0126 constexpr int custom_cat_min_id = 1000;
0127 constexpr int user_catalog_id   = 0;
0128 constexpr float default_maglim  = 99;
0129 const QString flux_unit         = "mag";
0130 const QString flux_frequency    = "400 nm";
0131 using CatalogColorMap           = std::map<QString, QColor>;
0132 using ColorMap                  = std::map<int, CatalogColorMap>;
0133 using CatalogObjectList         = std::list<CatalogObject>;
0134 using CatalogObjectVector       = std::vector<CatalogObject>;
0135 
0136 /**
0137  * \returns A hash table of the form `color scheme: color` by
0138  * parsing a string of the form `[default color];[scheme file
0139  * name];[color]...`.
0140  */
0141 CatalogColorMap parse_color_string(const QString &str);
0142 
0143 /**
0144  * \returns A color string of the form`[default color];[scheme file
0145  * name];[color]...`.
0146  *
0147  * The inverse of `CatalogsDB::parse_color_string`.
0148  */
0149 QString to_color_string(CatalogColorMap colors);
0150 
0151 /**
0152  * Manages the catalog database and provides an interface to provide an
0153  * interface to query and modify the database. For more information on
0154  * how the catalog database system works see the KStars Handbook.
0155  *
0156  * The class manages a database connection which is assumed to be
0157  * working (invariant). If the database can't be accessed a
0158  * DatabaseError is thrown upon construction. The manager is designed
0159  * to hold as little state as possible because the database should be
0160  * the single source of truth. Prepared statements are made class
0161  * members, only if they are performance critical.
0162  *
0163  * Most methods in this class are thread safe.
0164  *
0165  * The intention is that you access a/the catalogs database directly
0166  * locally in the code where objects from the database are required and
0167  * not through layers of references and pointers.
0168  *
0169  * The main DSO database can be accessed as follows:
0170  * ```cpp
0171  * CatalogsDB::DBManager manager{ CatalogsDB::dso_db_path() };
0172  * for(auto& o : manager.get_objects(10)) {
0173  *     // do something
0174  * }
0175  * ```
0176  *
0177  * To query the database, first check if the required query is already
0178  * hardcoded into the `DBManager`. If this is not the case you can either
0179  * add it (if it is performance critical and executed frequently) or use
0180  * `DBManager::general_master_query` to construct a custom `SQL` query.
0181  */
0182 class DBManager
0183 {
0184   public:
0185     /**
0186      * Constructs a database manager from the \p filename which is
0187      * resolved to a path in the kstars data directory.
0188      *
0189      * The constructor resolves the path to the database, opens it
0190      * (throws if that does not work), checks the database version
0191      * (throws if that does not match), initializes the database,
0192      * registers the user catalog and updates the all_catalog_view.
0193      */
0194     DBManager(const QString &filename);
0195     DBManager(const DBManager &other);
0196 
0197     DBManager &operator=(DBManager other)
0198     {
0199         using std::swap;
0200         DBManager tmp{ other };
0201 
0202         m_db_file = other.m_db_file;
0203         swap(m_db, other.m_db);
0204         swap(m_q_cat_by_id, other.m_q_cat_by_id);
0205         swap(m_q_obj_by_trixel, other.m_q_obj_by_trixel);
0206         swap(m_q_obj_by_trixel_no_nulls, other.m_q_obj_by_trixel_no_nulls);
0207         swap(m_q_obj_by_trixel_null_mag, other.m_q_obj_by_trixel_null_mag);
0208         swap(m_q_obj_by_name, other.m_q_obj_by_name);
0209         swap(m_q_obj_by_name_exact, other.m_q_obj_by_name_exact);
0210         swap(m_q_obj_by_lim, other.m_q_obj_by_lim);
0211         swap(m_q_obj_by_maglim, other.m_q_obj_by_maglim);
0212         swap(m_q_obj_by_maglim_and_type, other.m_q_obj_by_maglim_and_type);
0213         swap(m_q_obj_by_oid, other.m_q_obj_by_oid);
0214 
0215         return *this;
0216     };
0217 
0218     ~DBManager()
0219     {
0220         m_db.commit();
0221         m_db.close();
0222     }
0223 
0224     /**
0225      * @return the filename of the database
0226      */
0227     const QString &db_file_name() const { return m_db_file; };
0228 
0229     /**
0230      * @return wether the catalog with the \p id has been found and
0231      * the catalog.
0232      *
0233      * @todo use std::optional when transitioning to c++17
0234      */
0235     const std::pair<bool, Catalog> get_catalog(const int id);
0236 
0237     /**
0238      * \return a vector with all catalogs from the database. If \p
0239      * include_disabled is `true`, disabled catalogs will be included.
0240      */
0241     const std::vector<Catalog> get_catalogs(bool include_disabled = false);
0242 
0243     /**
0244      * @return true if the catalog with \p id exists
0245      *
0246      * @todo use std::optional when transitioning to c++17
0247      */
0248     bool catalog_exists(const int id);
0249 
0250     /**
0251      * @return return a vector of objects in the trixel with \p id.
0252      */
0253     inline CatalogObjectVector get_objects_in_trixel(const int trixel) {
0254         return _get_objects_in_trixel_generic(m_q_obj_by_trixel, trixel);
0255     }
0256 
0257     /**
0258      * @return return a vector of objects of known mag in the trixel with \p id.
0259      */
0260     inline CatalogObjectVector get_objects_in_trixel_no_nulls(const int trixel) {
0261         return _get_objects_in_trixel_generic(m_q_obj_by_trixel_no_nulls, trixel);
0262     }
0263 
0264     /**
0265      * @return return a vector of objects of unknown mag in the trixel with \p id.
0266      */
0267     inline CatalogObjectVector get_objects_in_trixel_null_mag(const int trixel) {
0268         return _get_objects_in_trixel_generic(m_q_obj_by_trixel_null_mag, trixel);
0269     }
0270 
0271     /**
0272      * \brief Find an objects by name.
0273      *
0274      * This will search the `name`, `long_name` and `catalog_identifier`
0275      * fields in all enabled catalogs for \p `name` and then return a new
0276      * instance of `CatalogObject` sourced from the master catalog.
0277      *
0278      * \param limit Upper limit to the quanitity of results. `-1` means "no
0279      * limit"
0280      * \param exactMatchOnly If true, the supplied name must match exactly
0281      *
0282      * \return a list of matching objects
0283      */
0284     CatalogObjectList find_objects_by_name(const QString &name, const int limit = -1,
0285                                            const bool exactMatchOnly = false);
0286 
0287     /**
0288      * \brief Find an objects by name in the catalog with \p `catalog_id`.
0289      *
0290      * \return a list of matching objects
0291      */
0292     CatalogObjectList find_objects_by_name(const int catalog_id, const QString &name,
0293                                            const int limit = -1);
0294 
0295     /**
0296      * \brief Find an objects by searching the name four wildcard. See
0297      * the LIKE sqlite statement.
0298      *
0299      * \return a list of matching objects
0300      */
0301     CatalogObjectList find_objects_by_wildcard(const QString &wildcard,
0302                                                const int limit = -1);
0303     /**
0304      * \brief Find an objects by searching the master catlog with a
0305      * query like `SELECT ... FROM master WHERE \p where ORDER BY \p
0306      * order_by ...`.
0307      *
0308      * To be used if performance does not matter (much).
0309      * \p order_by can be ommitted.
0310      *
0311      * \return wether the query was successful, an error message if
0312      * any and a list of matching objects
0313      */
0314     std::tuple<bool, const QString, CatalogObjectList>
0315     general_master_query(const QString &where, const QString &order_by = "",
0316                          const int limit = -1);
0317 
0318     /**
0319      * \brief Get an object by \p `oid`. Optinally a \p `catalog_id` can be speicfied.
0320      *
0321      * \returns if the object was found and the object itself
0322      */
0323     std::pair<bool, CatalogObject> get_object(const CatalogObject::oid &oid);
0324     std::pair<bool, CatalogObject> get_object(const CatalogObject::oid &oid,
0325                                               const int catalog_id);
0326 
0327     /**
0328      * Get \p limit objects with magnitude smaller than \p maglim (smaller =
0329      * brighter) from the database.
0330      */
0331     CatalogObjectList get_objects(float maglim = default_maglim, int limit = -1);
0332 
0333     /**
0334      * Get all objects from the database.
0335      */
0336     CatalogObjectList get_objects_all();
0337 
0338     /**
0339      * Get \p limit objects of \p type with magnitude smaller than \p
0340      * maglim (smaller = brighter) from the database. Optionally one
0341      * can filter by \p `catalog_id`.
0342      */
0343     CatalogObjectList get_objects(SkyObject::TYPE type, float maglim = default_maglim,
0344                                   int limit = -1);
0345     /**
0346      * Get \p limit objects from the catalog with \p `catalog_id` of
0347      * \p type with magnitude smaller than \p maglim (smaller =
0348      * brighter) from the database. Optionally one can filter by \p
0349      * `catalog_id`.
0350      */
0351     CatalogObjectList get_objects_in_catalog(SkyObject::TYPE type, const int catalog_id,
0352                                              float maglim = default_maglim,
0353                                              int limit    = -1);
0354 
0355     /**
0356      * @return return the htmesh level used by the catalog db
0357      */
0358     int htmesh_level() const { return m_htmesh_level; };
0359 
0360     /**
0361      * \brief Enable or disable a catalog.
0362      * \return `true` in case of succes, `false` and an error message in case
0363      * of an error
0364      *
0365      * This will recreate the master table.
0366      */
0367     std::pair<bool, QString> set_catalog_enabled(const int id, const bool enabled);
0368 
0369     /**
0370      * \brief remove a catalog
0371      * \return `true` in case of succes, `false` and an error message
0372      * in case of an error
0373      *
0374      * This will recreate the master table.
0375      */
0376     std::pair<bool, QString> remove_catalog(const int id);
0377 
0378     /**
0379      * Add a `CatalogObject` to a table with \p `catalog_id`. For the rest of
0380      * the arguments see `CatalogObject::CatalogObject`.
0381      *
0382      * \returns wether the operation was successful and if not, an error
0383      * message
0384      */
0385     std::pair<bool, QString> add_object(const int catalog_id, const SkyObject::TYPE t,
0386                                         const CachingDms &r, const CachingDms &d,
0387                                         const QString &n, const float m = NaN::f,
0388                                         const QString &lname              = QString(),
0389                                         const QString &catalog_identifier = QString(),
0390                                         const float a = 0.0, const float b = 0.0,
0391                                         const double pa = 0.0, const float flux = 0);
0392 
0393     /**
0394      * Add the \p `object` to a table with \p `catalog_id`. For the
0395      * rest of the arguments see `CatalogObject::CatalogObject`.
0396      *
0397      * \returns wether the operation was successful and if not, an
0398      * error message
0399      */
0400     std::pair<bool, QString> add_object(const int catalog_id, const CatalogObject &obj);
0401 
0402     /**
0403      * Add the \p `objects` to a table with \p `catalog_id`. For the
0404      * rest of the arguments see `CatalogObject::CatalogObject`.
0405      *
0406      * \returns wether the operation was successful and if not, an
0407      * error message
0408      */
0409     std::pair<bool, QString> add_objects(const int catalog_id,
0410                                          const CatalogObjectVector &objects);
0411 
0412     /**
0413      * Remove the catalog object with the \p `oid` from the catalog with the
0414      * \p `catalog_id`.
0415      *
0416      * Refreshes the master catalog.
0417      *
0418      * \returns wether the operation was successful and if not, an
0419      * error message
0420      */
0421     std::pair<bool, QString> remove_object(const int catalog_id,
0422                                            const CatalogObject::oid &id);
0423 
0424     /**
0425      * Dumps the catalog with \p `id` into the file under the path \p
0426      * `file_path`.  This file can then be imported with
0427      * `import_catalog`.  If the file already exists, it will be
0428      * overwritten.
0429      *
0430      * The `user_version` and `application_id` pragmas are set to special
0431      * values, but otherwise the dump format is equal to the internal
0432      * database format.
0433      *
0434      * \returns wether the operation was successful and if not, an error
0435      * message
0436      */
0437     std::pair<bool, QString> dump_catalog(int catalog_id, QString file_path);
0438 
0439     /**
0440      * Loads a dumped catalog from path \p `file_path`. Will overwrite
0441      * an existing catalog if \p `overwrite` is set to true. Immutable
0442      * catalogs are overwritten by default.
0443      *
0444      * Checks if the pragma `application_id` matches
0445      * `CatalogsDB::application_id` and the pragma `user_version` to match
0446      * the database format version.
0447      *
0448      * \returns wether the operation was successful and if not, an error
0449      * message
0450      */
0451     std::pair<bool, QString> import_catalog(const QString &file_path,
0452                                             const bool overwrite = false);
0453     /**
0454      * Registers a new catalog in the database.
0455      *
0456      * For the parameters \sa Catalog. The catalog gets inserted into
0457      * `m_catalogs`. The `all_catalog_view` is updated.
0458      *
0459      * \return true in case of success, false in case of an error
0460      * (along with the error)
0461      */
0462     std::pair<bool, QString>
0463     register_catalog(const int id, const QString &name, const bool mut,
0464                      const bool enabled, const double precedence,
0465                      const QString &author      = cat_defaults.author,
0466                      const QString &source      = cat_defaults.source,
0467                      const QString &description = cat_defaults.description,
0468                      const int version          = cat_defaults.version,
0469                      const QString &color       = cat_defaults.color,
0470                      const QString &license     = cat_defaults.license,
0471                      const QString &maintainer  = cat_defaults.maintainer,
0472                      const QDateTime &timestamp = cat_defaults.timestamp);
0473 
0474     std::pair<bool, QString> register_catalog(const Catalog &cat);
0475 
0476     /**
0477      * Update the metatadata \p `catalog`.
0478      *
0479      * The updated fields are: title, author, source, description.
0480      *
0481      * \return true in case of success, false in case of an error
0482      * (along with the error).
0483      */
0484     std::pair<bool, QString> update_catalog_meta(const Catalog &cat);
0485 
0486     /**
0487      * Clone objects from the catalog with \p `id_1` to another with `id_2`. Useful to create a
0488      * custom catalog from an immutable one.
0489      */
0490     std::pair<bool, QString> copy_objects(const int id_1, const int id_2);
0491 
0492     /**
0493      * Finds the smallest free id for a catalog.
0494      */
0495     int find_suitable_catalog_id();
0496 
0497     /**
0498      * \returns statistics about the master catalog.
0499      */
0500     const std::pair<bool, CatalogStatistics> get_master_statistics();
0501 
0502     /**
0503      * \returns statistics about the catalog with \p `catalog_id`.
0504      */
0505     const std::pair<bool, CatalogStatistics> get_catalog_statistics(const int catalog_id);
0506 
0507     /**
0508      * Compiles the master catalog by merging the individual catalogs based
0509      * on `oid` and precedence and creates an index by (trixel, magnitude) on
0510      * the master table. **Caution** you may want to call
0511      * `update_catalog_views` beforhand.
0512      *
0513      * @return true in case of success, false in case of an error
0514      */
0515     bool compile_master_catalog();
0516 
0517     /**
0518      * Updates the all_catalog_view so that it includes all known
0519      * catalogs.
0520      *
0521      * @return true in case of success, false in case of an error
0522      */
0523     bool update_catalog_views();
0524 
0525     /** \returns the catalog colors as a hash table of the form `catalog id:
0526      *  scheme: color`.
0527      *
0528      *  The colors are loaded from the `Catalog::color` field and the
0529      *  `SqlStatements::color_table` in that order.
0530      */
0531     ColorMap get_catalog_colors();
0532 
0533     /** \returns the catalog colors as a hash table of for the catalog
0534      * with \p id in the form `scheme: color`.
0535      *
0536      *  The colors are loaded from the `Catalog::color` field and the
0537      *  `SqlStatements::color_table` in that order.
0538      */
0539     CatalogColorMap get_catalog_colors(const int id);
0540 
0541     /** Saves the configures colors of the catalog with id \p id in \p
0542      * colors into the database.  \returns wether the insertion was
0543      * possible and an error message if not.
0544      */
0545     std::pair<bool, QString> insert_catalog_colors(const int id,
0546                                                    const CatalogColorMap &colors);
0547 
0548   private:
0549     /**
0550      * The backing catalog database.
0551      */
0552     QSqlDatabase m_db;
0553 
0554     /**
0555      * The filename of the database.
0556      *
0557      * Will be a reference to a member of `m_db_paths`.
0558      */
0559     QString m_db_file;
0560 
0561     //@{
0562     /**
0563      * Some performance criticall sql queries are prepared stored as memebers.
0564      * When using those queries `m_mutex` should be locked!
0565      *
0566      * \sa prepare_queries
0567      */
0568 
0569     QSqlQuery m_q_cat_by_id;
0570     QSqlQuery m_q_obj_by_trixel;
0571     QSqlQuery m_q_obj_by_trixel_null_mag;
0572     QSqlQuery m_q_obj_by_trixel_no_nulls;
0573     QSqlQuery m_q_obj_by_name;
0574     QSqlQuery m_q_obj_by_name_exact;
0575     QSqlQuery m_q_obj_by_lim;
0576     QSqlQuery m_q_obj_by_maglim;
0577     QSqlQuery m_q_obj_by_maglim_and_type;
0578     QSqlQuery m_q_obj_by_oid;
0579     //@}
0580 
0581     /**
0582      * The level of the htmesh used to index the catalog entries.
0583      *
0584      * If the htmesh level of a catalog is different, the catalog will
0585      * be reindexed upon importing it.
0586      *
0587      * A value of -1 means that the htmesh-level has not been
0588      * deterined yet.
0589      */
0590     int m_htmesh_level = -1;
0591 
0592     /**
0593      * The version of the database.
0594      *
0595      * A value of -1 means that the htmesh-level has not been
0596      * deterined yet.
0597      */
0598     int m_db_version = -1;
0599 
0600     /**
0601      * A simple mutex to be locked when using prepared statements,
0602      * that are stored in the class.
0603      */
0604     QMutex m_mutex;
0605 
0606     //@{
0607     /**
0608      * Helpers
0609      */
0610 
0611     /**
0612      * Initializes the database with the minimum viable tables.
0613      *
0614      * The catalog registry is created and the database version is set
0615      * to SqlStatements::current_db_version and the htmesh-level is
0616      * set to SqlStatements::default_htmesh_level if they don't exist.
0617      *
0618      * @return true in case of success, false in case of an error
0619      */
0620     bool initialize_db();
0621 
0622     /**
0623      * Reads the database version and the htmesh level from the
0624      * database. If the meta table does not exist, the default vaulues
0625      * SqlStatements::current_db_version and
0626      * SqlStatements::default_htmesh_level.
0627      *
0628      * @return [version, htmesh-level, is-init?]
0629      */
0630     std::tuple<int, int, bool> get_db_meta();
0631 
0632     /**
0633      * Gets a vector of catalog ids of catalogs. If \p include_disabled is
0634      * `true`, disabled catalogs will be included.
0635      */
0636     std::vector<int> get_catalog_ids(bool include_enabled = false);
0637 
0638     /**
0639      * Prepares performance critical sql queries.
0640      *
0641      * @return [success, error]
0642      */
0643     std::pair<bool, QSqlError> prepare_queries();
0644 
0645     /**
0646      * Read a `CatalogObject` from the tip of the \p query.
0647      */
0648     CatalogObject read_catalogobject(const QSqlQuery &query) const;
0649 
0650     /**
0651      * Read the first `CatalogObject` from the tip of the \p `query`
0652      * that hasn't been exec'd yet.
0653      */
0654     std::pair<bool, CatalogObject> read_first_object(QSqlQuery &query) const;
0655 
0656     /**
0657      * Read all `CatalogObject`s from the \p query.
0658      */
0659     CatalogObjectList fetch_objects(QSqlQuery &query) const;
0660 
0661     /**
0662      * Internal implementation to forcably remove a catalog (even the
0663      * user catalog, use with caution!)
0664      */
0665     std::pair<bool, QString> remove_catalog_force(const int id);
0666 
0667     /**
0668      *
0669      */
0670     CatalogObjectVector _get_objects_in_trixel_generic(QSqlQuery &query, const int trixel);
0671 
0672     //@}
0673 };
0674 
0675 /**
0676  * Database related error, thrown when database access fails or an
0677  * action does not succeed.
0678  *
0679  * QSqlError is not used here to encapsulate the database further.
0680  */
0681 class DatabaseError : std::exception
0682 {
0683   public:
0684     enum class ErrorType
0685     {
0686         OPEN,
0687         VERSION,
0688         INIT,
0689         CREATE_CATALOG,
0690         CREATE_MASTER,
0691         NOT_FOUND,
0692         PREPARE,
0693         UNKNOWN
0694     };
0695 
0696     DatabaseError(QString message, ErrorType type = ErrorType::UNKNOWN,
0697                   const QSqlError &error = QSqlError())
0698         : m_message{ std::move(message) }, m_type{ type }, m_error{ error }, m_report{
0699               m_message.toStdString() +
0700               (error.text().length() > 0 ? "\nSQL ERROR: " + error.text().toStdString() :
0701                                            std::string(""))
0702           } {};
0703 
0704     const char *what() const noexcept override { return m_report.c_str(); }
0705     const QString &message() const noexcept { return m_message; }
0706     ErrorType type() const noexcept { return m_type; }
0707 
0708   private:
0709     const QString m_message;
0710     const ErrorType m_type;
0711     const QSqlError m_error;
0712     const std::string m_report;
0713 };
0714 
0715 /** \returns the path to the dso database */
0716 QString dso_db_path();
0717 
0718 /** \returns true and a catalog if the catalog metadata (name, author,
0719     ...) can be read */
0720 std::pair<bool, Catalog> read_catalog_meta_from_file(const QString &path);
0721 
0722 
0723 
0724 
0725 /**
0726  * A concurrent wrapper around \sa CatalogsDB::DBManager
0727  *
0728  * This wrapper can be instantiated from the main thread. It spawns
0729  * its own thread and moves itself to the thread, allowing the main
0730  * thread to call DB operations without blocking the user
0731  * interface. It provides a generic wrapper interface, \sa
0732  * AsyncDBManager::execute(), to call the methods of DBManager.
0733  *
0734  * Since it is hard to use signal-slot communication with a generic
0735  * wrapper like \sa AsyncDBManager::execute(), a wrapper is provided
0736  * for the two most likely overloads of \sa
0737  * DBManager::find_objects_by_name, which are time-consuming and
0738  * frequently used, and these can be directly invoked from
0739  * QMetaObject::invokeMethod or an appropriate signal
0740  *
0741  * The void override of \sa AsyncDBManager::init() does the most
0742  * commonly done thing, which is to open the DSO database
0743  *
0744  */
0745 class AsyncDBManager : public QObject {
0746     // Note: Follows the active object pattern described here
0747     // https://youtu.be/SncJ3D-fO7g?list=PL6CJYn40gN6jgr-Rpl3J4XDQYhmUnxb-g&t=272
0748     Q_OBJECT
0749 
0750 private:
0751     std::shared_ptr<DBManager> m_manager;
0752     std::queue<std::unique_ptr<CatalogObjectList>> m_result;
0753     std::shared_ptr<QThread> m_thread;
0754     QMutex m_resultMutex;
0755 
0756 public:
0757     template <typename... Args>
0758     using DBManagerMethod = CatalogObjectList (DBManager::*)(Args...);
0759 
0760     /**
0761      * Constructor, does nothing. Call init() to do the actual setup after the QThread stars
0762      */
0763     template <typename... Args>
0764     AsyncDBManager(Args... args)
0765         : QObject(nullptr)
0766         , m_manager(nullptr)
0767         , m_thread(nullptr)
0768     {
0769         m_thread.reset(new QThread);
0770         moveToThread(m_thread.get());
0771         connect(m_thread.get(), &QThread::started, [&]() {
0772             init(args...);
0773         });
0774         m_thread->start();
0775 
0776         /*
0777          * This is an attempt to fix a bug introduced in 5a2ba9f8e8b275f44b7593a50ca66f09cb2f985d
0778          * where KStars Crashes on MacOS when launched by double clicking (NOT by Terminal or QT Creator)
0779          * and then the find dialog is accessed.  For some reason, this fixes it?
0780          */
0781         #ifdef Q_OS_OSX
0782             QThread::msleep(100);
0783         #endif
0784     }
0785 
0786     ~AsyncDBManager()
0787     {
0788         QMetaObject::invokeMethod(this, "cleanup");
0789         m_thread->wait();
0790     }
0791 
0792     /**
0793      * Return a pointer to the DBManager, for non-DB functionalities
0794      */
0795     inline std::shared_ptr<DBManager> manager() { return m_manager; }
0796 
0797     /**
0798      * Return a pointer to the QThread object
0799      */
0800     inline std::shared_ptr<QThread> thread() { return m_thread; }
0801 
0802     /**
0803      * Construct the DBManager object
0804      *
0805      * Should be done in the thread corresponding to this object
0806      */
0807     template <typename... Args> void init(Args&&... args)
0808     {
0809         m_manager = std::make_shared<DBManager>(std::forward<Args>(args)...);
0810     }
0811 
0812     /**
0813      * A generic wrapper to call any method on DBManager that returns a CatalogObjectList
0814      *
0815      * For practical use examples, see \sa
0816      * AsyncDBManager::find_objects_by_name below which uses this
0817      * wrapper
0818      */
0819     template <typename... Args>
0820     void execute(DBManagerMethod<Args...> dbManagerMethod, Args... args)
0821     {
0822         // c.f. https://stackoverflow.com/questions/25392935/wrap-a-function-pointer-in-c-with-variadic-template
0823 
0824         // N.B. The perfect forwarding idiom is not used because we
0825         // also want to be able to bind to lvalue references, whereas
0826         // the types deduced for the arguments has to match the type
0827         // deduced to identify the function overload to be used.
0828         QMutexLocker _{&m_resultMutex};
0829         m_result.emplace(
0830             std::make_unique<CatalogObjectList>((m_manager.get()->*dbManagerMethod)(args...))
0831             );
0832         emit resultReady();
0833     }
0834 
0835     /**
0836      * execute(), but specialized to find_objects_by_name
0837      *
0838      * @fixme Code duplication needed to support older compilers
0839      */
0840     void _find_objects_by_name(const QString& name, const int limit, const bool exactMatchOnly)
0841     {
0842         // FIXME: Remove this and use execute() once C++1x compilers
0843         // support variadic template type deduction properly
0844         QMutexLocker _{&m_resultMutex};
0845         m_result.emplace(
0846             std::make_unique<CatalogObjectList>((m_manager.get()->find_objects_by_name)(name, limit, exactMatchOnly))
0847             );
0848         emit resultReady();
0849     }
0850 
0851 signals:
0852     void resultReady(void);
0853     void threadReady(void);
0854 
0855 
0856 public slots:
0857 
0858     void init()
0859     {
0860         m_manager = std::make_shared<DBManager>(dso_db_path());
0861     }
0862 
0863     /**
0864      * Calls the given DBManager method, storing the result for later retrieval
0865      *
0866      * \p dbManagerMethod method to execute (must return a CatalogObjectList)
0867      * \p args arguments to supply to the method
0868      */
0869 
0870     void find_objects_by_name(const QString& name, const int limit = -1)
0871     {
0872         // N.B. One cannot have a function pointer to a function with
0873         // default arguments, so all arguments must be supplied here
0874 
0875         // FIXME: Uncomment to use generic wrapper execute() once
0876         // compilers gain proper type-deduction support
0877         // execute<const QString&, const int, const bool>(
0878         //     &DBManager::find_objects_by_name,
0879         //     name, limit, false);
0880 
0881         _find_objects_by_name(name, limit, false);
0882     }
0883 
0884     void find_objects_by_name_exact(const QString &name)
0885     {
0886         // FIXME: Uncomment to use generic wrapper execute() once
0887         // compilers gain proper type-deduction support
0888         // execute<const QString&, const int, const bool>(
0889         //     &DBManager::find_objects_by_name,
0890         //     name, 1, true);
0891 
0892         _find_objects_by_name(name, 1, true);
0893     }
0894 
0895     /**
0896      * Returns the result of the previous call, or a nullptr if none exists
0897      */
0898     std::unique_ptr<CatalogObjectList> result()
0899     {
0900         QMutexLocker _{&m_resultMutex};
0901         if (m_result.empty())
0902         {
0903             return std::unique_ptr<CatalogObjectList>();
0904         }
0905         std::unique_ptr<CatalogObjectList> result = std::move(m_result.front());
0906         m_result.pop();
0907         return result;
0908     }
0909 
0910 private slots:
0911 
0912     void cleanup()
0913     {
0914         Q_ASSERT(m_manager.use_count() == 1);
0915         m_manager.reset();
0916         m_thread->quit();
0917     }
0918 
0919     void emitReady()
0920     {
0921         emit threadReady();
0922     }
0923 
0924 };
0925 
0926 } // namespace CatalogsDB