File indexing completed on 2024-04-14 03:43:03

0001 /*
0002     SPDX-FileCopyrightText: 2001 Jason Harris <jharris@30doradus.org>
0003     SPDX-FileCopyrightText: 2021 Valentin Boettcher <hiro at protagon.space; @hiro98:tchncs.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "catalogscomponent.h"
0009 #include "skypainter.h"
0010 #include "skymap.h"
0011 #include "kstarsdata.h"
0012 #include "Options.h"
0013 #include "MeshIterator.h"
0014 #include "projections/projector.h"
0015 #include "skylabeler.h"
0016 #include "kstars_debug.h"
0017 #include "kstars.h"
0018 #include "skymapcomposite.h"
0019 #include "kspaths.h"
0020 #include "import_skycomp.h"
0021 
0022 #include <QtConcurrent>
0023 
0024 #include <cmath>
0025 
0026 constexpr std::size_t expectedKnownMagObjectsPerTrixel = 500;
0027 constexpr std::size_t expectedUnknownMagObjectsPerTrixel = 1500;
0028 
0029 CatalogsComponent::CatalogsComponent(SkyComposite *parent, const QString &db_filename,
0030                                      bool load_default)
0031     : SkyComponent(parent)
0032     , m_db_manager(db_filename)
0033     , m_skyMesh{ SkyMesh::Create(m_db_manager.htmesh_level()) }
0034     , m_mainCache(m_skyMesh->size(), calculateCacheSize(Options::dSOCachePercentage()))
0035     , m_unknownMagCache(m_skyMesh->size(), calculateCacheSize(Options::dSOCachePercentage()))
0036 {
0037     if (load_default)
0038     {
0039         const auto &default_file = KSPaths::locate(QStandardPaths::AppLocalDataLocation,
0040                                    Options::dSODefaultCatalogFilename());
0041 
0042         if (QFile(default_file).exists())
0043         {
0044             m_db_manager.import_catalog(default_file, false);
0045         }
0046     }
0047 
0048     m_catalog_colors = m_db_manager.get_catalog_colors();
0049     tryImportSkyComponents();
0050     qCInfo(KSTARS) << "Loaded DSO catalogs.";
0051 }
0052 
0053 double compute_maglim()
0054 {
0055     double maglim = Options::magLimitDrawDeepSky();
0056 
0057     //adjust maglimit for ZoomLevel
0058     static const double lgmin{ log10(MINZOOM) };
0059     static const double lgmax{ log10(MAXZOOM) };
0060     double lgz = log10(Options::zoomFactor());
0061     if (lgz <= 0.75 * lgmax)
0062         maglim -=
0063             (Options::magLimitDrawDeepSky() - Options::magLimitDrawDeepSkyZoomOut()) *
0064             (0.75 * lgmax - lgz) / (0.75 * lgmax - lgmin);
0065 
0066     return maglim;
0067 }
0068 
0069 void CatalogsComponent::draw(SkyPainter *skyp)
0070 {
0071     if (!selected() || Options::zoomFactor() < Options::dSOMinZoomFactor())
0072         return;
0073 
0074     KStarsData *data          = KStarsData::Instance();
0075     const auto &default_color = data->colorScheme()->colorNamed("DSOColor");
0076     skyp->setPen(default_color);
0077     skyp->setBrush(Qt::NoBrush);
0078 
0079     bool showUnknownMagObjects = Options::showUnknownMagObjects();
0080     auto maglim                = compute_maglim();
0081 
0082     auto &labeler = *SkyLabeler::Instance();
0083     labeler.setPen(
0084         QColor(KStarsData::Instance()->colorScheme()->colorNamed("DSNameColor")));
0085     const auto &color_scheme = KStarsData::Instance()->colorSchemeFileName();
0086 
0087     auto &map       = *SkyMap::Instance();
0088     auto hideLabels = (map.isSlewing() && Options::hideOnSlew()) ||
0089                       !(Options::showDeepSkyMagnitudes() || Options::showDeepSkyNames());
0090 
0091     const auto label_padding{ 1 + (1 - (Options::deepSkyLabelDensity() / 100)) * 50 };
0092     auto &proj = *map.projector();
0093 
0094     updateSkyMesh(map);
0095 
0096     size_t num_trixels{ 0 };
0097     const auto zoomFactor = Options::zoomFactor();
0098     const double sizeScale = dms::PI * zoomFactor / 10800.0; // FIXME: magic number 10800
0099 
0100     // Note: This function handles objects of known and unknown
0101     // magnitudes differently. This is mostly because objects in the
0102     // PGC catalog do not have magnitude information, which leads to
0103     // hundreds of thousands of objects of unknown magnitude. This
0104     // means we must find some way to:
0105     // (a) Prevent PGC objects from flooding the screen at low zoom
0106     //     levels
0107     // (b) Prevent PGC labels from crowding out more common NGC/M
0108     //     labels
0109     // (c) Process the large number of PGC objects efficiently at low
0110     //     zoom levels (even when they are not actually displayed)
0111 
0112     // The problem is that normally, we have relied on magnitude as a
0113     // proxy to prioritize object labels, as well as to short-circuit
0114     // filtering decisions by having our objects sorted by
0115     // magnitude. We can no longer do that.
0116 
0117     // Therefore, we first handle objects of known magnitude, given
0118     // them priority in SkyLabeler label-space, solving (b). Then, we
0119     // place aggressive filters on objects of unknown magnitude and
0120     // size, by explicitly special-casing galaxies: generally
0121     // speaking, large nebulae and small galaxies don't have known
0122     // magnitudes; so we allow objects of unknown size and unknown
0123     // magnitudes to be displayed at lower zoom levels provided they
0124     // are not galaxies. With these series of tricks, lest we say
0125     // hacks, we solve (a). Finally, we make the unknown mag loop
0126     // concurrent since we are unable to bail-out when its time like
0127     // we can with the objects of known magnitude, addressing
0128     // (c). Thus, the user experience with the PGC flood of 1 million
0129     // galaxies of unknown magnitude, and many of them also of unknown
0130     // size, remains smooth.
0131 
0132     // Helper lambda to fill the appropriate cache for a given trixel
0133     auto fillCache = [&](
0134         TrixelCache<ObjectList>::element& cacheElement,
0135         ObjectList (CatalogsDB::DBManager::*fillFunction)(const int),
0136         Trixel trixel
0137         ) -> void {
0138         if (!cacheElement.is_set())
0139         {
0140             try
0141             {
0142                 cacheElement = (m_db_manager.*fillFunction)(trixel);
0143             }
0144             catch (const CatalogsDB::DatabaseError &e)
0145             {
0146                 qCCritical(KSTARS)
0147                     << "Could not load catalog objects in trixel: " << trixel << ", "
0148                     << e.what();
0149 
0150                 KMessageBox::detailedError(
0151                     nullptr, i18n("Could not load catalog objects in trixel: %1", trixel),
0152                     e.what());
0153 
0154                 throw; // do not silently fail
0155             }
0156         }
0157     };
0158 
0159     // Helper lambda to JIT update and draw
0160     auto drawObjects = [&](std::vector<CatalogObject*>& objects) {
0161         // TODO: If we are sure that JITupdate has no side effects
0162         // that may cause races etc, it will be worth parallelizing
0163 
0164         for (CatalogObject *object : objects) {
0165             object->JITupdate();
0166             auto &color = m_catalog_colors[object->catalogId()][color_scheme];
0167             if (!color.isValid())
0168             {
0169                 color = m_catalog_colors[object->catalogId()]["default"];
0170 
0171                 if (!color.isValid())
0172                 {
0173                     color = default_color;
0174                 }
0175             }
0176 
0177             skyp->setPen(color);
0178 
0179             if (Options::showInlineImages())
0180                 object->load_image();
0181 
0182             if (skyp->drawCatalogObject(*object) && !hideLabels)
0183             {
0184                 labeler.drawNameLabel(object, proj.toScreen(object), label_padding);
0185             }
0186         }
0187     };
0188 
0189     std::vector<CatalogObject*> drawListKnownMag;
0190     drawListKnownMag.reserve(expectedKnownMagObjectsPerTrixel);
0191 
0192     // Handle the objects of known magnitude
0193     MeshIterator region(m_skyMesh, DRAW_BUF);
0194     while (region.hasNext())
0195     {
0196         Trixel trixel = region.next();
0197         num_trixels++;
0198 
0199         // Fill the cache for this trixel
0200         auto &objectsKnownMag = m_mainCache[trixel];
0201         fillCache(objectsKnownMag, &CatalogsDB::DBManager::get_objects_in_trixel_no_nulls, trixel);
0202         drawListKnownMag.clear();
0203 
0204         // Filter based on magnitude and size
0205         for (const auto &object : objectsKnownMag.data())
0206         {
0207             const auto mag          = object.mag();
0208             const auto a            = object.a(); // major axis
0209             const double size       = a * sizeScale;
0210             const bool magCriterion = (mag < maglim);
0211 
0212             if (!magCriterion)
0213             {
0214                 break; // the known-mag objects are strictly sorted by
0215                        // magnitude, unknown magnitude first
0216             }
0217 
0218             bool sizeCriterion =
0219                 (size > 1.0 || size == 0 || zoomFactor > 2000.);
0220 
0221             if (!sizeCriterion)
0222                 break;
0223 
0224             drawListKnownMag.push_back(const_cast<CatalogObject*>(&object));
0225         }
0226 
0227         // JIT update and draw
0228         drawObjects(drawListKnownMag);
0229     }
0230 
0231     // Handle the objects of unknown magnitude
0232     if (showUnknownMagObjects)
0233     {
0234         std::vector<CatalogObject*> drawListUnknownMag;
0235         drawListUnknownMag.reserve(expectedUnknownMagObjectsPerTrixel);
0236         QMutex drawListUnknownMagLock;
0237 
0238         MeshIterator region(m_skyMesh, DRAW_BUF);
0239         while (region.hasNext())
0240         {
0241             Trixel trixel = region.next();
0242             drawListUnknownMag.clear();
0243 
0244             // Fill cache
0245             auto &objectsUnknownMag = m_unknownMagCache[trixel];
0246             fillCache(objectsUnknownMag, &CatalogsDB::DBManager::get_objects_in_trixel_null_mag, trixel);
0247 
0248             // Filter
0249             QtConcurrent::blockingMap(
0250                 objectsUnknownMag.data(),
0251                 [&](const auto &object)
0252                 {
0253                     auto a            = object.a(); // major axis
0254                     double size = a * sizeScale;
0255 
0256                     // For objects of unknown mag but known size, adjust
0257                     // display behavior as if it were 22 mags/arcsec² =
0258                     // 13.1 mag/arcmin² surface brightness, comparing it
0259                     // to the magnitude limit.
0260                     bool magCriterion = (a <= 0.0 || (13.1 - 5*log10(a)) < maglim);
0261 
0262                     if (!magCriterion)
0263                         return;
0264 
0265                     bool sizeCriterion =
0266                         (size > 1.0 || (size == 0 && object.type() != SkyObject::GALAXY) || zoomFactor > 10000.);
0267 
0268                     if (!sizeCriterion)
0269                         return;
0270 
0271                     QMutexLocker _{&drawListUnknownMagLock};
0272                     drawListUnknownMag.push_back(const_cast<CatalogObject*>(&object));
0273                 });
0274 
0275             // JIT update and draw
0276             drawObjects(drawListUnknownMag);
0277         }
0278 
0279     }
0280 
0281     // prune only if the to-be-pruned trixels are likely not visible
0282     // and we are not zooming
0283     m_mainCache.prune(num_trixels * 1.2);
0284     m_unknownMagCache.prune(num_trixels * 1.2);
0285 };
0286 
0287 void CatalogsComponent::updateSkyMesh(SkyMap &map, MeshBufNum_t buf)
0288 {
0289     SkyPoint *focus = map.focus();
0290     float radius    = map.projector()->fov();
0291     if (radius > 180.0)
0292         radius = 180.0;
0293 
0294     m_skyMesh->aperture(focus, radius + 1.0, buf);
0295 }
0296 
0297 CatalogObject &CatalogsComponent::insertStaticObject(const CatalogObject &obj)
0298 {
0299     auto trixel     = m_skyMesh->index(&obj);
0300     auto &lst       = m_static_objects[trixel];
0301     auto found_iter = std::find(lst.begin(), lst.end(), obj);
0302 
0303     // Ideally, we would remove the object from ObjectsList if it's
0304     // respective catalog is disabled, but there ain't a good way to
0305     // do this right now
0306 
0307     if (!(found_iter == lst.end()))
0308     {
0309         auto &found = *found_iter;
0310         found       = obj;
0311         found.JITupdate();
0312         return found;
0313     }
0314 
0315     auto copy = obj;
0316     copy.JITupdate();
0317 
0318     lst.push_back(std::move(copy));
0319     auto &inserted = lst.back();
0320 
0321     // we don't bother with translations here
0322     auto &object_list = objectLists(inserted.type());
0323 
0324     object_list.append({ inserted.name(), &inserted });
0325     if (inserted.longname() != inserted.name())
0326         object_list.append({ inserted.longname(), &inserted });
0327 
0328     return inserted;
0329 }
0330 
0331 SkyObject *CatalogsComponent::findByName(const QString &name, bool exact)
0332 {
0333     auto objects = exact ? m_db_manager.find_objects_by_name(name, 1, true)
0334                    : m_db_manager.find_objects_by_name(name);
0335 
0336     if (objects.size() == 0)
0337         return nullptr;
0338 
0339     return &insertStaticObject(objects.front());
0340 }
0341 
0342 void CatalogsComponent::objectsInArea(QList<SkyObject *> &list, const SkyRegion &region)
0343 {
0344     if (!selected())
0345         return;
0346 
0347     for (SkyRegion::const_iterator it = region.constBegin(); it != region.constEnd();
0348             ++it)
0349     {
0350         try
0351         {
0352             for (auto &dso : m_db_manager.get_objects_in_trixel(it.key()))
0353             {
0354                 auto &obj = insertStaticObject(dso);
0355                 list.append(&obj);
0356             }
0357         }
0358         catch (const CatalogsDB::DatabaseError &e)
0359         {
0360             qCCritical(KSTARS) << "Could not load catalog objects in trixel: " << it.key()
0361                                << ", " << e.what();
0362 
0363             KMessageBox::detailedError(
0364                 nullptr, i18n("Could not load catalog objects in trixel: %1", it.key()),
0365                 e.what());
0366             throw; // do not silently fail
0367         }
0368     }
0369 }
0370 
0371 SkyObject *CatalogsComponent::objectNearest(SkyPoint *p, double &maxrad)
0372 {
0373     if (!selected())
0374         return nullptr;
0375 
0376     m_skyMesh->aperture(p, maxrad, OBJ_NEAREST_BUF);
0377     MeshIterator region(m_skyMesh, OBJ_NEAREST_BUF);
0378     double smallest_r{ 360 };
0379     CatalogObject nearest{};
0380     bool found{ false };
0381 
0382     while (region.hasNext())
0383     {
0384         auto trixel = region.next();
0385         try
0386         {
0387             auto objects = m_db_manager.get_objects_in_trixel(trixel);
0388             if (!found)
0389                 found = objects.size() > 0;
0390 
0391             for (auto &obj : objects)
0392             {
0393                 obj.JITupdate();
0394 
0395                 double r = obj.angularDistanceTo(p).Degrees();
0396                 if (r < smallest_r)
0397                 {
0398                     smallest_r = r;
0399                     nearest    = obj;
0400                 }
0401             }
0402         }
0403         catch (const CatalogsDB::DatabaseError &e)
0404         {
0405             KMessageBox::detailedError(
0406                 nullptr, i18n("Could not load catalog objects in trixel: %1", trixel),
0407                 e.what());
0408             throw; // do not silently fail
0409         }
0410     }
0411 
0412     if (!found)
0413         return nullptr;
0414 
0415     maxrad = smallest_r;
0416 
0417     return &insertStaticObject(nearest);
0418 }
0419 
0420 void CatalogsComponent::tryImportSkyComponents()
0421 {
0422     auto skycom_db = SkyComponentsImport::get_skycomp_db();
0423     if (!skycom_db.first)
0424         return;
0425 
0426     const auto move_skycompdb = [&]()
0427     {
0428         const auto &path = skycom_db.second.databaseName();
0429         const auto &new_path =
0430             QString("%1.%2.backup").arg(path).arg(QDateTime::currentMSecsSinceEpoch());
0431 
0432         QFile::rename(path, new_path);
0433     };
0434 
0435     const auto resp = KMessageBox::questionYesNoCancel(
0436                           nullptr, i18n("Import custom and internet resolved objects "
0437                                         "from the old DSO database into the new one?"));
0438 
0439     if (resp != KMessageBox::Yes)
0440     {
0441         move_skycompdb();
0442         return;
0443     }
0444 
0445     const auto &success = SkyComponentsImport::get_objects(skycom_db.second);
0446     if (!std::get<0>(success))
0447         KMessageBox::detailedError(nullptr, i18n("Could not import the objects."),
0448                                    std::get<1>(success));
0449 
0450     const auto &add_success =
0451         m_db_manager.add_objects(CatalogsDB::user_catalog_id, std::get<2>(success));
0452 
0453     if (!add_success.first)
0454         KMessageBox::detailedError(nullptr, i18n("Could not import the objects."),
0455                                    add_success.second);
0456     else
0457     {
0458         KMessageBox::information(
0459             nullptr, i18np("Successfully added %1 object to the user catalog.",
0460                            "Successfully added %1 objects to the user catalog.",
0461                            std::get<2>(success).size()));
0462         move_skycompdb();
0463     }
0464 };