File indexing completed on 2025-01-19 09:45:57
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 ®ion) 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 };