1 /*
2     SPDX-FileCopyrightText: 2005 Jason Harris <kstars@30doradus.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "cometscomponent.h"
8 
9 #ifndef KSTARS_LITE
10 #include "kstars.h"
11 #endif
12 #include "ksfilereader.h"
13 #include "kspaths.h"
14 #include "kstarsdata.h"
15 #include "ksutils.h"
16 #include "ksnotification.h"
17 #include "kstars_debug.h"
18 #ifndef KSTARS_LITE
19 #include "skymap.h"
20 #else
21 #include "kstarslite.h"
22 #endif
23 #include "Options.h"
24 #include "skylabeler.h"
25 #include "skypainter.h"
26 #include "solarsystemcomposite.h"
27 #include "auxiliary/filedownloader.h"
28 #include "auxiliary/kspaths.h"
29 #include "projections/projector.h"
30 #include "skyobjects/kscomet.h"
31 
32 #include <QFile>
33 #include <QHttpMultiPart>
34 #include <QPen>
35 #include <QStandardPaths>
36 
37 #include <cmath>
38 
CometsComponent(SolarSystemComposite * parent)39 CometsComponent::CometsComponent(SolarSystemComposite *parent)
40     : SolarSystemListComponent(parent)
41 {
42     loadData();
43 }
44 
selected()45 bool CometsComponent::selected()
46 {
47     return Options::showComets();
48 }
49 
50 /*
51  * @short Initialize the comets list.
52  * Reads in the comets data from the comets.dat file.
53  *
54  * Populate the list of Comets from the data file.
55  * The data file is a CSV file with the following columns :
56  * @li 1 full name [string]
57  * @li 2 modified julian day of orbital elements [int]
58  * @li 3 perihelion distance in AU [double]
59  * @li 4 eccentricity of orbit [double]
60  * @li 5 inclination angle of orbit in degrees [double]
61  * @li 6 argument of perihelion in degrees [double]
62  * @li 7 longitude of the ascending node in degrees [double]
63  * @li 8 time of perihelion passage (YYYYMMDD.DDD) [double]
64  * @li 9 orbit solution ID [string]
65  * @li 10 Near-Earth Object (NEO) flag [bool]
66  * @li 11 comet total magnitude parameter [float]
67  * @li 12 comet nuclear magnitude parameter [float]
68  * @li 13 object diameter (from equivalent sphere) [float]
69  * @li 14 object bi/tri-axial ellipsoid dimensions [string]
70  * @li 15 geometric albedo [float]
71  * @li 16 rotation period [float]
72  * @li 17 orbital period [float]
73  * @li 18 earth minimum orbit intersection distance [double]
74  * @li 19 orbit classification [string]
75  * @li 20 comet total magnitude slope parameter
76  * @li 21 comet nuclear magnitude slope parameter
77  * @note See KSComet constructor for more details.
78  */
loadData()79 void CometsComponent::loadData()
80 {
81     QString name, orbit_class;
82 
83     emitProgressText(i18n("Loading comets"));
84     qCInfo(KSTARS) << "Loading comets";
85 
86     qDeleteAll(m_ObjectList);
87     m_ObjectList.clear();
88 
89     objectNames(SkyObject::COMET).clear();
90     objectLists(SkyObject::COMET).clear();
91 
92     QString file_name = KSPaths::locate(QStandardPaths::AppDataLocation, QString("cometels.json.gz"));
93 
94     try
95     {
96         KSUtils::MPCParser com_parser(file_name);
97         com_parser.for_each(
98             [&](const auto & get)
99         {
100             KSComet *com = nullptr;
101             name = get("Designation_and_name").toString();
102 
103             int perihelion_year, perihelion_month, perihelion_day, perihelion_hour, perihelion_minute, perihelion_second;
104 
105             // Perihelion Distance in AU
106             double perihelion_distance = get("Perihelion_dist").toDouble();
107             // Orbital Eccentricity
108             double eccentricity = get("e").toDouble();
109             // Argument of perihelion, J2000.0 (degrees)
110             double perihelion_argument = get("Peri").toDouble();
111             // Longitude of the ascending node, J2000.0 (degrees)
112             double ascending_node = get("Node").toDouble();
113             // Inclination in degrees, J2000.0 (degrees)
114             double inclination = get("i").toDouble();
115 
116             // Perihelion Date
117             perihelion_year = get("Year_of_perihelion").toInt();
118             perihelion_month = get("Month_of_perihelion").toInt();
119             // Stored as double in MPC
120             double peri_day = get("Day_of_perihelion").toDouble();
121             perihelion_day = static_cast<int>(peri_day);
122             double peri_hour = (peri_day - perihelion_day) * 24;
123             perihelion_hour = static_cast<int>(peri_hour);
124             perihelion_minute = static_cast<int>((peri_hour - perihelion_hour) * 60);
125             perihelion_second = ( (( peri_hour - perihelion_hour) * 60) - perihelion_minute) * 60;
126 
127             long double Tp = KStarsDateTime(QDate(perihelion_year, perihelion_month, perihelion_day),
128                                             QTime(perihelion_hour, perihelion_minute, perihelion_second)).djd();
129 
130             // Orbit type
131             orbit_class = get("Orbit_type").toString();
132             double absolute_magnitude = get("H").toDouble();
133             double slope_parameter = get("G").toDouble();
134 
135             com = new KSComet(name,
136                               QString(),
137                               perihelion_distance,
138                               eccentricity,
139                               dms(inclination),
140                               dms(perihelion_argument),
141                               dms(ascending_node),
142                               Tp,
143                               absolute_magnitude,
144                               101.0,
145                               slope_parameter,
146                               101.0);
147 
148             com->setOrbitClass(orbit_class);
149             com->setAngularSize(0.005);
150             appendListObject(com);
151 
152             // Add *short* name to the list of object names
153             objectNames(SkyObject::COMET).append(com->name());
154             objectLists(SkyObject::COMET).append(QPair<QString, const SkyObject *>(com->name(), com));
155         });
156     }
157     catch (const std::runtime_error)
158     {
159         qCInfo(KSTARS) << "Loading comets failed.";
160         qCInfo(KSTARS) << " -> was trying to read " + file_name;
161         return;
162     }
163 }
164 
165 // Used for JPL Data
166 // DO NOT REMOVE, we can revert to JPL at any time.
167 //void CometsComponent::loadData()
168 //{
169 //    QString name, orbit_id, orbit_class, dimensions;
170 
171 //    emitProgressText(i18n("Loading comets"));
172 //    qCInfo(KSTARS) << "Loading comets";
173 
174 //    qDeleteAll(m_ObjectList);
175 //    m_ObjectList.clear();
176 
177 //    objectNames(SkyObject::COMET).clear();
178 //    objectLists(SkyObject::COMET).clear();
179 
180 //    QString file_name =
181 //        KSPaths::locate(QStandardPaths::AppDataLocation, QString("comets.dat"));
182 
183 //    try
184 //    {
185 //        KSUtils::JPLParser com_parser(file_name);
186 //        com_parser.for_each(
187 //            [&](const auto &get)
188 //            {
189 //                KSComet *com = nullptr;
190 //                name         = get("full_name").toString();
191 //                name         = name.trimmed();
192 //                bool neo;
193 //                double q, e, dble_i, dble_w, dble_N, Tp, earth_moid;
194 //                float M1, M2, K1, K2, diameter, albedo, rot_period, period;
195 //                q        = get("q").toString().toDouble();
196 //                e        = get("e").toString().toDouble();
197 //                dble_i   = get("i").toString().toDouble();
198 //                dble_w   = get("w").toString().toDouble();
199 //                dble_N   = get("om").toString().toDouble();
200 //                Tp       = get("tp").toString().toDouble();
201 //                orbit_id = get("orbit_id").toString();
202 //                neo      = get("neo").toString() == "Y";
203 
204 //                if (get("M1").toString().toFloat() == 0.0)
205 //                    M1 = 101.0;
206 //                else
207 //                    M1 = get("M1").toString().toFloat();
208 
209 //                if (get("M2").toString().toFloat() == 0.0)
210 //                    M2 = 101.0;
211 //                else
212 //                    M2 = get("M2").toString().toFloat();
213 
214 //                diameter    = get("diameter").toString().toFloat();
215 //                dimensions  = get("extent").toString();
216 //                albedo      = get("albedo").toString().toFloat();
217 //                rot_period  = get("rot_per").toString().toFloat();
218 //                period      = get("per.y").toDouble();
219 //                earth_moid  = get("moid").toString().toDouble();
220 //                orbit_class = get("class").toString();
221 //                K1          = get("H").toString().toFloat();
222 //                K2          = get("G").toString().toFloat();
223 
224 //                com = new KSComet(name, QString(), q, e, dms(dble_i), dms(dble_w),
225 //                                  dms(dble_N), Tp, M1, M2, K1, K2);
226 //                com->setOrbitID(orbit_id);
227 //                com->setNEO(neo);
228 //                com->setDiameter(diameter);
229 //                com->setDimensions(dimensions);
230 //                com->setAlbedo(albedo);
231 //                com->setRotationPeriod(rot_period);
232 //                com->setPeriod(period);
233 //                com->setEarthMOID(earth_moid);
234 //                com->setOrbitClass(orbit_class);
235 //                com->setAngularSize(0.005);
236 //                appendListObject(com);
237 
238 //                // Add *short* name to the list of object names
239 //                objectNames(SkyObject::COMET).append(com->name());
240 //                objectLists(SkyObject::COMET)
241 //                    .append(QPair<QString, const SkyObject *>(com->name(), com));
242 //            });
243 //    }
244 //    catch (const std::runtime_error &e)
245 //    {
246 //        qCInfo(KSTARS) << "Loading comets failed.";
247 //        qCInfo(KSTARS) << " -> was trying to read " + file_name;
248 //        return;
249 //    }
250 //}
251 
draw(SkyPainter * skyp)252 void CometsComponent::draw(SkyPainter *skyp)
253 {
254     Q_UNUSED(skyp)
255 #ifndef KSTARS_LITE
256     if (!selected() || Options::zoomFactor() < 10 * MINZOOM)
257         return;
258 
259     bool hideLabels       = !Options::showCometNames() || (SkyMap::Instance()->isSlewing() && Options::hideLabels());
260     double rsunLabelLimit = Options::maxRadCometName();
261 
262     //FIXME: Should these be config'able?
263     skyp->setPen(QPen(QColor("transparent")));
264     skyp->setBrush(QBrush(QColor("white")));
265 
266     for (auto so : m_ObjectList)
267     {
268         KSComet *com = dynamic_cast<KSComet *>(so);
269         double mag   = com->mag();
270         if (std::isnan(mag) == 0)
271         {
272             bool drawn = skyp->drawComet(com);
273             if (drawn && !(hideLabels || com->rsun() >= rsunLabelLimit))
274                 SkyLabeler::AddLabel(com, SkyLabeler::COMET_LABEL);
275         }
276     }
277 #endif
278 }
279 
280 // DO NOT REMOVE
281 //void CometsComponent::updateDataFile(bool isAutoUpdate)
282 //{
283 //    delete (downloadJob);
284 //    downloadJob = new FileDownloader();
285 
286 //    if (isAutoUpdate == false)
287 //        downloadJob->setProgressDialogEnabled(true, i18n("Comets Update"),
288 //                                              i18n("Downloading comets updates..."));
289 //    downloadJob->registerDataVerification([&](const QByteArray &data)
290 //                                          { return data.startsWith("{\"signature\""); });
291 
292 //    connect(downloadJob, SIGNAL(downloaded()), this, SLOT(downloadReady()));
293 
294 //    // For auto-update, we ignore errors
295 //    if (isAutoUpdate == false)
296 //        connect(downloadJob, SIGNAL(error(QString)), this, SLOT(downloadError(QString)));
297 
298 //    QUrl url             = QUrl("https://ssd-api.jpl.nasa.gov/sbdb_query.api");
299 //    QByteArray post_data = KSUtils::getJPLQueryString(
300 //        "c",
301 //        "full_name,epoch.mjd,q,e,i,w,om,tp,orbit_id,neo,"
302 //        "M1,M2,diameter,extent,albedo,rot_per,per.y,moid,H,G,class",
303 //        QVector<KSUtils::JPLFilter>{});
304 //    // FIXME: find out what { "Af", "!=", "D" } used to mean
305 
306 //    downloadJob->post(url, post_data);
307 //}
308 
updateDataFile(bool isAutoUpdate)309 void CometsComponent::updateDataFile(bool isAutoUpdate)
310 {
311     delete (downloadJob);
312     downloadJob = new FileDownloader();
313 
314     if (isAutoUpdate == false)
315         downloadJob->setProgressDialogEnabled(true, i18n("Comets Update"),
316                                               i18n("Downloading comets updates..."));
317 
318     connect(downloadJob, SIGNAL(downloaded()), this, SLOT(downloadReady()));
319 
320     // For auto-update, we ignore errors
321     if (isAutoUpdate == false)
322         connect(downloadJob, SIGNAL(error(QString)), this, SLOT(downloadError(QString)));
323 
324     QUrl url = QUrl("https://www.minorplanetcenter.net/Extended_Files/cometels.json.gz");
325     downloadJob->get(url);
326 }
327 
downloadReady()328 void CometsComponent::downloadReady()
329 {
330     // Comment the first line
331     QByteArray data = downloadJob->downloadedData();
332 
333     // Write data to cometels.json.gz
334     QFile file(QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation))
335                .filePath("cometels.json.gz"));
336     if (file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
337     {
338         file.write(data);
339         file.close();
340     }
341     else
342         qCWarning(KSTARS) << "Failed writing comet data to" << file.fileName();
343 
344     QString focusedComet;
345 
346 #ifdef KSTARS_LITE
347     SkyObject *foc = KStarsLite::Instance()->map()->focusObject();
348     if (foc && foc->type() == SkyObject::COMET)
349     {
350         focusedComet = foc->name();
351         KStarsLite::Instance()->map()->setFocusObject(nullptr);
352     }
353 #else
354     SkyObject *foc = KStars::Instance()->map()->focusObject();
355     if (foc && foc->type() == SkyObject::COMET)
356     {
357         focusedComet = foc->name();
358         KStars::Instance()->map()->setFocusObject(nullptr);
359     }
360 #endif
361 
362     // Reload comets
363     loadData();
364 
365 #ifdef KSTARS_LITE
366     KStarsLite::Instance()->data()->setFullTimeUpdate();
367     if (!focusedComet.isEmpty())
368         KStarsLite::Instance()->map()->setFocusObject(
369             KStarsLite::Instance()->data()->objectNamed(focusedComet));
370 #else
371     if (!focusedComet.isEmpty())
372         KStars::Instance()->map()->setFocusObject(
373             KStars::Instance()->data()->objectNamed(focusedComet));
374     KStars::Instance()->data()->setFullTimeUpdate();
375 #endif
376 
377     downloadJob->deleteLater();
378 }
379 
380 // DO NOT REMOVE
381 //void CometsComponent::downloadReady()
382 //{
383 //    // Comment the first line
384 //    QByteArray data = downloadJob->downloadedData();
385 
386 //    // Write data to comets.dat
387 //    QFile file(QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation))
388 //                   .filePath("comets.dat"));
389 //    if (file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
390 //    {
391 //        file.write(data);
392 //        file.close();
393 //    }
394 //    else
395 //        qCWarning(KSTARS) << "Failed writing comet data to" << file.fileName();
396 
397 //    QString focusedComet;
398 
399 //#ifdef KSTARS_LITE
400 //    SkyObject *foc = KStarsLite::Instance()->map()->focusObject();
401 //    if (foc && foc->type() == SkyObject::COMET)
402 //    {
403 //        focusedComet = foc->name();
404 //        KStarsLite::Instance()->map()->setFocusObject(nullptr);
405 //    }
406 //#else
407 //    SkyObject *foc = KStars::Instance()->map()->focusObject();
408 //    if (foc && foc->type() == SkyObject::COMET)
409 //    {
410 //        focusedComet = foc->name();
411 //        KStars::Instance()->map()->setFocusObject(nullptr);
412 //    }
413 //#endif
414 
415 //    // Reload comets
416 //    loadData();
417 
418 //#ifdef KSTARS_LITE
419 //    KStarsLite::Instance()->data()->setFullTimeUpdate();
420 //    if (!focusedComet.isEmpty())
421 //        KStarsLite::Instance()->map()->setFocusObject(
422 //            KStarsLite::Instance()->data()->objectNamed(focusedComet));
423 //#else
424 //    if (!focusedComet.isEmpty())
425 //        KStars::Instance()->map()->setFocusObject(
426 //            KStars::Instance()->data()->objectNamed(focusedComet));
427 //    KStars::Instance()->data()->setFullTimeUpdate();
428 //#endif
429 
430 //    downloadJob->deleteLater();
431 //}
432 
downloadError(const QString & errorString)433 void CometsComponent::downloadError(const QString &errorString)
434 {
435     KSNotification::error(i18n("Error downloading asteroids data: %1", errorString));
436     qCCritical(KSTARS) << errorString;
437     downloadJob->deleteLater();
438 }
439