File indexing completed on 2024-07-14 07:17:19

0001 /*
0002     SPDX-FileCopyrightText: 2005 Thomas Kabelmann <thomas.kabelmann@gmx.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "asteroidscomponent.h"
0008 #include "ksutils.h"
0009 
0010 #ifndef KSTARS_LITE
0011 #include "kstars.h"
0012 #endif
0013 #include "ksfilereader.h"
0014 #include "kstarsdata.h"
0015 #include "kstars_debug.h"
0016 #include "Options.h"
0017 #include "solarsystemcomposite.h"
0018 #include "skycomponent.h"
0019 #include "skylabeler.h"
0020 #ifndef KSTARS_LITE
0021 #include "skymap.h"
0022 #else
0023 #include "kstarslite.h"
0024 #endif
0025 #include "skypainter.h"
0026 #include "auxiliary/kspaths.h"
0027 #include "auxiliary/ksnotification.h"
0028 #include "auxiliary/filedownloader.h"
0029 #include "projections/projector.h"
0030 
0031 #include <KLocalizedString>
0032 
0033 #include <QDebug>
0034 #include <QStandardPaths>
0035 #include <QHttpMultiPart>
0036 #include <QPen>
0037 
0038 #include <cmath>
0039 
0040 AsteroidsComponent::AsteroidsComponent(SolarSystemComposite *parent)
0041     : BinaryListComponent(this, "asteroids"), SolarSystemListComponent(parent)
0042 {
0043     loadData();
0044 }
0045 
0046 bool AsteroidsComponent::selected()
0047 {
0048     return Options::showAsteroids();
0049 }
0050 
0051 /*
0052  * @short Initialize the asteroids list.
0053  * Reads in the asteroids data from the asteroids.dat file
0054  * and writes it into the Binary File;
0055  *
0056  * The data file is a CSV file with the following columns :
0057  * @li 1 full name [string]
0058  * @li 2 Modified Julian Day of orbital elements [int]
0059  * @li 3 perihelion distance in AU [double]
0060  * @li 4 semi-major axis
0061  * @li 5 eccentricity of orbit [double]
0062  * @li 6 inclination angle of orbit in degrees [double]
0063  * @li 7 argument of perihelion in degrees [double]
0064  * @li 8 longitude of the ascending node in degrees [double]
0065  * @li 9 mean anomaly
0066  * @li 10 time of perihelion passage (YYYYMMDD.DDD) [double]
0067  * @li 11 orbit solution ID [string]
0068  * @li 12 absolute magnitude [float]
0069  * @li 13 slope parameter [float]
0070  * @li 14 Near-Earth Object (NEO) flag [bool]
0071  * @li 15 comet total magnitude parameter [float] (we should remove this column)
0072  * @li 16 comet nuclear magnitude parameter [float] (we should remove this column)
0073  * @li 17 object diameter (from equivalent sphere) [float]
0074  * @li 18 object bi/tri-axial ellipsoid dimensions [string]
0075  * @li 19 geometric albedo [float]
0076  * @li 20 rotation period [float]
0077  * @li 21 orbital period [float]
0078  * @li 22 earth minimum orbit intersection distance [double]
0079  * @li 23 orbit classification [string]
0080  */
0081 void AsteroidsComponent::loadDataFromText()
0082 {
0083     QString name, full_name, orbit_id, orbit_class, dimensions;
0084     int mJD;
0085     double q, a, e, dble_i, dble_w, dble_N, dble_M, H, G, earth_moid;
0086     long double JD;
0087     float diameter, albedo, rot_period, period;
0088     bool neo;
0089 
0090     emitProgressText(i18n("Loading asteroids"));
0091     qCInfo(KSTARS) << "Loading asteroids";
0092 
0093     try
0094     {
0095         KSUtils::JPLParser ast_parser(filepath_txt);
0096         auto fieldMap = ast_parser.fieldMap();
0097         bool isString = fieldMap.count("epoch_mjd") == 1;
0098 
0099         ast_parser.for_each(
0100             [&](const auto & get)
0101         {
0102             full_name = get("full_name").toString();
0103             full_name = full_name.trimmed();
0104             int catN  = full_name.section(' ', 0, 0).toInt();
0105             name      = full_name.section(' ', 1, -1);
0106 
0107             //JM temporary hack to avoid Europa,Io, and Asterope duplication
0108             if (name == i18nc("Asteroid name (optional)", "Europa") ||
0109                     name == i18nc("Asteroid name (optional)", "Io") ||
0110                     name == i18nc("Asteroid name (optional)", "Asterope"))
0111                 name += i18n(" (Asteroid)");
0112 
0113             // JM 2022.08.26: Try to check if the file is in the new format
0114             // where epoch_mjd field is a string
0115             if (isString)
0116             {
0117                 mJD         = get("epoch_mjd").toString().toInt();
0118                 period      = get("per_y").toString().toDouble();
0119             }
0120             // If not fall back to old behavior
0121             else
0122             {
0123                 mJD         = get("epoch.mjd").toInt();
0124                 period      = get("per.y").toDouble();
0125             }
0126 
0127             q           = get("q").toString().toDouble();
0128             a           = get("a").toString().toDouble();
0129             e           = get("e").toString().toDouble();
0130             dble_i      = get("i").toString().toDouble();
0131             dble_w      = get("w").toString().toDouble();
0132             dble_N      = get("om").toString().toDouble();
0133             dble_M      = get("ma").toString().toDouble();
0134             orbit_id    = get("orbit_id").toString();
0135             H           = get("H").toString().toDouble();
0136             G           = get("G").toString().toDouble();
0137             neo         = get("neo").toString() == "Y";
0138             diameter    = get("diameter").toString().toFloat();
0139             dimensions  = get("extent").toString();
0140             albedo      = get("albedo").toString().toFloat();
0141             rot_period  = get("rot_per").toString().toFloat();
0142             earth_moid  = get("moid").toString().toDouble();
0143             orbit_class = get("class").toString();
0144 
0145             JD = static_cast<double>(mJD) + 2400000.5;
0146 
0147             KSAsteroid *new_asteroid = nullptr;
0148 
0149             // Diameter is missing from JPL data
0150             if (name == i18nc("Asteroid name (optional)", "Pluto"))
0151                 diameter = 2390;
0152 
0153             new_asteroid =
0154                 new KSAsteroid(catN, name, QString(), JD, a, e, dms(dble_i),
0155                                dms(dble_w), dms(dble_N), dms(dble_M), H, G);
0156 
0157             new_asteroid->setPerihelion(q);
0158             new_asteroid->setOrbitID(orbit_id);
0159             new_asteroid->setNEO(neo);
0160             new_asteroid->setDiameter(diameter);
0161             new_asteroid->setDimensions(dimensions);
0162             new_asteroid->setAlbedo(albedo);
0163             new_asteroid->setRotationPeriod(rot_period);
0164             new_asteroid->setPeriod(period);
0165             new_asteroid->setEarthMOID(earth_moid);
0166             new_asteroid->setOrbitClass(orbit_class);
0167             new_asteroid->setPhysicalSize(diameter);
0168             //new_asteroid->setAngularSize(0.005);
0169 
0170             appendListObject(new_asteroid);
0171 
0172             // Add name to the list of object names
0173             objectNames(SkyObject::ASTEROID).append(name);
0174             objectLists(SkyObject::ASTEROID)
0175             .append(QPair<QString, const SkyObject *>(name, new_asteroid));
0176         });
0177     }
0178     catch (const std::runtime_error &e)
0179     {
0180         qCInfo(KSTARS) << "Loading asteroid objects failed.";
0181         qCInfo(KSTARS) << " -> was trying to read " + filepath_txt;
0182         return;
0183     }
0184 }
0185 
0186 void AsteroidsComponent::draw(SkyPainter *skyp)
0187 {
0188     Q_UNUSED(skyp)
0189 #ifndef KSTARS_LITE
0190     if (!selected())
0191         return;
0192 
0193     bool hideLabels = !Options::showAsteroidNames() || (SkyMap::Instance()->isSlewing() && Options::hideLabels());
0194 
0195     double labelMagLimit            = Options::asteroidLabelDensity(); // Slider min value 0, max value 20.
0196     const double showMagLimit       = Options::magLimitAsteroid();
0197     const double lgmin              = log10(MINZOOM);
0198     const double lgmax              = log10(MAXZOOM);
0199     const double lgz                = log10(Options::zoomFactor());
0200     const double densityLabelFactor = 10.0; // Value of 10.0 influences the slider mag value [0, 2],
0201                                             // where a value 5.0 influences the slider mag value [0, 4].
0202     const double zoomLimit          = (lgz - lgmin) / (lgmax - lgmin); // Min-max normalize into [lgmin, lgmax].
0203 
0204     // Map labelMagLimit into interval [0, 20.0 / densityLabelFactor]
0205     labelMagLimit = std::max(1e-3, labelMagLimit) / densityLabelFactor;
0206     // If zooming closer, then the labelMagLimit gets closer to showMagLimit value.
0207     // If sliding density value to the right, then the labelMagLimit gets closer to showMagLimit value.
0208     // It is however assured that labelMagLimit <= showMagLimit.
0209     labelMagLimit = showMagLimit - 20.0 / densityLabelFactor + std::max(zoomLimit, labelMagLimit);
0210 
0211     foreach (SkyObject *so, m_ObjectList)
0212     {
0213         KSAsteroid *ast = dynamic_cast<KSAsteroid *>(so);
0214 
0215         if (!ast->toDraw() || std::isnan(ast->mag()) || ast->mag() > showMagLimit)
0216             continue;
0217 
0218         bool drawn = false;
0219 
0220         if (ast->image().isNull() == false)
0221             drawn = skyp->drawPlanet(ast);
0222         else
0223             drawn = skyp->drawAsteroid(ast);
0224 
0225         if (drawn && !hideLabels && ast->mag() <= labelMagLimit)
0226             SkyLabeler::AddLabel(ast, SkyLabeler::ASTEROID_LABEL);
0227     }
0228 #endif
0229 }
0230 
0231 SkyObject *AsteroidsComponent::objectNearest(SkyPoint *p, double &maxrad)
0232 {
0233     SkyObject *oBest = nullptr;
0234 
0235     if (!selected())
0236         return nullptr;
0237 
0238     for (auto o : m_ObjectList)
0239     {
0240         if (!((dynamic_cast<KSAsteroid*>(o)->toDraw())))
0241             continue;
0242 
0243         double r = o->angularDistanceTo(p).Degrees();
0244         if (r < maxrad)
0245         {
0246             oBest  = o;
0247             maxrad = r;
0248         }
0249     }
0250 
0251     return oBest;
0252 }
0253 
0254 void AsteroidsComponent::updateDataFile(bool isAutoUpdate)
0255 {
0256     delete (downloadJob);
0257     downloadJob = new FileDownloader();
0258 
0259     if (isAutoUpdate == false)
0260         downloadJob->setProgressDialogEnabled(true, i18n("Asteroid Update"),
0261                                               i18n("Downloading asteroids updates..."));
0262     downloadJob->registerDataVerification([&](const QByteArray & data)
0263     {
0264         return data.startsWith("{\"signature\"");
0265     });
0266 
0267     QObject::connect(downloadJob, SIGNAL(downloaded()), this, SLOT(downloadReady()));
0268     if (isAutoUpdate == false)
0269         QObject::connect(downloadJob, SIGNAL(error(QString)), this,
0270                          SLOT(downloadError(QString)));
0271 
0272     QUrl url = QUrl("https://ssd-api.jpl.nasa.gov/sbdb_query.api");
0273 
0274     QByteArray mag = QString::number(Options::magLimitAsteroidDownload()).toUtf8();
0275     QByteArray post_data =
0276         KSUtils::getJPLQueryString("a",
0277                                    "full_name,neo,H,G,diameter,extent,albedo,rot_per,"
0278                                    "orbit_id,epoch_mjd,e,a,q,i,om,w,ma,per_y,moid,class",
0279     QVector<KSUtils::JPLFilter> { { "H", "LT", mag } });
0280     downloadJob->post(url, post_data);
0281 }
0282 
0283 void AsteroidsComponent::downloadReady()
0284 {
0285     // Comment the first line
0286     QByteArray data = downloadJob->downloadedData();
0287 
0288     // Write data to asteroids.dat
0289     QFile file(QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation))
0290                .filePath("asteroids.dat"));
0291     if (file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
0292     {
0293         file.write(data);
0294         file.close();
0295     }
0296     else
0297         qCWarning(KSTARS) << "Failed writing asteroid data to" << file.fileName();
0298 
0299     QString focusedAstroid;
0300 
0301 #ifdef KSTARS_LITE
0302     SkyObject *foc = KStarsLite::Instance()->map()->focusObject();
0303     if (foc && foc->type() == SkyObject::ASTEROID)
0304     {
0305         focusedAstroid = foc->name();
0306         KStarsLite::Instance()->map()->setFocusObject(nullptr);
0307     }
0308 #else
0309     SkyObject *foc = KStars::Instance()->map()->focusObject();
0310     if (foc && foc->type() == SkyObject::ASTEROID)
0311     {
0312         focusedAstroid = foc->name();
0313         KStars::Instance()->map()->setFocusObject(nullptr);
0314     }
0315 
0316 #endif
0317     // Reload asteroids
0318     loadData(true);
0319 
0320 #ifdef KSTARS_LITE
0321     KStarsLite::Instance()->data()->setFullTimeUpdate();
0322     if (!focusedAstroid.isEmpty())
0323         KStarsLite::Instance()->map()->setFocusObject(
0324             KStarsLite::Instance()->data()->objectNamed(focusedAstroid));
0325 #else
0326     if (!focusedAstroid.isEmpty())
0327         KStars::Instance()->map()->setFocusObject(
0328             KStars::Instance()->data()->objectNamed(focusedAstroid));
0329     KStars::Instance()->data()->setFullTimeUpdate();
0330 #endif
0331     downloadJob->deleteLater();
0332 }
0333 
0334 void AsteroidsComponent::downloadError(const QString &errorString)
0335 {
0336     KSNotification::error(i18n("Error downloading asteroids data: %1", errorString));
0337     qDebug() << Q_FUNC_INFO << i18n("Error downloading asteroids data: %1", errorString);
0338     downloadJob->deleteLater();
0339 }