1 /*
2 * Copyright (C) 2009, 2012 Matthew Gates
3 * Copyright (C) 2015 Nick Fedoseev (Iridium flares)
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18 */
19
20 #include "StelProjector.hpp"
21 #include "StelPainter.hpp"
22 #include "StelApp.hpp"
23 #include "StelCore.hpp"
24 #include "StelGui.hpp"
25 #include "StelGuiItems.hpp"
26 #include "StelLocation.hpp"
27 #include "StelObjectMgr.hpp"
28 #include "StelModuleMgr.hpp"
29 #include "StelLocaleMgr.hpp"
30 #include "StelFileMgr.hpp"
31 #include "StelTextureMgr.hpp"
32 #include "StelIniParser.hpp"
33 #include "Satellites.hpp"
34 #include "Satellite.hpp"
35 #include "SatellitesListModel.hpp"
36 #include "Planet.hpp"
37 #include "SolarSystem.hpp"
38 #include "StelJsonParser.hpp"
39 #include "SatellitesDialog.hpp"
40 #include "LabelMgr.hpp"
41 #include "StelTranslator.hpp"
42 #include "StelProgressController.hpp"
43 #include "StelUtils.hpp"
44 #include "StelActionMgr.hpp"
45
46 #include "external/qtcompress/qzipreader.h"
47
48 #include <QNetworkAccessManager>
49 #include <QNetworkReply>
50 #include <QKeyEvent>
51 #include <QDebug>
52 #include <QFileInfo>
53 #include <QFile>
54 #include <QTimer>
55 #include <QVariantMap>
56 #include <QVariant>
57 #include <QDir>
58 #include <QTemporaryFile>
59 #include <QRegularExpression>
60
getStelModule() const61 StelModule* SatellitesStelPluginInterface::getStelModule() const
62 {
63 return new Satellites();
64 }
65
getPluginInfo() const66 StelPluginInfo SatellitesStelPluginInterface::getPluginInfo() const
67 {
68 // Allow to load the resources when used as a static plugin
69 Q_INIT_RESOURCE(Satellites);
70
71 StelPluginInfo info;
72 info.id = "Satellites";
73 info.displayedName = N_("Satellites");
74 info.authors = "Matthew Gates, Jose Luis Canales";
75 info.contact = STELLARIUM_DEV_URL;
76 info.description = N_("Prediction of artificial satellite positions in Earth orbit based on NORAD TLE data");
77 info.version = SATELLITES_PLUGIN_VERSION;
78 info.license = SATELLITES_PLUGIN_LICENSE;
79 return info;
80 }
81
82 // WARNING! Update also the version number in resources/satellites.json,
83 // otherwise the local copy of that file will be overwritten every time
84 // Stellarium starts. (Less of a problem if it manages to get one update.)
85 QString Satellites::SatellitesCatalogVersion = "0.12.0";
86
Satellites()87 Satellites::Satellites()
88 : satelliteListModel(Q_NULLPTR)
89 , toolbarButton(Q_NULLPTR)
90 , earth(Q_NULLPTR)
91 , defaultHintColor(0.0f, 0.4f, 0.6f)
92 , updateState(CompleteNoUpdates)
93 , downloadMgr(Q_NULLPTR)
94 , progressBar(Q_NULLPTR)
95 , numberDownloadsComplete(0)
96 , updateTimer(Q_NULLPTR)
97 , updatesEnabled(false)
98 , autoAddEnabled(false)
99 , autoRemoveEnabled(false)
100 , updateFrequencyHours(0)
101 #if(SATELLITES_PLUGIN_IRIDIUM == 1)
102 , iridiumFlaresPredictionDepth(7)
103 #endif
104 {
105 setObjectName("Satellites");
106 configDialog = new SatellitesDialog();
107 }
108
deinit()109 void Satellites::deinit()
110 {
111 Satellite::hintTexture.clear();
112 texPointer.clear();
113 }
114
~Satellites()115 Satellites::~Satellites()
116 {
117 delete configDialog;
118 }
119
120
init()121 void Satellites::init()
122 {
123 QSettings* conf = StelApp::getInstance().getSettings();
124
125 try
126 {
127 // TODO: Compatibility with installation-dir modules? --BM
128 // It seems that the original code couldn't handle them either.
129 QString dirPath = StelFileMgr::getUserDir() + "/modules/Satellites";
130 // TODO: Ideally, this should return a QDir object
131 StelFileMgr::makeSureDirExistsAndIsWritable(dirPath);
132 dataDir.setPath(dirPath);
133
134 // load standard magnitudes and RCS data for satellites
135 loadExtraData();
136
137 // If no settings in the main config file, create with defaults
138 if (!conf->childGroups().contains("Satellites"))
139 {
140 //qDebug() << "Satellites: created section in config file.";
141 restoreDefaultSettings();
142 }
143
144 // populate settings from main config file.
145 loadSettings();
146
147 // absolute file name for inner catalogue of the satellites
148 catalogPath = dataDir.absoluteFilePath("satellites.json");
149
150 // Load and find resources used in the plugin
151 texPointer = StelApp::getInstance().getTextureManager().createTexture(StelFileMgr::getInstallationDir()+"/textures/pointeur5.png");
152 Satellite::hintTexture = StelApp::getInstance().getTextureManager().createTexture(":/satellites/hint.png");
153
154 // key bindings and other actions
155 QString satGroup = N_("Satellites");
156 addAction("actionShow_Satellite_Hints", satGroup, N_("Artificial satellites"), "flagHintsVisible", "Ctrl+Z");
157 addAction("actionShow_Satellite_Labels", satGroup, N_("Satellite labels"), "flagLabelsVisible", "Alt+Shift+Z");
158 addAction("actionShow_Satellite_ConfigDialog_Global", satGroup, N_("Show settings dialog"), configDialog, "visible", "Alt+Z");
159
160 // Gui toolbar button
161 StelGui* gui = dynamic_cast<StelGui*>(StelApp::getInstance().getGui());
162 if (gui!=Q_NULLPTR)
163 {
164 toolbarButton = new StelButton(Q_NULLPTR,
165 QPixmap(":/satellites/bt_satellites_on.png"),
166 QPixmap(":/satellites/bt_satellites_off.png"),
167 QPixmap(":/graphicGui/miscGlow32x32.png"),
168 "actionShow_Satellite_Hints",
169 false,
170 "actionShow_Satellite_ConfigDialog_Global");
171 gui->getButtonBar()->addButton(toolbarButton, "065-pluginsGroup");
172 }
173 }
174 catch (std::runtime_error &e)
175 {
176 qWarning() << "[Satellites] init error: " << e.what();
177 return;
178 }
179
180 // If the json file does not already exist, create it from the resource in the Qt resource
181 if(QFileInfo(catalogPath).exists())
182 {
183 if (!checkJsonFileFormat() || readCatalogVersion() != SatellitesCatalogVersion)
184 {
185 displayMessage(q_("The old satellites.json file is no longer compatible - using default file"), "#bb0000");
186 restoreDefaultCatalog();
187 }
188 }
189 else
190 {
191 qDebug() << "[Satellites] satellites.json does not exist - copying default file to " << QDir::toNativeSeparators(catalogPath);
192 restoreDefaultCatalog();
193 }
194
195 qDebug() << "[Satellites] loading catalogue file:" << QDir::toNativeSeparators(catalogPath);
196
197 // create satellites according to content of satellites.json file
198 loadCatalog();
199
200 // Set up download manager and the update schedule
201 downloadMgr = new QNetworkAccessManager(this);
202 connect(downloadMgr, SIGNAL(finished(QNetworkReply*)),
203 this, SLOT(saveDownloadedUpdate(QNetworkReply*)));
204 updateState = CompleteNoUpdates;
205 updateTimer = new QTimer(this);
206 updateTimer->setSingleShot(false); // recurring check for update
207 updateTimer->setInterval(13000); // check once every 13 seconds to see if it is time for an update
208 connect(updateTimer, SIGNAL(timeout()), this, SLOT(checkForUpdate()));
209 updateTimer->start();
210
211 earth = GETSTELMODULE(SolarSystem)->getEarth();
212 GETSTELMODULE(StelObjectMgr)->registerStelObjectMgr(this);
213
214 // Handle changes to the observer location or wide range of dates:
215 StelCore* core = StelApp::getInstance().getCore();
216 connect(core, SIGNAL(locationChanged(StelLocation)), this, SLOT(updateObserverLocation(StelLocation)));
217 connect(core, SIGNAL(configurationDataSaved()), this, SLOT(saveSettings()));
218 connect(&StelApp::getInstance(), SIGNAL(languageChanged()), this, SLOT(translateData()));
219
220 bindingGroups();
221 }
222
translateData()223 void Satellites::translateData()
224 {
225 bindingGroups();
226 for (const auto& sat : qAsConst(satellites))
227 {
228 if (sat->initialized)
229 sat->recomputeSatData();
230 }
231 }
232
updateSatellitesVisibility()233 void Satellites::updateSatellitesVisibility()
234 {
235 if (getFlagHintsVisible())
236 setFlagHintsVisible(false);
237 }
238
bindingGroups()239 void Satellites::bindingGroups()
240 {
241 StelActionMgr* actionMgr = StelApp::getInstance().getStelActionManager();
242 QStringList groups = getGroupIdList();
243 QString satGroup = N_("Satellites");
244 QString showSatGroup = q_("Show satellites from the group");
245 QString hideSatGroup = q_("Hide satellites from the group");
246 QStringList::const_iterator constIterator;
247 for (constIterator = groups.constBegin(); constIterator != groups.constEnd(); ++constIterator)
248 {
249 QString groupId = (*constIterator).toLocal8Bit().constData();
250 QString actionShowName = QString("actionShow_Satellite_Group_%1").arg(groupId);
251 QString actionShowDescription = QString("%1 \"%2\"").arg(showSatGroup, q_(groupId));
252 StelAction* actionShow = actionMgr->findAction(actionShowName);
253 if (actionShow!=Q_NULLPTR)
254 actionMgr->findAction(actionShowName)->setText(actionShowDescription);
255 else
256 addAction(actionShowName, satGroup, actionShowDescription, this, [=](){setSatGroupVisible(groupId, true);});
257
258 QString actionHideName = QString("actionHide_Satellite_Group_%1").arg(groupId);
259 QString actionHideDescription = QString("%1 \"%2\"").arg(hideSatGroup, q_(groupId));
260 StelAction* actionHide = actionMgr->findAction(actionHideName);
261 if (actionHide!=Q_NULLPTR)
262 actionMgr->findAction(actionHideName)->setText(actionHideDescription);
263 else
264 addAction(actionHideName, satGroup, actionHideDescription, this, [=](){setSatGroupVisible(groupId, false);});
265 }
266 }
267
setSatGroupVisible(const QString & groupId,bool visible)268 void Satellites::setSatGroupVisible(const QString& groupId, bool visible)
269 {
270 for (const auto& sat : qAsConst(satellites))
271 {
272 if (sat->initialized && sat->groups.contains(groupId))
273 {
274 SatFlags flags = sat->getFlags();
275 visible ? flags |= SatDisplayed : flags &= ~SatDisplayed;
276 sat->setFlags(flags);
277 }
278 }
279 emit satGroupVisibleChanged();
280 }
281
backupCatalog(bool deleteOriginal)282 bool Satellites::backupCatalog(bool deleteOriginal)
283 {
284 QFile old(catalogPath);
285 if (!old.exists())
286 {
287 qWarning() << "[Satellites] no file to backup";
288 return false;
289 }
290
291 QString backupPath = catalogPath + ".old";
292 if (QFileInfo(backupPath).exists())
293 QFile(backupPath).remove();
294
295 if (old.copy(backupPath))
296 {
297 if (deleteOriginal)
298 {
299 if (!old.remove())
300 {
301 qWarning() << "[Satellites] WARNING: unable to remove old catalogue file!";
302 return false;
303 }
304 }
305 }
306 else
307 {
308 qWarning() << "[Satellites] WARNING: failed to back up catalogue file as"
309 << QDir::toNativeSeparators(backupPath);
310 return false;
311 }
312
313 return true;
314 }
315
getCallOrder(StelModuleActionName actionName) const316 double Satellites::getCallOrder(StelModuleActionName actionName) const
317 {
318 if (actionName==StelModule::ActionDraw)
319 return StelApp::getInstance().getModuleMgr().getModule("SolarSystem")->getCallOrder(actionName)+1.;
320 return 0;
321 }
322
searchAround(const Vec3d & av,double limitFov,const StelCore * core) const323 QList<StelObjectP> Satellites::searchAround(const Vec3d& av, double limitFov, const StelCore* core) const
324 {
325 QList<StelObjectP> result;
326 if (!hintFader)
327 return result;
328
329 if (qAbs(core->getTimeRate())>=Satellite::timeRateLimit) // Do not show satellites when time rate is over limit
330 return result;
331
332 if (core->getCurrentPlanet()!=earth || !isValidRangeDates(core))
333 return result;
334
335 Vec3d v(av);
336 v.normalize();
337 double cosLimFov = cos(limitFov * M_PI/180.);
338 Vec3d equPos;
339
340 for (const auto& sat : satellites)
341 {
342 if (sat->initialized && sat->displayed)
343 {
344 equPos = sat->XYZ;
345 equPos.normalize();
346 if (equPos[0]*v[0] + equPos[1]*v[1] + equPos[2]*v[2]>=cosLimFov)
347 {
348 result.append(qSharedPointerCast<StelObject>(sat));
349 }
350 }
351 }
352 return result;
353 }
354
searchByNameI18n(const QString & nameI18n) const355 StelObjectP Satellites::searchByNameI18n(const QString& nameI18n) const
356 {
357 if (!hintFader)
358 return Q_NULLPTR;
359
360 StelCore* core = StelApp::getInstance().getCore();
361
362 if (qAbs(core->getTimeRate())>=Satellite::timeRateLimit) // Do not show satellites when time rate is over limit
363 return Q_NULLPTR;
364
365 if (core->getCurrentPlanet()!=earth || !isValidRangeDates(core))
366 return Q_NULLPTR;
367
368 QString objw = nameI18n.toUpper();
369
370 StelObjectP result = searchByNoradNumber(objw);
371 if (result)
372 return result;
373
374 for (const auto& sat : satellites)
375 {
376 if (sat->initialized && sat->displayed)
377 {
378 if (sat->getNameI18n().toUpper() == objw)
379 return qSharedPointerCast<StelObject>(sat);
380 }
381 }
382
383 return Q_NULLPTR;
384 }
385
searchByName(const QString & englishName) const386 StelObjectP Satellites::searchByName(const QString& englishName) const
387 {
388 if (!hintFader)
389 return Q_NULLPTR;
390
391 StelCore* core = StelApp::getInstance().getCore();
392
393 if (qAbs(core->getTimeRate())>=Satellite::timeRateLimit) // Do not show satellites when time rate is over limit
394 return Q_NULLPTR;
395
396 if (core->getCurrentPlanet()!=earth || !isValidRangeDates(core))
397 return Q_NULLPTR;
398
399 QString objw = englishName.toUpper();
400
401 StelObjectP result = searchByNoradNumber(objw);
402 if (result)
403 return result;
404
405 for (const auto& sat : satellites)
406 {
407 if (sat->initialized && sat->displayed)
408 {
409 if (sat->getEnglishName().toUpper() == objw)
410 return qSharedPointerCast<StelObject>(sat);
411 }
412 }
413
414 return Q_NULLPTR;
415 }
416
searchByID(const QString & id) const417 StelObjectP Satellites::searchByID(const QString &id) const
418 {
419 for (const auto& sat : satellites)
420 {
421 if (sat->initialized && sat->getID() == id)
422 {
423 return qSharedPointerCast<StelObject>(sat);
424 }
425 }
426
427 return Q_NULLPTR;
428 }
429
searchByNoradNumber(const QString & noradNumber) const430 StelObjectP Satellites::searchByNoradNumber(const QString &noradNumber) const
431 {
432 if (!hintFader)
433 return Q_NULLPTR;
434
435 StelCore* core = StelApp::getInstance().getCore();
436
437 if (qAbs(core->getTimeRate())>=Satellite::timeRateLimit) // Do not show satellites when time rate is over limit
438 return Q_NULLPTR;
439
440 if (core->getCurrentPlanet()!=earth || !isValidRangeDates(core))
441 return Q_NULLPTR;
442
443 // If the search string is a catalogue number...
444 QRegularExpression regExp("^(NORAD)\\s*(\\d+)\\s*$");
445 QRegularExpressionMatch match=regExp.match(noradNumber);
446 if (match.hasMatch())
447 {
448 QString numberString = match.captured(2);
449
450 for (const auto& sat : satellites)
451 {
452 if (sat->initialized && sat->displayed)
453 {
454 if (sat->getCatalogNumberString() == numberString)
455 return qSharedPointerCast<StelObject>(sat);
456 }
457 }
458 }
459
460 return StelObjectP();
461 }
462
listMatchingObjects(const QString & objPrefix,int maxNbItem,bool useStartOfWords) const463 QStringList Satellites::listMatchingObjects(const QString& objPrefix, int maxNbItem, bool useStartOfWords) const
464 {
465 QStringList result;
466 if (!hintFader || maxNbItem <= 0)
467 return result;
468
469 StelCore* core = StelApp::getInstance().getCore();
470
471 if (qAbs(core->getTimeRate())>=Satellite::timeRateLimit) // Do not show satellites when time rate is over limit
472 return result;
473
474 if (core->getCurrentPlanet()!=earth || !isValidRangeDates(core))
475 return result;
476
477 QString objw = objPrefix.toUpper();
478
479 QString numberPrefix;
480 QRegularExpression regExp("^(NORAD)\\s*(\\d+)\\s*$");
481 QRegularExpressionMatch match=regExp.match(objw);
482 if (match.hasMatch())
483 {
484 QString numberString = match.captured(2);
485 bool ok;
486 /* int number = */ numberString.toInt(&ok);
487 if (ok)
488 numberPrefix = numberString;
489 }
490
491 QStringList names;
492 for (const auto& sobj : satellites)
493 {
494 if (!sobj->initialized || !sobj->displayed)
495 continue;
496
497 names.append(sobj->getNameI18n());
498 names.append(sobj->getEnglishName());
499 if (!numberPrefix.isEmpty() && sobj->getCatalogNumberString().startsWith(numberPrefix))
500 names.append(QString("NORAD %1").arg(sobj->getCatalogNumberString()));
501 }
502
503 QString fullMatch = "";
504 for (const auto& name : qAsConst(names))
505 {
506 if (!matchObjectName(name, objPrefix, useStartOfWords))
507 continue;
508
509 if (name==objPrefix)
510 fullMatch = name;
511 else
512 result.append(name);
513
514 if (result.size() >= maxNbItem)
515 break;
516 }
517
518 result.sort();
519 if (!fullMatch.isEmpty())
520 result.prepend(fullMatch);
521
522 return result;
523 }
524
listAllObjects(bool inEnglish) const525 QStringList Satellites::listAllObjects(bool inEnglish) const
526 {
527 QStringList result;
528
529 if (!hintFader)
530 return result;
531
532 StelCore* core = StelApp::getInstance().getCore();
533
534 if (qAbs(core->getTimeRate())>=Satellite::timeRateLimit) // Do not show satellites when time rate is over limit
535 return result;
536
537 if (core->getCurrentPlanet()!=earth || !isValidRangeDates(core))
538 return result;
539
540 for (const auto& sat : satellites)
541 {
542 if (inEnglish)
543 result << sat->getEnglishName();
544 else
545 result << sat->getNameI18n();
546 }
547 return result;
548 }
549
configureGui(bool show)550 bool Satellites::configureGui(bool show)
551 {
552 if (show)
553 configDialog->setVisible(true);
554 return true;
555 }
556
restoreDefaults(void)557 void Satellites::restoreDefaults(void)
558 {
559 restoreDefaultSettings();
560 restoreDefaultCatalog();
561 loadCatalog();
562 loadSettings();
563 }
564
restoreDefaultSettings()565 void Satellites::restoreDefaultSettings()
566 {
567 QSettings* conf = StelApp::getInstance().getSettings();
568 conf->beginGroup("Satellites");
569
570 // delete all existing Satellite settings...
571 conf->remove("");
572
573 conf->setValue("show_satellite_hints", true);
574 conf->setValue("show_satellite_labels", false);
575 conf->setValue("updates_enabled", true);
576 conf->setValue("auto_add_enabled", true);
577 conf->setValue("auto_remove_enabled", true);
578 conf->setValue("hint_color", "0.0,0.4,0.6");
579 conf->setValue("invisible_satellite_color", "0.2,0.2,0.2");
580 conf->setValue("transit_satellite_color", "0.0,0.0,0.0");
581 conf->setValue("hint_font_size", 10);
582 conf->setValue("update_frequency_hours", 72);
583 conf->setValue("orbit_line_flag", false);
584 conf->setValue("orbit_line_segments", 90);
585 conf->setValue("orbit_fade_segments", 5);
586 conf->setValue("orbit_segment_duration", 20);
587 conf->setValue("valid_epoch_age", 30);
588 conf->setValue("iconic_mode_enabled", false);
589
590 conf->endGroup(); // saveTleSources() opens it for itself
591
592 // TLE update sources
593 QStringList urls;
594 urls << "1,http://www.celestrak.com/NORAD/elements/visual.txt" // Auto-add ON!
595 << "http://www.celestrak.com/NORAD/elements/tle-new.txt"
596 << "1,http://www.celestrak.com/NORAD/elements/science.txt"
597 << "http://www.celestrak.com/NORAD/elements/noaa.txt"
598 << "http://www.celestrak.com/NORAD/elements/goes.txt"
599 << "1,http://www.celestrak.com/NORAD/elements/amateur.txt"
600 << "1,http://www.celestrak.com/NORAD/elements/gps-ops.txt"
601 << "1,http://www.celestrak.com/NORAD/elements/galileo.txt"
602 << "http://www.celestrak.com/NORAD/elements/iridium.txt"
603 << "http://www.celestrak.com/NORAD/elements/iridium-NEXT.txt"
604 << "http://www.celestrak.com/NORAD/elements/geo.txt"
605 << "1,http://www.celestrak.com/NORAD/elements/stations.txt"
606 << "http://www.celestrak.com/NORAD/elements/weather.txt"
607 << "http://www.celestrak.com/NORAD/elements/resource.txt"
608 << "http://www.celestrak.com/NORAD/elements/sarsat.txt"
609 << "http://www.celestrak.com/NORAD/elements/dmc.txt"
610 << "http://www.celestrak.com/NORAD/elements/tdrss.txt"
611 << "http://www.celestrak.com/NORAD/elements/argos.txt"
612 << "http://www.celestrak.com/NORAD/elements/intelsat.txt"
613 << "http://www.celestrak.com/NORAD/elements/gorizont.txt"
614 << "http://www.celestrak.com/NORAD/elements/raduga.txt"
615 << "http://www.celestrak.com/NORAD/elements/molniya.txt"
616 << "http://www.celestrak.com/NORAD/elements/orbcomm.txt"
617 << "http://www.celestrak.com/NORAD/elements/globalstar.txt"
618 << "http://www.celestrak.com/NORAD/elements/x-comm.txt"
619 << "http://www.celestrak.com/NORAD/elements/other-comm.txt"
620 << "1,http://www.celestrak.com/NORAD/elements/glo-ops.txt"
621 << "1,http://www.celestrak.com/NORAD/elements/beidou.txt"
622 << "http://www.celestrak.com/NORAD/elements/sbas.txt"
623 << "http://www.celestrak.com/NORAD/elements/nnss.txt"
624 << "http://www.celestrak.com/NORAD/elements/engineering.txt"
625 << "http://www.celestrak.com/NORAD/elements/education.txt"
626 << "http://www.celestrak.com/NORAD/elements/geodetic.txt"
627 << "http://www.celestrak.com/NORAD/elements/radar.txt"
628 << "http://www.celestrak.com/NORAD/elements/cubesat.txt"
629 << "http://www.celestrak.com/NORAD/elements/other.txt"
630 << "1,http://www.celestrak.com/NORAD/elements/supplemental/starlink.txt"
631 << "https://www.amsat.org/amsat/ftp/keps/current/nasabare.txt"
632 << "http://www.celestrak.com/NORAD/elements/oneweb.txt"
633 << "http://www.celestrak.com/NORAD/elements/planet.txt"
634 << "http://www.celestrak.com/NORAD/elements/spire.txt"
635 << "1,https://www.prismnet.com/~mmccants/tles/classfd.zip";
636
637 saveTleSources(urls);
638 }
639
restoreDefaultCatalog()640 void Satellites::restoreDefaultCatalog()
641 {
642 if (QFileInfo(catalogPath).exists())
643 backupCatalog(true);
644
645 QFile src(":/satellites/satellites.json");
646 if (!src.copy(catalogPath))
647 {
648 qWarning() << "[Satellites] cannot copy json resource to " + QDir::toNativeSeparators(catalogPath);
649 }
650 else
651 {
652 qDebug() << "[Satellites] copied default satellites.json to " << QDir::toNativeSeparators(catalogPath);
653 // The resource is read only, and the new file inherits this... make sure the new file
654 // is writeable by the Stellarium process so that updates can be done.
655 QFile dest(catalogPath);
656 dest.setPermissions(dest.permissions() | QFile::WriteOwner);
657
658 // Make sure that in the case where an online update has previously been done, but
659 // the json file has been manually removed, that an update is scheduled in a timely
660 // manner
661 StelApp::getInstance().getSettings()->remove("Satellites/last_update");
662 lastUpdate = QDateTime::fromString("2015-05-01T12:00:00", Qt::ISODate);
663 }
664 }
665
loadSettings()666 void Satellites::loadSettings()
667 {
668 QSettings* conf = StelApp::getInstance().getSettings();
669 conf->beginGroup("Satellites");
670
671 // Load update sources list...
672 updateUrls.clear();
673
674 // Backward compatibility: try to detect and read an old-style array.
675 // TODO: Assume that the user hasn't modified their conf in a stupid way?
676 // if (conf->contains("tle_url0")) // This can skip some operations...
677 QRegularExpression keyRE("^tle_url\\d+$");
678 QStringList urls;
679 for (const auto& key : conf->childKeys())
680 {
681 if (keyRE.match(key).hasMatch())
682 {
683 QString url = conf->value(key).toString();
684 conf->remove(key); // Delete old-style keys
685 if (url.isEmpty())
686 continue;
687 // NOTE: This URL is also hard-coded in restoreDefaultSettings().
688 if (url == "http://celestrak.com/NORAD/elements/visual.txt")
689 url.prepend("1,"); // Same as in the new default configuration
690 urls << url;
691 }
692 }
693 // If any have been read, save them in the new format.
694 if (!urls.isEmpty())
695 {
696 conf->endGroup();
697 setTleSources(urls);
698 conf->beginGroup("Satellites");
699 }
700 else
701 {
702 int size = conf->beginReadArray("tle_sources");
703 for (int i = 0; i < size; i++)
704 {
705 conf->setArrayIndex(i);
706 QString url = conf->value("url").toString();
707 if (!url.isEmpty())
708 {
709 if (conf->value("add_new").toBool())
710 url.prepend("1,");
711 updateUrls.append(url);
712 }
713 }
714 conf->endArray();
715 }
716
717 // NOTE: Providing default values AND using restoreDefaultSettings() to create the section seems redundant. --BM
718
719 // updater related settings...
720 updateFrequencyHours = conf->value("update_frequency_hours", 72).toInt();
721 // last update default is the first Towel Day. <3 DA
722 lastUpdate = QDateTime::fromString(conf->value("last_update", "2001-05-25T12:00:00").toString(), Qt::ISODate);
723 setFlagHintsVisible(conf->value("show_satellite_hints", true).toBool());
724 Satellite::showLabels = conf->value("show_satellite_labels", false).toBool();
725 updatesEnabled = conf->value("updates_enabled", true).toBool();
726 autoAddEnabled = conf->value("auto_add_enabled", true).toBool();
727 autoRemoveEnabled = conf->value("auto_remove_enabled", true).toBool();
728 #if(SATELLITES_PLUGIN_IRIDIUM == 1)
729 iridiumFlaresPredictionDepth = conf->value("flares_prediction_depth", 7).toInt();
730 #endif
731
732 // Get a font for labels
733 labelFont.setPixelSize(conf->value("hint_font_size", 10).toInt());
734
735 // orbit drawing params
736 Satellite::orbitLinesFlag = conf->value("orbit_line_flag", false).toBool();
737 Satellite::orbitLineSegments = conf->value("orbit_line_segments", 180).toInt();
738 Satellite::orbitLineFadeSegments = conf->value("orbit_fade_segments", 5).toInt();
739 Satellite::orbitLineSegmentDuration = conf->value("orbit_segment_duration", 5).toInt();
740 setInvisibleSatelliteColor(Vec3f(conf->value("invisible_satellite_color", "0.2,0.2,0.2").toString()));
741 setTransitSatelliteColor(Vec3f(conf->value("transit_satellite_color", "0.0,0.0,0.0").toString()));
742 Satellite::timeRateLimit = conf->value("time_rate_limit", 1.0).toDouble();
743 Satellite::tleEpochAge = conf->value("valid_epoch_age", 30).toInt();
744
745 // iconic mode
746 setFlagIconicMode(conf->value("iconic_mode_enabled", false).toBool());
747 setFlagHideInvisible(conf->value("hide_invisible_satellites", false).toBool());
748
749 conf->endGroup();
750 }
751
saveSettingsToConfig()752 void Satellites::saveSettingsToConfig()
753 {
754 QSettings* conf = StelApp::getInstance().getSettings();
755 conf->beginGroup("Satellites");
756
757 // updater related settings...
758 conf->setValue("update_frequency_hours", updateFrequencyHours);
759 conf->setValue("show_satellite_hints", getFlagHintsVisible());
760 conf->setValue("show_satellite_labels", Satellite::showLabels);
761 conf->setValue("updates_enabled", updatesEnabled );
762 conf->setValue("auto_add_enabled", autoAddEnabled);
763 conf->setValue("auto_remove_enabled", autoRemoveEnabled);
764 #if(SATELLITES_PLUGIN_IRIDIUM == 1)
765 conf->setValue("flares_prediction_depth", iridiumFlaresPredictionDepth);
766 #endif
767
768 // Get a font for labels
769 conf->setValue("hint_font_size", labelFont.pixelSize());
770
771 // orbit drawing params
772 conf->setValue("orbit_line_flag", Satellite::orbitLinesFlag);
773 conf->setValue("orbit_line_segments", Satellite::orbitLineSegments);
774 conf->setValue("orbit_fade_segments", Satellite::orbitLineFadeSegments);
775 conf->setValue("orbit_segment_duration", Satellite::orbitLineSegmentDuration);
776
777 conf->setValue("valid_epoch_age", Satellite::tleEpochAge);
778
779 // iconic mode
780 conf->setValue("iconic_mode_enabled", getFlagIconicMode());
781 conf->setValue("hide_invisible_satellites", getFlagHideInvisible());
782
783 conf->endGroup();
784
785 // Update sources...
786 saveTleSources(updateUrls);
787 }
788
getInvisibleSatelliteColor() const789 Vec3f Satellites::getInvisibleSatelliteColor() const
790 {
791 return Satellite::invisibleSatelliteColor;
792 }
793
setInvisibleSatelliteColor(const Vec3f & c)794 void Satellites::setInvisibleSatelliteColor(const Vec3f &c)
795 {
796 Satellite::invisibleSatelliteColor = c;
797 emit invisibleSatelliteColorChanged(c);
798 }
799
getTransitSatelliteColor() const800 Vec3f Satellites::getTransitSatelliteColor() const
801 {
802 return Satellite::transitSatelliteColor;
803 }
804
setTransitSatelliteColor(const Vec3f & c)805 void Satellites::setTransitSatelliteColor(const Vec3f &c)
806 {
807 Satellite::transitSatelliteColor = c;
808 emit transitSatelliteColorChanged(c);
809 }
810
loadCatalog()811 void Satellites::loadCatalog()
812 {
813 setDataMap(loadDataMap());
814 }
815
readCatalogVersion()816 const QString Satellites::readCatalogVersion()
817 {
818 QString jsonVersion("unknown");
819 QFile satelliteJsonFile(catalogPath);
820 if (!satelliteJsonFile.open(QIODevice::ReadOnly))
821 {
822 qWarning() << "[Satellites] cannot open " << QDir::toNativeSeparators(catalogPath);
823 return jsonVersion;
824 }
825
826 QVariantMap map;
827 try
828 {
829 map = StelJsonParser::parse(&satelliteJsonFile).toMap();
830 satelliteJsonFile.close();
831 }
832 catch (std::runtime_error &e)
833 {
834 qDebug() << "[Satellites] File format is wrong! Error: " << e.what();
835 return jsonVersion;
836 }
837
838 if (map.contains("version"))
839 {
840 QString version = map.value("version").toString();
841 QRegularExpression vRx("(\\d+\\.\\d+\\.\\d+)");
842 QRegularExpressionMatch match=vRx.match(version);
843 if (match.hasMatch())
844 jsonVersion = match.captured(1);
845 }
846 else if (map.contains("creator"))
847 {
848 QString creator = map.value("creator").toString();
849 QRegularExpression vRx(".*(\\d+\\.\\d+\\.\\d+).*");
850 QRegularExpressionMatch match=vRx.match(creator);
851 if (match.hasMatch())
852 jsonVersion = match.captured(1);
853 }
854
855 //qDebug() << "[Satellites] catalogue version from file:" << jsonVersion;
856 return jsonVersion;
857 }
858
saveDataMap(const QVariantMap & map,QString path)859 bool Satellites::saveDataMap(const QVariantMap& map, QString path)
860 {
861 if (path.isEmpty())
862 path = catalogPath;
863
864 QFile jsonFile(path);
865
866 if (jsonFile.exists())
867 jsonFile.remove();
868
869 if (!jsonFile.open(QIODevice::WriteOnly))
870 {
871 qWarning() << "[Satellites] cannot open for writing:" << QDir::toNativeSeparators(path);
872 return false;
873 }
874 else
875 {
876 //qDebug() << "[Satellites] writing to:" << QDir::toNativeSeparators(path);
877 StelJsonParser::write(map, &jsonFile);
878 jsonFile.close();
879 return true;
880 }
881 }
882
loadDataMap(QString path)883 QVariantMap Satellites::loadDataMap(QString path)
884 {
885 if (path.isEmpty())
886 path = catalogPath;
887
888 QVariantMap map;
889 QFile jsonFile(path);
890 if (!jsonFile.open(QIODevice::ReadOnly))
891 qWarning() << "[Satellites] cannot open " << QDir::toNativeSeparators(path);
892 else
893 {
894 try
895 {
896 map = StelJsonParser::parse(&jsonFile).toMap();
897 jsonFile.close();
898 }
899 catch (std::runtime_error &e)
900 {
901 qDebug() << "[Satellites] File format is wrong! Error: " << e.what();
902 return QVariantMap();
903 }
904 }
905 return map;
906 }
907
setDataMap(const QVariantMap & map)908 void Satellites::setDataMap(const QVariantMap& map)
909 {
910 QVariantList defaultHintColorMap;
911 defaultHintColorMap << defaultHintColor[0] << defaultHintColor[1] << defaultHintColor[2];
912
913 if (map.contains("hintColor"))
914 {
915 defaultHintColorMap = map.value("hintColor").toList();
916 defaultHintColor.set(defaultHintColorMap.at(0).toFloat(), defaultHintColorMap.at(1).toFloat(), defaultHintColorMap.at(2).toFloat());
917 }
918
919 if (satelliteListModel)
920 satelliteListModel->beginSatellitesChange();
921
922 satellites.clear();
923 groups.clear();
924 QVariantMap satMap = map.value("satellites").toMap();
925 for (const auto& satId : satMap.keys())
926 {
927 QVariantMap satData = satMap.value(satId).toMap();
928
929 if (!satData.contains("hintColor"))
930 satData["hintColor"] = defaultHintColorMap;
931
932 if (!satData.contains("orbitColor"))
933 satData["orbitColor"] = satData["hintColor"];
934
935 if (!satData.contains("infoColor"))
936 satData["infoColor"] = satData["hintColor"];
937
938 int sid = satId.toInt();
939 if (!satData.contains("stdMag") && qsMagList.contains(sid))
940 satData["stdMag"] = qsMagList[sid];
941
942 if (!satData.contains("rcs") && rcsList.contains(sid))
943 satData["rcs"] = rcsList[sid];
944
945 SatelliteP sat(new Satellite(satId, satData));
946 if (sat->initialized)
947 {
948 satellites.append(sat);
949 groups.unite(sat->groups);
950 }
951 }
952 std::sort(satellites.begin(), satellites.end());
953
954 if (satelliteListModel)
955 satelliteListModel->endSatellitesChange();
956 }
957
createDataMap(void)958 QVariantMap Satellites::createDataMap(void)
959 {
960 QVariantMap map;
961 QVariantList defHintCol;
962 defHintCol << Satellite::roundToDp(defaultHintColor[0],3)
963 << Satellite::roundToDp(defaultHintColor[1],3)
964 << Satellite::roundToDp(defaultHintColor[2],3);
965
966 // TODO: Since v0.21 uncomment this line:
967 // map["creator"] = QString("Satellites plugin version %1").arg(SATELLITES_PLUGIN_VERSION);
968 // and remove this line:
969 map["creator"] = QString("Satellites plugin version %1").arg(SatellitesCatalogVersion);
970 map["version"] = QString("%1").arg(SatellitesCatalogVersion);
971 map["hintColor"] = defHintCol;
972 map["shortName"] = "satellite orbital data";
973 QVariantMap sats;
974 for (const auto& sat : qAsConst(satellites))
975 {
976 QVariantMap satMap = sat->getMap();
977
978 if (satMap["orbitColor"] == satMap["hintColor"])
979 satMap.remove("orbitColor");
980
981 if (satMap["hintColor"].toList() == defHintCol)
982 satMap.remove("hintColor");
983
984 if (satMap["infoColor"].toList() == defHintCol)
985 satMap.remove("infoColor");
986
987 if (satMap["stdMag"].toFloat() > 98.f)
988 satMap.remove("stdMag");
989
990 if (satMap["rcs"].toFloat() < 0.f)
991 satMap.remove("rcs");
992
993 if (satMap["status"].toInt() == Satellite::StatusUnknown)
994 satMap.remove("status");
995
996 sats[sat->id] = satMap;
997 }
998 map["satellites"] = sats;
999 return map;
1000 }
1001
markLastUpdate()1002 void Satellites::markLastUpdate()
1003 {
1004 lastUpdate = QDateTime::currentDateTime();
1005 QSettings* conf = StelApp::getInstance().getSettings();
1006 conf->setValue("Satellites/last_update", lastUpdate.toString(Qt::ISODate));
1007 }
1008
getGroups() const1009 QSet<QString> Satellites::getGroups() const
1010 {
1011 return groups;
1012 }
1013
getGroupIdList() const1014 QStringList Satellites::getGroupIdList() const
1015 {
1016 QStringList groupList(groups.values());
1017 groupList.sort();
1018 return groupList;
1019 }
1020
addGroup(const QString & groupId)1021 void Satellites::addGroup(const QString& groupId)
1022 {
1023 if (groupId.isEmpty())
1024 return;
1025 groups.insert(groupId);
1026 }
1027
getSatellites(const QString & group,Status vis) const1028 QHash<QString,QString> Satellites::getSatellites(const QString& group, Status vis) const
1029 {
1030 QHash<QString,QString> result;
1031
1032 for (const auto& sat : satellites)
1033 {
1034 if (sat->initialized)
1035 {
1036 if ((group.isEmpty() || sat->groups.contains(group)) && ! result.contains(sat->id))
1037 {
1038 if (vis==Both ||
1039 (vis==Visible && sat->displayed) ||
1040 (vis==NotVisible && !sat->displayed) ||
1041 (vis==OrbitError && !sat->orbitValid) ||
1042 (vis==NewlyAdded && sat->isNew()))
1043 result.insert(sat->id, sat->name);
1044 }
1045 }
1046 }
1047 return result;
1048 }
1049
getSatellitesListModel()1050 SatellitesListModel* Satellites::getSatellitesListModel()
1051 {
1052 if (!satelliteListModel)
1053 satelliteListModel = new SatellitesListModel(&satellites, this);
1054 return satelliteListModel;
1055 }
1056
getById(const QString & id) const1057 SatelliteP Satellites::getById(const QString& id) const
1058 {
1059 for (const auto& sat : satellites)
1060 {
1061 if (sat->initialized && sat->id == id)
1062 return sat;
1063 }
1064 return SatelliteP();
1065 }
1066
listAllIds() const1067 QStringList Satellites::listAllIds() const
1068 {
1069 QStringList result;
1070 for (const auto& sat : satellites)
1071 {
1072 if (sat->initialized)
1073 result.append(sat->id);
1074 }
1075 return result;
1076 }
1077
add(const TleData & tleData)1078 bool Satellites::add(const TleData& tleData)
1079 {
1080 // More validation?
1081 if (tleData.id.isEmpty() || tleData.name.isEmpty() || tleData.first.isEmpty() || tleData.second.isEmpty())
1082 return false;
1083
1084 // Duplicates check
1085 if (searchByID(getSatIdFromLine2(tleData.second.trimmed()))!=Q_NULLPTR)
1086 return false;
1087
1088 QVariantList hintColor;
1089 hintColor << defaultHintColor[0] << defaultHintColor[1] << defaultHintColor[2];
1090
1091 QVariantMap satProperties;
1092 satProperties.insert("name", tleData.name);
1093 satProperties.insert("tle1", tleData.first);
1094 satProperties.insert("tle2", tleData.second);
1095 satProperties.insert("hintColor", hintColor);
1096 //TODO: Decide if newly added satellites are visible by default --BM
1097 satProperties.insert("visible", true);
1098 satProperties.insert("orbitVisible", false);
1099 int sid = tleData.id.toInt();
1100 if (qsMagList.contains(sid))
1101 satProperties.insert("stdMag", qsMagList[sid]);
1102 if (rcsList.contains(sid))
1103 satProperties.insert("rcs", rcsList[sid]);
1104 // special case: starlink satellites; details: http://satobs.org/seesat/Apr-2020/0174.html
1105 if (!rcsList.contains(sid) && tleData.name.startsWith("STARLINK"))
1106 satProperties.insert("rcs", 22.68); // Starlink's solar array is 8.1 x 2.8 metres.
1107 if (tleData.status != Satellite::StatusUnknown)
1108 satProperties.insert("status", tleData.status);
1109 // Guess the group
1110 QVariantList groupList = satProperties.value("groups", QVariantList()).toList();
1111 QStringList satGroups;
1112 if (groupList.isEmpty())
1113 {
1114 if (tleData.name.startsWith("STARLINK"))
1115 {
1116 satGroups.append("starlink");
1117 satGroups.append("communications");
1118 }
1119 if (tleData.name.startsWith("IRIDIUM"))
1120 {
1121 QStringList d = tleData.name.split(" ");
1122 if (d.at(1).toInt()>=100)
1123 satGroups.append("iridium next");
1124 else
1125 satGroups.append("iridium");
1126 satGroups.append("communications");
1127 }
1128 if (tleData.name.startsWith("FLOCK") || tleData.name.startsWith("SKYSAT"))
1129 satGroups.append("earth resources");
1130 if (tleData.name.startsWith("ONEWEB"))
1131 {
1132 satGroups.append("oneweb");
1133 satGroups.append("communications");
1134 }
1135 if (tleData.name.startsWith("LEMUR"))
1136 {
1137 satGroups.append("spire");
1138 satGroups.append("earth resources");
1139 }
1140 if (tleData.name.startsWith("GPS"))
1141 {
1142 satGroups.append("gps");
1143 satGroups.append("navigation");
1144 }
1145 if (tleData.name.startsWith("IRNSS"))
1146 {
1147 satGroups.append("irnss");
1148 satGroups.append("navigation");
1149 }
1150 if (tleData.name.startsWith("QZS"))
1151 {
1152 satGroups.append("qzss");
1153 }
1154 if (tleData.name.startsWith("TDRS"))
1155 {
1156 satGroups.append("tdrss");
1157 satGroups.append("communications");
1158 satGroups.append("geostationary");
1159 }
1160 if (tleData.name.startsWith("BEIDOU"))
1161 {
1162 satGroups.append("beidou");
1163 satGroups.append("navigation");
1164 }
1165 if (tleData.name.startsWith("COSMOS"))
1166 {
1167 satGroups.append("cosmos");
1168 if (tleData.name.contains("("))
1169 {
1170 satGroups.append("glonass");
1171 satGroups.append("navigation");
1172 }
1173 }
1174 if (tleData.name.startsWith("GSAT") && (tleData.name.contains("PRN") || tleData.name.contains("GALILEO")))
1175 {
1176 satGroups.append("galileo");
1177 satGroups.append("navigation");
1178 }
1179 if (tleData.name.startsWith("INTELSAT") || tleData.name.startsWith("GLOBALSTAR") || tleData.name.startsWith("ORBCOMM") || tleData.name.startsWith("GORIZONT") || tleData.name.startsWith("RADUGA") || tleData.name.startsWith("MOLNIYA") || tleData.name.startsWith("DIRECTV") || tleData.name.startsWith("CHINASAT") || tleData.name.startsWith("YAMAL"))
1180 {
1181 QString satName = tleData.name.split(" ").at(0).toLower();
1182 if (satName.contains("-"))
1183 satName = satName.split("-").at(0);
1184 satGroups.append(satName);
1185 satGroups.append("communications");
1186 if (satName.startsWith("INTELSAT") || satName.startsWith("RADUGA") || satName.startsWith("GORIZONT") || satName.startsWith("DIRECTV") || satName.startsWith("CHINASAT") || satName.startsWith("YAMAL"))
1187 satGroups.append("geostationary");
1188 if (satName.startsWith("INTELSAT") || satName.startsWith("DIRECTV") || satName.startsWith("YAMAL"))
1189 satGroups.append("tv");
1190 }
1191 if (tleData.name.contains(" DEB"))
1192 satGroups.append("debris");
1193 if (tleData.name.startsWith("SOYUZ-MS"))
1194 satGroups.append("crewed");
1195 if (tleData.name.startsWith("PROGRESS-MS") || tleData.name.startsWith("CYGNUS NG"))
1196 satGroups.append("resupply");
1197 if (tleData.status==Satellite::StatusNonoperational)
1198 satGroups.append("non-operational");
1199 }
1200 if (tleData.sourceURL.contains("celestrak.com", Qt::CaseInsensitive))
1201 {
1202 // add groups, based on Celestrak's groups
1203 QString fileName = QUrl(tleData.sourceURL).fileName().toLower().replace(".txt", "");
1204 if (!satGroups.contains(fileName))
1205 satGroups.append(fileName);
1206 }
1207 if (!satGroups.isEmpty())
1208 {
1209 satProperties.insert("groups", satGroups);
1210 for (const auto& str : qAsConst(satGroups))
1211 {
1212 if (!getGroupIdList().contains(str))
1213 addGroup(str);
1214 }
1215 }
1216
1217 SatelliteP sat(new Satellite(tleData.id, satProperties));
1218 if (sat->initialized)
1219 {
1220 qDebug() << "[Satellites] satellite added:" << tleData.id << tleData.name;
1221 satellites.append(sat);
1222 sat->setNew();
1223 return true;
1224 }
1225 return false;
1226 }
1227
add(const TleDataList & newSatellites)1228 void Satellites::add(const TleDataList& newSatellites)
1229 {
1230 if (satelliteListModel)
1231 satelliteListModel->beginSatellitesChange();
1232
1233 int numAdded = 0;
1234 for (const auto& tleSet : newSatellites)
1235 {
1236 if (add(tleSet))
1237 {
1238 numAdded++;
1239 }
1240 }
1241 if (numAdded > 0)
1242 std::sort(satellites.begin(), satellites.end());
1243
1244 if (satelliteListModel)
1245 satelliteListModel->endSatellitesChange();
1246
1247 qDebug() << "[Satellites] "
1248 << newSatellites.count() << "satellites proposed for addition, "
1249 << numAdded << " added, "
1250 << satellites.count() << " total after the operation.";
1251 }
1252
remove(const QStringList & idList)1253 void Satellites::remove(const QStringList& idList)
1254 {
1255 if (satelliteListModel)
1256 satelliteListModel->beginSatellitesChange();
1257
1258 StelObjectMgr* objMgr = GETSTELMODULE(StelObjectMgr);
1259 int numRemoved = 0;
1260 for (int i = 0; i < satellites.size(); i++)
1261 {
1262 const SatelliteP& sat = satellites.at(i);
1263 if (idList.contains(sat->id))
1264 {
1265 QList<StelObjectP> selected = objMgr->getSelectedObject("Satellite");
1266 if (selected.contains(sat.staticCast<StelObject>()))
1267 objMgr->unSelect();
1268
1269 qDebug() << "Satellite removed:" << sat->id << sat->name;
1270 satellites.removeAt(i);
1271 i--; //Compensate for the change in the array's indexing
1272 numRemoved++;
1273 }
1274 }
1275 // As the satellite list is kept sorted, no need for re-sorting.
1276
1277 if (satelliteListModel)
1278 satelliteListModel->endSatellitesChange();
1279
1280 qDebug() << "[Satellites] "
1281 << idList.count() << "satellites proposed for removal, "
1282 << numRemoved << " removed, "
1283 << satellites.count() << " remain.";
1284 }
1285
getSecondsToUpdate(void)1286 int Satellites::getSecondsToUpdate(void)
1287 {
1288 QDateTime nextUpdate = lastUpdate.addSecs(updateFrequencyHours * 3600);
1289 return QDateTime::currentDateTime().secsTo(nextUpdate);
1290 }
1291
setTleSources(QStringList tleSources)1292 void Satellites::setTleSources(QStringList tleSources)
1293 {
1294 updateUrls = tleSources;
1295 saveTleSources(updateUrls);
1296 }
1297
saveTleSources(const QStringList & urls)1298 void Satellites::saveTleSources(const QStringList& urls)
1299 {
1300 QSettings* conf = StelApp::getInstance().getSettings();
1301 conf->beginGroup("Satellites");
1302
1303 // clear old source list
1304 conf->remove("tle_sources");
1305
1306 int index = 0;
1307 conf->beginWriteArray("tle_sources");
1308 for (auto url : urls)
1309 {
1310 conf->setArrayIndex(index++);
1311 if (url.startsWith("1,"))
1312 {
1313 conf->setValue("add_new", true);
1314 url.remove(0, 2);
1315 }
1316 else if (url.startsWith("0,"))
1317 url.remove(0, 2);
1318 conf->setValue("url", url);
1319 }
1320 conf->endArray();
1321
1322 conf->endGroup();
1323 }
1324
getFlagLabelsVisible() const1325 bool Satellites::getFlagLabelsVisible() const
1326 {
1327 return Satellite::showLabels;
1328 }
1329
setUpdatesEnabled(bool enabled)1330 void Satellites::setUpdatesEnabled(bool enabled)
1331 {
1332 if (enabled != updatesEnabled)
1333 {
1334 updatesEnabled = enabled;
1335 emit settingsChanged();
1336 emit updatesEnabledChanged(enabled);
1337 }
1338 }
1339
setAutoAddEnabled(bool enabled)1340 void Satellites::setAutoAddEnabled(bool enabled)
1341 {
1342 if (autoAddEnabled != enabled)
1343 {
1344 autoAddEnabled = enabled;
1345 emit autoAddEnabledChanged(enabled);
1346 emit settingsChanged();
1347 }
1348 }
1349
setAutoRemoveEnabled(bool enabled)1350 void Satellites::setAutoRemoveEnabled(bool enabled)
1351 {
1352 if (autoRemoveEnabled != enabled)
1353 {
1354 autoRemoveEnabled = enabled;
1355 emit autoRemoveEnabledChanged(enabled);
1356 emit settingsChanged();
1357 }
1358 }
1359
getFlagIconicMode() const1360 bool Satellites::getFlagIconicMode() const
1361 {
1362 return Satellite::iconicModeFlag;
1363 }
1364
getFlagHideInvisible() const1365 bool Satellites::getFlagHideInvisible() const
1366 {
1367 return Satellite::hideInvisibleSatellitesFlag;
1368 }
1369
setFlagIconicMode(bool b)1370 void Satellites::setFlagIconicMode(bool b)
1371 {
1372 if (Satellite::iconicModeFlag != b)
1373 {
1374 Satellite::iconicModeFlag = b;
1375 emit settingsChanged();
1376 emit flagIconicModeChanged(b);
1377 }
1378 }
1379
setFlagHideInvisible(bool b)1380 void Satellites::setFlagHideInvisible(bool b)
1381 {
1382 if (Satellite::hideInvisibleSatellitesFlag != b)
1383 {
1384 Satellite::hideInvisibleSatellitesFlag = b;
1385 emit settingsChanged();
1386 emit flagHideInvisibleChanged(b);
1387 }
1388 }
1389
setFlagHintsVisible(bool b)1390 void Satellites::setFlagHintsVisible(bool b)
1391 {
1392 if (hintFader != b)
1393 {
1394 hintFader = b;
1395 emit settingsChanged(); // GZ IS THIS REQUIRED/USEFUL??
1396 emit flagHintsVisibleChanged(b);
1397 emit StelApp::getInstance().getCore()->updateSearchLists();
1398 }
1399 }
1400
setFlagLabelsVisible(bool b)1401 void Satellites::setFlagLabelsVisible(bool b)
1402 {
1403 if (Satellite::showLabels != b)
1404 {
1405 Satellite::showLabels = b;
1406 emit settingsChanged(); // GZ IS THIS REQUIRED/USEFUL??
1407 emit flagLabelsVisibleChanged(b);
1408 }
1409 }
1410
setLabelFontSize(int size)1411 void Satellites::setLabelFontSize(int size)
1412 {
1413 if (labelFont.pixelSize() != size)
1414 {
1415 labelFont.setPixelSize(size);
1416 emit labelFontSizeChanged(size);
1417 emit settingsChanged();
1418 }
1419 }
1420
setOrbitLineSegments(int s)1421 void Satellites::setOrbitLineSegments(int s)
1422 {
1423 if (s != Satellite::orbitLineSegments)
1424 {
1425 Satellite::orbitLineSegments=s;
1426 emit orbitLineSegmentsChanged(s);
1427 recalculateOrbitLines();
1428 }
1429 }
1430
setOrbitLineFadeSegments(int s)1431 void Satellites::setOrbitLineFadeSegments(int s)
1432 {
1433 if (s != Satellite::orbitLineFadeSegments)
1434 {
1435 Satellite::orbitLineFadeSegments=s;
1436 emit orbitLineFadeSegmentsChanged(s);
1437 recalculateOrbitLines();
1438 }
1439 }
1440
setOrbitLineSegmentDuration(int s)1441 void Satellites::setOrbitLineSegmentDuration(int s)
1442 {
1443 if (s != Satellite::orbitLineSegmentDuration)
1444 {
1445 Satellite::orbitLineSegmentDuration=s;
1446 emit orbitLineSegmentDurationChanged(s);
1447 recalculateOrbitLines();
1448 }
1449 }
1450
setTleEpochAgeDays(int age)1451 void Satellites::setTleEpochAgeDays(int age)
1452 {
1453 if (age != Satellite::tleEpochAge)
1454 {
1455 Satellite::tleEpochAge=age;
1456 emit tleEpochAgeDaysChanged(age);
1457 }
1458 }
1459
setUpdateFrequencyHours(int hours)1460 void Satellites::setUpdateFrequencyHours(int hours)
1461 {
1462 if (updateFrequencyHours != hours)
1463 {
1464 updateFrequencyHours = hours;
1465 emit updateFrequencyHoursChanged(hours);
1466 emit settingsChanged();
1467 }
1468 }
1469
checkForUpdate(void)1470 void Satellites::checkForUpdate(void)
1471 {
1472 if (updatesEnabled && (updateState != Updating)
1473 && (lastUpdate.addSecs(updateFrequencyHours * 3600) <= QDateTime::currentDateTime()))
1474 {
1475 updateFromOnlineSources();
1476 }
1477 }
1478
updateFromOnlineSources()1479 void Satellites::updateFromOnlineSources()
1480 {
1481 // never update TLE's for any date before Oct 4, 1957, 19:28:34GMT ;-)
1482 if (StelApp::getInstance().getCore()->getJD()<2436116.3115)
1483 return;
1484
1485 if (updateState==Satellites::Updating)
1486 {
1487 qWarning() << "[Satellites] Internet update already in progress!";
1488 return;
1489 }
1490 else
1491 {
1492 qDebug() << "[Satellites] starting Internet update...";
1493 }
1494
1495 // Setting lastUpdate should be done only when the update is finished. -BM
1496
1497 // TODO: Perhaps tie the emptiness of updateUrls to updatesEnabled... --BM
1498 if (updateUrls.isEmpty())
1499 {
1500 qWarning() << "[Satellites] update failed."
1501 << "No update sources are defined!";
1502
1503 // Prevent from re-entering this method on the next check:
1504 markLastUpdate();
1505 // TODO: Do something saner, such as disabling internet updates,
1506 // or stopping the timer. --BM
1507 emit updateStateChanged(OtherError);
1508 emit tleUpdateComplete(0, satellites.count(), 0, 0);
1509 return;
1510 }
1511
1512 updateState = Satellites::Updating;
1513 emit(updateStateChanged(updateState));
1514 updateSources.clear();
1515 numberDownloadsComplete = 0;
1516
1517 if (progressBar==Q_NULLPTR)
1518 progressBar = StelApp::getInstance().addProgressBar();
1519
1520 progressBar->setValue(0);
1521 progressBar->setRange(0, updateUrls.size());
1522 progressBar->setFormat("TLE download %v/%m");
1523
1524 for (auto url : qAsConst(updateUrls))
1525 {
1526 TleSource source;
1527 source.file = Q_NULLPTR;
1528 source.addNew = false;
1529 if (url.startsWith("1,"))
1530 {
1531 // Also prevents inconsistent behaviour if the user toggles the flag
1532 // while an update is in progress.
1533 source.addNew = autoAddEnabled;
1534 url.remove(0, 2);
1535 }
1536 else if (url.startsWith("0,"))
1537 url.remove(0, 2);
1538
1539 source.url.setUrl(url);
1540 if (source.url.isValid())
1541 {
1542 updateSources.append(source);
1543 downloadMgr->get(QNetworkRequest(source.url));
1544 }
1545 }
1546 }
1547
saveDownloadedUpdate(QNetworkReply * reply)1548 void Satellites::saveDownloadedUpdate(QNetworkReply* reply)
1549 {
1550 // check the download worked, and save the data to file if this is the case.
1551 if (reply->error() == QNetworkReply::NoError && reply->bytesAvailable()>0)
1552 {
1553 // download completed successfully.
1554 QString name = QString("tle%1.txt").arg(numberDownloadsComplete);
1555 QString path = dataDir.absoluteFilePath(name);
1556 // QFile as a child object to the plugin to ease memory management
1557 QFile* tmpFile = new QFile(path, this);
1558 if (tmpFile->exists())
1559 tmpFile->remove();
1560
1561 if (tmpFile->open(QIODevice::WriteOnly | QIODevice::Text))
1562 {
1563 QByteArray fd = reply->readAll();
1564 // qWarning() << "[Satellites] Processing an URL:" << reply->url().toString();
1565 if (reply->url().toString().contains(".zip", Qt::CaseInsensitive))
1566 {
1567 QTemporaryFile zip;
1568 if (zip.open())
1569 {
1570 // qWarning() << "[Satellites] Processing a ZIP archive...";
1571 zip.write(fd);
1572 zip.close();
1573 QString archive = zip.fileName();
1574 QByteArray data;
1575
1576 Stel::QZipReader reader(archive);
1577 if (reader.status() != Stel::QZipReader::NoError)
1578 qWarning() << "[Satellites] Unable to open as a ZIP archive";
1579 else
1580 {
1581 QList<Stel::QZipReader::FileInfo> infoList = reader.fileInfoList();
1582 for (const auto& info : qAsConst(infoList))
1583 {
1584 // qWarning() << "[Satellites] Processing:" << info.filePath;
1585 if (info.isFile)
1586 data.append(reader.fileData(info.filePath));
1587 }
1588 // qWarning() << "[Satellites] Extracted data:" << data;
1589 fd = data;
1590 }
1591 reader.close();
1592 zip.remove();
1593 }
1594 else
1595 qWarning() << "[Satellites] Unable to open a temporary file";
1596 }
1597 tmpFile->write(fd);
1598 tmpFile->close();
1599
1600 // The reply URL can be different form the requested one...
1601 QUrl url = reply->request().url();
1602 for (int i = 0; i < updateSources.count(); i++)
1603 {
1604 if (updateSources[i].url == url)
1605 {
1606 updateSources[i].file = tmpFile;
1607 tmpFile = Q_NULLPTR;
1608 break;
1609 }
1610 }
1611 if (tmpFile) // Something strange just happened...
1612 delete tmpFile; // ...so we have to clean.
1613 }
1614 else
1615 {
1616 qWarning() << "[Satellites] cannot save update file:"
1617 << tmpFile->error()
1618 << tmpFile->errorString();
1619 }
1620 }
1621 else
1622 qWarning() << "[Satellites] FAILED to download" << reply->url().toString(QUrl::RemoveUserInfo) << "Error:" << reply->errorString();
1623
1624 numberDownloadsComplete++;
1625 if (progressBar)
1626 progressBar->setValue(numberDownloadsComplete);
1627
1628 // Check if all files have been downloaded.
1629 // TODO: It's better to keep track of the network requests themselves. --BM
1630 if (numberDownloadsComplete < updateSources.size())
1631 return;
1632
1633 if (progressBar)
1634 {
1635 StelApp::getInstance().removeProgressBar(progressBar);
1636 progressBar = Q_NULLPTR;
1637 }
1638
1639 // All files have been downloaded, finish the update
1640 TleDataHash newData;
1641 for (int i = 0; i < updateSources.count(); i++)
1642 {
1643 if (!updateSources[i].file)
1644 continue;
1645 if (updateSources[i].file->open(QFile::ReadOnly|QFile::Text))
1646 {
1647 parseTleFile(*updateSources[i].file, newData, updateSources[i].addNew, updateSources[i].url.toString(QUrl::None));
1648 updateSources[i].file->close();
1649 delete updateSources[i].file;
1650 updateSources[i].file = Q_NULLPTR;
1651 }
1652 }
1653 updateSources.clear();
1654 updateSatellites(newData);
1655 }
1656
updateObserverLocation(const StelLocation & loc)1657 void Satellites::updateObserverLocation(const StelLocation &loc)
1658 {
1659 Q_UNUSED(loc)
1660 recalculateOrbitLines();
1661 }
1662
setFlagOrbitLines(bool b)1663 void Satellites::setFlagOrbitLines(bool b)
1664 {
1665 Satellite::orbitLinesFlag = b;
1666 }
1667
getFlagOrbitLines() const1668 bool Satellites::getFlagOrbitLines() const
1669 {
1670 return Satellite::orbitLinesFlag;
1671 }
1672
recalculateOrbitLines(void)1673 void Satellites::recalculateOrbitLines(void)
1674 {
1675 for (const auto& sat : qAsConst(satellites))
1676 {
1677 if (sat->initialized && sat->displayed && sat->orbitDisplayed)
1678 sat->recalculateOrbitLines();
1679 }
1680 }
1681
displayMessage(const QString & message,const QString hexColor)1682 void Satellites::displayMessage(const QString& message, const QString hexColor)
1683 {
1684 messageIDs << GETSTELMODULE(LabelMgr)->labelScreen(message, 30, 30 + (20*messageIDs.count()), true, 16, hexColor, false, 9000);
1685 }
1686
1687
saveCatalog(QString path)1688 void Satellites::saveCatalog(QString path)
1689 {
1690 saveDataMap(createDataMap(), path);
1691 }
1692
updateFromFiles(QStringList paths,bool deleteFiles)1693 void Satellites::updateFromFiles(QStringList paths, bool deleteFiles)
1694 {
1695 // Container for the new data.
1696 TleDataHash newTleSets;
1697 for (const auto& tleFilePath : paths)
1698 {
1699 QFile tleFile(tleFilePath);
1700 if (tleFile.open(QIODevice::ReadOnly|QIODevice::Text))
1701 {
1702 parseTleFile(tleFile, newTleSets, autoAddEnabled);
1703 tleFile.close();
1704
1705 if (deleteFiles)
1706 tleFile.remove();
1707 }
1708 }
1709 updateSatellites(newTleSets);
1710 }
1711
updateSatellites(TleDataHash & newTleSets)1712 void Satellites::updateSatellites(TleDataHash& newTleSets)
1713 {
1714 // Save the update time.
1715 // One of the reasons it's here is that lastUpdate is used below.
1716 markLastUpdate();
1717
1718 if (newTleSets.isEmpty())
1719 {
1720 qWarning() << "[Satellites] update files contain no TLE sets!";
1721 updateState = OtherError;
1722 emit(updateStateChanged(updateState));
1723 return;
1724 }
1725
1726 if (satelliteListModel)
1727 satelliteListModel->beginSatellitesChange();
1728
1729 // Right, we should now have a map of all the elements we downloaded. For each satellite
1730 // which this module is managing, see if it exists with an updated element, and update it if so...
1731 int sourceCount = newTleSets.count(); // newTleSets is modified below
1732 int updatedCount = 0;
1733 int totalCount = 0;
1734 int addedCount = 0;
1735 int missingCount = 0; // Also the number of removed sats, if any.
1736 QStringList toBeRemoved;
1737 for (const auto& sat : qAsConst(satellites))
1738 {
1739 totalCount++;
1740
1741 // Satellites marked as "user-defined" are protected from updates and
1742 // removal.
1743 if (sat->userDefined)
1744 {
1745 qDebug() << "Satellite ignored (user-protected):"
1746 << sat->id << sat->name;
1747 continue;
1748 }
1749
1750 QString id = sat->id;
1751 TleData newTle = newTleSets.take(id);
1752 if (!newTle.name.isEmpty())
1753 {
1754 if (sat->tleElements.first != newTle.first ||
1755 sat->tleElements.second != newTle.second ||
1756 sat->name != newTle.name)
1757 {
1758 // We have updated TLE elements for this satellite
1759 sat->setNewTleElements(newTle.first, newTle.second);
1760
1761 // Update the name if it has been changed in the source list
1762 sat->name = newTle.name;
1763
1764 // Update operational status
1765 sat->status = newTle.status;
1766
1767 // we reset this to "now" when we started the update.
1768 sat->lastUpdated = lastUpdate;
1769 updatedCount++;
1770 }
1771 int sid = id.toInt();
1772 if (qsMagList.contains(sid))
1773 sat->stdMag = qsMagList[sid];
1774 if (rcsList.contains(sid))
1775 sat->RCS = rcsList[sid];
1776 // special case: starlink satellites; details: http://satobs.org/seesat/Apr-2020/0174.html
1777 if (!rcsList.contains(sid) && sat->name.startsWith("STARLINK"))
1778 sat->RCS = 22.68; // Starlink's solar array is 8.1 x 2.8 metres.
1779
1780 if (sat->status==Satellite::StatusNonoperational && !sat->groups.contains("non-operational"))
1781 sat->groups.insert("non-operational");
1782 if (sat->status!=Satellite::StatusNonoperational && sat->groups.contains("non-operational"))
1783 sat->groups.remove("non-operational");
1784 }
1785 else
1786 {
1787 if (autoRemoveEnabled)
1788 toBeRemoved.append(sat->id);
1789 else
1790 qWarning() << "[Satellites]" << sat->id << sat->name
1791 << "is missing in the update lists.";
1792 missingCount++;
1793 }
1794 // All satellites, who has invalid orbit should be removed.
1795 if (!sat->orbitValid)
1796 {
1797 toBeRemoved.append(sat->id);
1798 qWarning() << "[Satellites]" << sat->id << sat->name
1799 << "has invalid orbit and will be removed.";
1800 missingCount++;
1801 }
1802 }
1803
1804 // Only those not in the loaded collection have remained
1805 // (autoAddEnabled is not checked, because it's already in the flags)
1806 for (const auto& tleData : newTleSets)
1807 {
1808 if (tleData.addThis)
1809 {
1810 // Add the satellite...
1811 if (add(tleData))
1812 addedCount++;
1813 }
1814 }
1815 if (addedCount)
1816 std::sort(satellites.begin(), satellites.end());
1817
1818 if (autoRemoveEnabled && !toBeRemoved.isEmpty())
1819 {
1820 qWarning() << "[Satellites] purging objects that were not updated...";
1821 remove(toBeRemoved);
1822 }
1823
1824 if (updatedCount > 0 ||
1825 (autoRemoveEnabled && missingCount > 0))
1826 {
1827 saveDataMap(createDataMap());
1828 updateState = CompleteUpdates;
1829 }
1830 else
1831 updateState = CompleteNoUpdates;
1832
1833 if (satelliteListModel)
1834 satelliteListModel->endSatellitesChange();
1835
1836 qDebug() << "[Satellites] update finished."
1837 << updatedCount << "/" << totalCount << "updated,"
1838 << addedCount << "added,"
1839 << missingCount << "missing or removed."
1840 << sourceCount << "source entries parsed.";
1841
1842 emit(updateStateChanged(updateState));
1843 emit(tleUpdateComplete(updatedCount, totalCount, addedCount, missingCount));
1844 }
1845
parseTleFile(QFile & openFile,TleDataHash & tleList,bool addFlagValue,const QString & tleURL)1846 void Satellites::parseTleFile(QFile& openFile, TleDataHash& tleList, bool addFlagValue, const QString &tleURL)
1847 {
1848 if (!openFile.isOpen() || !openFile.isReadable())
1849 return;
1850
1851 // Code mostly re-used from updateFromFiles()
1852 int lineNumber = 0;
1853 TleData lastData;
1854
1855 // Celestrak's "status code" list
1856 const QMap<QString, Satellite::OptStatus> satOpStatusMap = {
1857 { "+", Satellite::StatusOperational },
1858 { "-", Satellite::StatusNonoperational },
1859 { "P", Satellite::StatusPartiallyOperational },
1860 { "B", Satellite::StatusStandby },
1861 { "S", Satellite::StatusSpare },
1862 { "X", Satellite::StatusExtendedMission },
1863 { "D", Satellite::StatusDecayed },
1864 { "?", Satellite::StatusUnknown }
1865 };
1866
1867 while (!openFile.atEnd())
1868 {
1869 QString line = QString(openFile.readLine()).trimmed();
1870 if (line.length() < 65) // this is title line
1871 {
1872 // New entry in the list, so reset all fields
1873 lastData = TleData();
1874 lastData.addThis = addFlagValue;
1875 lastData.sourceURL = tleURL;
1876
1877 // The thing in square brackets after the name is actually
1878 // Celestrak's "status code". Parse it!
1879 QRegularExpression statusRx("\\s*\\[(\\D{1})\\]\\s*$", QRegularExpression::InvertedGreedinessOption );
1880 QRegularExpressionMatch match;
1881 if (line.indexOf(statusRx, 0, &match)>-1)
1882 lastData.status = satOpStatusMap.value(match.captured(1).toUpper(), Satellite::StatusUnknown);
1883
1884 //TODO: We need to think of some kind of escaping these
1885 //characters in the JSON parser. --BM
1886 line.replace(QRegularExpression("\\s*\\[([^\\]])*\\]\\s*$"),""); // remove "status code" from name
1887 lastData.name = line;
1888 }
1889 else
1890 {
1891 // TODO: Yet another place suitable for a standard TLE regex. --BM
1892 if (QRegularExpression("^1 .*").match(line).hasMatch())
1893 lastData.first = line;
1894 else if (QRegularExpression("^2 .*").match(line).hasMatch())
1895 {
1896 lastData.second = line;
1897 // The Satellite Catalogue Number is the second number
1898 // on the second line.
1899 QString id = getSatIdFromLine2(line);
1900 if (id.isEmpty())
1901 {
1902 qDebug() << "[Satellites] failed to extract SatId from \"" << line << "\"";
1903 continue;
1904 }
1905 lastData.id = id;
1906
1907 // This is the second line and there will be no more,
1908 // so if everything is OK, save the elements.
1909 if (!lastData.name.isEmpty() && !lastData.first.isEmpty())
1910 {
1911 // Some satellites can be listed in multiple files,
1912 // and only some of those files may be marked for adding,
1913 // so try to preserve the flag - if it's set,
1914 // feel free to overwrite the existing value.
1915 // If not, overwrite only if it's not in the list already.
1916 // NOTE: Second case overwrite may need to check which TLE set is newer.
1917 if (lastData.addThis || !tleList.contains(id))
1918 tleList.insert(id, lastData); // Overwrite if necessary
1919 }
1920 //TODO: Error warnings? --BM
1921 }
1922 else
1923 qDebug() << "[Satellites] unprocessed line " << lineNumber << " in file " << QDir::toNativeSeparators(openFile.fileName());
1924 }
1925 }
1926 }
1927
getSatIdFromLine2(const QString & line)1928 QString Satellites::getSatIdFromLine2(const QString& line)
1929 {
1930 #if (QT_VERSION>=QT_VERSION_CHECK(5, 14, 0))
1931 QString id = line.split(' ', Qt::SkipEmptyParts).at(1).trimmed();
1932 #else
1933 QString id = line.split(' ', QString::SkipEmptyParts).at(1).trimmed();
1934 #endif
1935 if (!id.isEmpty())
1936 {
1937 // Strip any leading zeros as they should be unique ints as strings.
1938 id.remove(QRegularExpression("^[0]*"));
1939 }
1940 return id;
1941 }
1942
loadExtraData()1943 void Satellites::loadExtraData()
1944 {
1945 // Description of file and some additional information you can find here:
1946 // 1) http://www.prismnet.com/~mmccants/tles/mccdesc.html
1947 // 2) http://www.prismnet.com/~mmccants/tles/intrmagdef.html
1948 QFile qsmFile(":/satellites/qs.mag");
1949 qsMagList.clear();
1950 if (qsmFile.open(QFile::ReadOnly))
1951 {
1952 while (!qsmFile.atEnd())
1953 {
1954 QString line = QString(qsmFile.readLine());
1955 int id = line.mid(0,5).trimmed().toInt();
1956 QString smag = line.mid(33,4).trimmed();
1957 if (!smag.isEmpty())
1958 qsMagList.insert(id, smag.toDouble());
1959 }
1960 qsmFile.close();
1961 }
1962
1963 QFile rcsFile(":/satellites/rcs");
1964 rcsList.clear();
1965 if (rcsFile.open(QFile::ReadOnly))
1966 {
1967 while (!rcsFile.atEnd())
1968 {
1969 QString line = QString(rcsFile.readLine());
1970 int id = line.mid(0,5).trimmed().toInt();
1971 QString srcs = line.mid(5,5).trimmed();
1972 if (!srcs.isEmpty())
1973 rcsList.insert(id, srcs.toDouble());
1974 }
1975 rcsFile.close();
1976 }
1977 }
1978
update(double deltaTime)1979 void Satellites::update(double deltaTime)
1980 {
1981 // Separated because first test should be very fast.
1982 if (!hintFader && hintFader.getInterstate() <= 0.f)
1983 return;
1984
1985 StelCore *core = StelApp::getInstance().getCore();
1986
1987 if (qAbs(core->getTimeRate())>=Satellite::timeRateLimit) // Do not show satellites when time rate is over limit
1988 return;
1989
1990 if (core->getCurrentPlanet() != earth || !isValidRangeDates(core))
1991 return;
1992
1993 hintFader.update(static_cast<int>(deltaTime*1000));
1994
1995 for (const auto& sat : qAsConst(satellites))
1996 {
1997 if (sat->initialized && sat->displayed)
1998 sat->update(deltaTime);
1999 }
2000 }
2001
draw(StelCore * core)2002 void Satellites::draw(StelCore* core)
2003 {
2004 // Separated because first test should be very fast.
2005 if (!hintFader && hintFader.getInterstate() <= 0.f)
2006 return;
2007
2008 if (qAbs(core->getTimeRate())>=Satellite::timeRateLimit) // Do not show satellites when time rate is over limit
2009 return;
2010
2011 if (core->getCurrentPlanet()!=earth || !isValidRangeDates(core))
2012 return;
2013
2014 StelProjectorP prj = core->getProjection(StelCore::FrameJ2000);
2015 StelPainter painter(prj);
2016 painter.setFont(labelFont);
2017 Satellite::hintBrightness = hintFader.getInterstate();
2018
2019 painter.setBlending(true);
2020 Satellite::hintTexture->bind();
2021 Satellite::viewportHalfspace = painter.getProjector()->getBoundingCap();
2022 for (const auto& sat : qAsConst(satellites))
2023 {
2024 if (sat && sat->initialized && sat->displayed)
2025 sat->draw(core, painter);
2026 }
2027
2028 if (GETSTELMODULE(StelObjectMgr)->getFlagSelectedObjectPointer())
2029 drawPointer(core, painter);
2030 }
2031
drawPointer(StelCore * core,StelPainter & painter)2032 void Satellites::drawPointer(StelCore* core, StelPainter& painter)
2033 {
2034 const StelProjectorP prj = core->getProjection(StelCore::FrameJ2000);
2035
2036 const QList<StelObjectP> newSelected = GETSTELMODULE(StelObjectMgr)->getSelectedObject("Satellite");
2037 if (!newSelected.empty())
2038 {
2039 const StelObjectP obj = newSelected[0];
2040 Vec3d pos=obj->getJ2000EquatorialPos(core);
2041 Vec3d screenpos;
2042
2043 // Compute 2D pos and return if outside screen
2044 if (!prj->project(pos, screenpos))
2045 return;
2046 painter.setColor(0.4f,0.5f,0.8f);
2047 texPointer->bind();
2048
2049 painter.setBlending(true);
2050
2051 // Size on screen
2052 double size = obj->getAngularSize(core)*M_PI/180.*static_cast<double>(prj->getPixelPerRadAtCenter());
2053 size += 12. + 3.*std::sin(2. * StelApp::getInstance().getTotalRunTime());
2054 // size+=20.f + 10.f*std::sin(2.f * StelApp::getInstance().getTotalRunTime());
2055 painter.drawSprite2dMode(static_cast<float>(screenpos[0]-size/2), static_cast<float>(screenpos[1]-size/2), 20, 90);
2056 painter.drawSprite2dMode(static_cast<float>(screenpos[0]-size/2), static_cast<float>(screenpos[1]+size/2), 20, 0);
2057 painter.drawSprite2dMode(static_cast<float>(screenpos[0]+size/2), static_cast<float>(screenpos[1]+size/2), 20, -90);
2058 painter.drawSprite2dMode(static_cast<float>(screenpos[0]+size/2), static_cast<float>(screenpos[1]-size/2), 20, -180);
2059 }
2060 }
2061
checkJsonFileFormat()2062 bool Satellites::checkJsonFileFormat()
2063 {
2064 QFile jsonFile(catalogPath);
2065 if (!jsonFile.open(QIODevice::ReadOnly))
2066 {
2067 qWarning() << "[Satellites] cannot open " << QDir::toNativeSeparators(catalogPath);
2068 return false;
2069 }
2070
2071 QVariantMap map;
2072 try
2073 {
2074 map = StelJsonParser::parse(&jsonFile).toMap();
2075 jsonFile.close();
2076 }
2077 catch (std::runtime_error& e)
2078 {
2079 qDebug() << "[Satellites] File format is wrong! Error:" << e.what();
2080 return false;
2081 }
2082
2083 return true;
2084 }
2085
isValidRangeDates(const StelCore * core) const2086 bool Satellites::isValidRangeDates(const StelCore *core) const
2087 {
2088 bool ok;
2089 double tJD = core->getJD();
2090 double uJD = StelUtils::getJulianDayFromISO8601String(lastUpdate.toString(Qt::ISODate), &ok);
2091 if (lastUpdate.isNull()) // No updates yet?
2092 uJD = tJD;
2093 // do not draw anything before Oct 4, 1957, 19:28:34GMT ;-)
2094 // upper limit for drawing is +5 years after latest update of TLE
2095 if ((tJD<2436116.3115) || (tJD>(uJD+1825)))
2096 return false;
2097 else
2098 return true;
2099 }
2100
2101 #if(SATELLITES_PLUGIN_IRIDIUM == 1)
2102 #ifdef _OLD_IRIDIUM_PREDICTIONS
getIridiumFlaresPrediction()2103 IridiumFlaresPredictionList Satellites::getIridiumFlaresPrediction()
2104 {
2105 StelCore* pcore = StelApp::getInstance().getCore();
2106 double currentJD = pcore->getJD(); // save current JD
2107 bool isTimeNow = pcore->getIsTimeNow();
2108 long iJD =currentJD;
2109 double JDMidnight = iJD + 0.5 - pcore->getCurrentLocation().longitude / 360.f;
2110 double delta = currentJD - JDMidnight;
2111
2112 pcore->setJD(iJD + 0.5 - pcore->getCurrentLocation().longitude / 360.f);
2113 pcore->update(10); // force update to get new coordinates
2114
2115 IridiumFlaresPredictionList predictions;
2116 predictions.clear();
2117 for (const auto& sat : satellites)
2118 {
2119 if (sat->initialized && sat->getEnglishName().startsWith("IRIDIUM"))
2120 {
2121 double dt = 0;
2122 double angle0 = 100;
2123 double lat0 = -1;
2124 while (dt<1)
2125 {
2126 Satellite::timeShift = dt+delta;
2127 sat->update(0);
2128
2129 Vec3d pos = sat->getAltAzPosApparent(pcore);
2130 double lat = pos.latitude();
2131 double lon = M_PI - pos.longitude();
2132 double v = sat->getVMagnitude(pcore);
2133 double angle = sat->sunReflAngle;
2134
2135 if (angle>0 && angle<2)
2136 {
2137 if (angle>angle0 && (v<1) && lat>5*M_PI/180)
2138 {
2139 IridiumFlaresPrediction flare;
2140 flare.datetime = StelUtils::julianDayToISO8601String(currentJD+dt+StelApp::getInstance().getCore()->getUTCOffset(currentJD+dt)/24.f);
2141 flare.satellite = sat->getEnglishName();
2142 flare.azimuth = lon;
2143 flare.altitude = lat;
2144 flare.magnitude = v;
2145
2146 predictions.append(flare);
2147
2148 dt +=0.005;
2149 }
2150 angle0 = angle;
2151 }
2152 if (angle>0)
2153 dt+=0.000002*angle*angle;
2154 else
2155 {
2156 if (lat0>0 && lat<0)
2157 dt+=0.05;
2158 else
2159 dt+=0.0002;
2160 }
2161 lat0 = lat;
2162 }
2163 }
2164 }
2165 Satellite::timeShift = 0.;
2166 if (isTimeNow)
2167 pcore->setTimeNow();
2168 else
2169 pcore->setJD(currentJD);
2170
2171 return predictions;
2172 }
2173 #else
2174
2175 struct SatDataStruct {
2176 double nextJD;
2177 double angleToSun;
2178 double altitude;
2179 double azimuth;
2180 double v;
2181 };
2182
getIridiumFlaresPrediction()2183 IridiumFlaresPredictionList Satellites::getIridiumFlaresPrediction()
2184 {
2185 StelCore* pcore = StelApp::getInstance().getCore();
2186 SolarSystem* ssystem = (SolarSystem*)StelApp::getInstance().getModuleMgr().getModule("SolarSystem");
2187
2188 double currentJD = pcore->getJD(); // save current JD
2189 bool isTimeNow = pcore->getIsTimeNow();
2190
2191 double predictionJD = currentJD - 1.; // investigate what's seen recently// yesterday
2192 double predictionEndJD = currentJD + getIridiumFlaresPredictionDepth(); // 7 days interval by default
2193 pcore->setJD(predictionJD);
2194
2195 bool useSouthAzimuth = StelApp::getInstance().getFlagSouthAzimuthUsage();
2196
2197 IridiumFlaresPredictionList predictions;
2198
2199 // create a lіst of Iridiums
2200 QMap<SatelliteP,SatDataStruct> iridiums;
2201 SatDataStruct sds;
2202 double nextJD = predictionJD + 1./24;
2203
2204 for (const auto& sat : satellites)
2205 {
2206 if (sat->initialized && sat->getEnglishName().startsWith("IRIDIUM"))
2207 {
2208 Vec3d pos = sat->getAltAzPosApparent(pcore);
2209 sds.angleToSun = sat->sunReflAngle;
2210 sds.altitude = pos.latitude();
2211 sds.azimuth = pos.longitude();
2212 sds.v = sat->getVMagnitude(pcore);
2213 double t;
2214 if (sds.altitude<0)
2215 t = qMax(-sds.altitude, 1.) / 2880;
2216 else
2217 if (sds.angleToSun>0)
2218 t = qMax(sds.angleToSun, 1.) / (2*86400);
2219 else
2220 {
2221 //qDebug() << "IRIDIUM warn: alt, angleToSun = " <<sds.altitude << sds.angleToSun;
2222 t = 0.25/1440; // we should never be here, but assuming 1/4 minute to leave this
2223 }
2224
2225 sds.nextJD = predictionJD + t;
2226 iridiums.insert(sat, sds);
2227 if (nextJD>sds.nextJD)
2228 nextJD = sds.nextJD;
2229 }
2230 }
2231 predictionJD = nextJD;
2232
2233 while (predictionJD<predictionEndJD)
2234 {
2235 nextJD = predictionJD + 1./24;
2236 pcore->setJD(predictionJD);
2237
2238 ssystem->getEarth()->computePosition(predictionJD);
2239 pcore->update(0);
2240
2241 for (auto i = iridiums.begin(); i != iridiums.end(); ++i)
2242 {
2243 if ( i.value().nextJD<=predictionJD)
2244 {
2245 i.key()->update(0);
2246
2247 double v = i.key()->getVMagnitude(pcore);
2248 bool flareFound = false;
2249 if (v > i.value().v)
2250 {
2251 if (i.value().v < 1. // brighness limit
2252 && i.value().angleToSun>0.
2253 && i.value().angleToSun<2.)
2254 {
2255 IridiumFlaresPrediction flare;
2256 flare.datetime = StelUtils::julianDayToISO8601String(predictionJD+StelApp::getInstance().getCore()->getUTCOffset(predictionJD)/24.f);
2257 flare.satellite = i.key()->getEnglishName();
2258 flare.azimuth = i.value().azimuth;
2259 if (useSouthAzimuth)
2260 {
2261 flare.azimuth += M_PI;
2262 if (flare.azimuth > M_PI*2)
2263 flare.azimuth -= M_PI*2;
2264 }
2265 flare.altitude = i.value().altitude;
2266 flare.magnitude = i.value().v;
2267
2268 predictions.append(flare);
2269 flareFound = true;
2270 //qDebug() << "Flare:" << flare.datetime << flare.satellite;
2271 }
2272 }
2273
2274 Vec3d pos = i.key()->getAltAzPosApparent(pcore);
2275
2276 i.value().v = flareFound ? 17 : v; // block extra report
2277 i.value().altitude = pos.latitude();
2278 i.value().azimuth = M_PI - pos.longitude();
2279 i.value().angleToSun = i.key()->sunReflAngle;
2280
2281 double t;
2282 if (flareFound)
2283 t = 1./24;
2284 else
2285 if (i.value().altitude<0)
2286 t = qMax((-i.value().altitude)*57,1.) / 5600;
2287 else
2288 if (i.value().angleToSun>0)
2289 t = qMax(i.value().angleToSun,1.) / (4*86400);
2290 else
2291 {
2292 //qDebug() << "IRIDIUM warn2: alt, angleToSun = " <<i.value().altitude << i.value().angleToSun;
2293 t = 0.25/1440; // we should never be here, but assuming 1/4 minute to leave this
2294 }
2295 i.value().nextJD = predictionJD + t;
2296 if (nextJD>i.value().nextJD)
2297 nextJD = i.value().nextJD;
2298 }
2299 }
2300 predictionJD = nextJD;
2301 }
2302
2303 //Satellite::timeShift = 0.;
2304 if (isTimeNow)
2305 pcore->setTimeNow();
2306 else
2307 pcore->setJD(currentJD);
2308
2309 return predictions;
2310 }
2311 #endif
2312 // close SATELLITES_PLUGIN_IRIDIUM
2313 #endif
2314
translations()2315 void Satellites::translations()
2316 {
2317 #if 0
2318 // Satellite groups
2319 // TRANSLATORS: Satellite group: Bright/naked-eye-visible satellites
2320 N_("visual");
2321 // TRANSLATORS: Satellite group: Scientific satellites
2322 N_("scientific");
2323 // TRANSLATORS: Satellite group: Communication satellites
2324 N_("communications");
2325 // TRANSLATORS: Satellite group: Navigation satellites
2326 N_("navigation");
2327 // TRANSLATORS: Satellite group: Amateur radio (ham) satellites
2328 N_("amateur");
2329 // TRANSLATORS: Satellite group: Weather (meteorological) satellites
2330 N_("weather");
2331 // TRANSLATORS: Satellite group: Earth Resources satellites
2332 N_("earth resources");
2333 // TRANSLATORS: Satellite group: Satellites in geostationary orbit
2334 N_("geostationary");
2335 // TRANSLATORS: Satellite group: Satellites that are no longer functioning
2336 N_("non-operational");
2337 // TRANSLATORS: Satellite group: Satellites belonging to the GPS constellation (the Global Positioning System)
2338 N_("gps");
2339 // TRANSLATORS: Satellite group: The Indian Regional Navigation Satellite System (IRNSS) is an autonomous regional satellite navigation system being developed by the Indian Space Research Organisation (ISRO) which would be under complete control of the Indian government.
2340 N_("irnss");
2341 // TRANSLATORS: Satellite group: The Quasi-Zenith Satellite System (QZSS), is a proposed three-satellite regional time transfer system and Satellite Based Augmentation System for the Global Positioning System, that would be receivable within Japan.
2342 N_("qzss");
2343 // TRANSLATORS: Satellite group: The Tracking and Data Relay Satellite System (TDRSS) is a network of communications satellites and ground stations used by NASA for space communications.
2344 N_("tdrss");
2345 // TRANSLATORS: Satellite group: Satellites belonging to the GLONASS constellation (GLObal NAvigation Satellite System)
2346 N_("glonass");
2347 // TRANSLATORS: Satellite group: Satellites belonging to the BeiDou constellation (BeiDou Navigation Satellite System)
2348 N_("beidou");
2349 // TRANSLATORS: Satellite group: Satellites belonging to the Galileo constellation (global navigation satellite system by the European Union)
2350 N_("galileo");
2351 // TRANSLATORS: Satellite group: Satellites belonging to the Iridium constellation (Iridium is a proper name)
2352 N_("iridium");
2353 // TRANSLATORS: Satellite group: Satellites belonging to the Iridium NEXT constellation (Iridium is a proper name)
2354 N_("iridium next");
2355 // TRANSLATORS: Satellite group: Satellites belonging to the Starlink constellation (Starlink is a proper name)
2356 N_("starlink");
2357 // TRANSLATORS: Satellite group: Satellites belonging to the Spire constellation (LEMUR satellites)
2358 N_("spire");
2359 // TRANSLATORS: Satellite group: Satellites belonging to the OneWeb constellation (OneWeb is a proper name)
2360 N_("oneweb");
2361 // TRANSLATORS: Satellite group: Space stations
2362 N_("stations");
2363 // TRANSLATORS: Satellite group: Education satellites
2364 N_("education");
2365 // TRANSLATORS: Satellite group: Satellites belonging to the space observatories
2366 N_("observatory");
2367 // TRANSLATORS: Satellite group: Satellites belonging to the INTELSAT satellites
2368 N_("intelsat");
2369 // TRANSLATORS: Satellite group: Satellites belonging to the GLOBALSTAR satellites
2370 N_("globalstar");
2371 // TRANSLATORS: Satellite group: Satellites belonging to the ORBCOMM satellites
2372 N_("orbcomm");
2373 // TRANSLATORS: Satellite group: Satellites belonging to the GORIZONT satellites
2374 N_("gorizont");
2375 // TRANSLATORS: Satellite group: Satellites belonging to the RADUGA satellites
2376 N_("raduga");
2377 // TRANSLATORS: Satellite group: Satellites belonging to the MOLNIYA satellites
2378 N_("molniya");
2379 // TRANSLATORS: Satellite group: Satellites belonging to the COSMOS satellites
2380 N_("cosmos");
2381 // TRANSLATORS: Satellite group: Debris of satellites
2382 N_("debris");
2383 // TRANSLATORS: Satellite group: Crewed satellites
2384 N_("crewed");
2385 // TRANSLATORS: Satellite group: Satellites of ISS resupply missions
2386 N_("resupply");
2387 // TRANSLATORS: Satellite group: are known to broadcast TV signals
2388 N_("tv");
2389 // TRANSLATORS: Satellite group: military satellites
2390 N_("military");
2391 // TRANSLATORS: Satellite group: geodetic satellites
2392 N_("geodetic");
2393
2394 // Satellite descriptions - bright and/or famous objects
2395 // Just A FEW objects please! (I'm looking at you, Alex!)
2396 // TRANSLATORS: Satellite description. "Hubble" is a person's name.
2397 N_("The Hubble Space Telescope");
2398 // TRANSLATORS: Satellite description.
2399 N_("The International Space Station");
2400 // TRANSLATORS: Satellite description.
2401 N_("China's first space station");
2402 // TRANSLATORS: Satellite description.
2403 N_("Tiangong space station (Chinese large modular space station)");
2404 // TRANSLATORS: Satellite description.
2405 N_("The russian space radio telescope RadioAstron");
2406 // TRANSLATORS: Satellite description.
2407 N_("International Gamma-Ray Astrophysics Laboratory");
2408 // TRANSLATORS: Satellite description.
2409 N_("The Gamma-Ray Observatory");
2410 // TRANSLATORS: Satellite description.
2411 N_("The Microvariability and Oscillations of Stars telescope");
2412 // TRANSLATORS: Satellite description.
2413 N_("The Interface Region Imaging Spectrograph");
2414 // TRANSLATORS: Satellite description.
2415 N_("The Spectroscopic Planet Observatory for Recognition of Interaction of Atmosphere");
2416 // TRANSLATORS: Satellite description.
2417 M_("Nuclear Spectroscopic Telescope Array");
2418 // TRANSLATORS: Satellite description.
2419 N_("The Dark Matter Particle Explorer");
2420 // TRANSLATORS: Satellite description.
2421 N_("Arcsecond Space Telescope Enabling Research in Astrophysics");
2422 // TRANSLATORS: Satellite description.
2423 N_("Reuven Ramaty High Energy Solar Spectroscopic Imager");
2424 // TRANSLATORS: Satellite description.
2425 N_("The Chandra X-ray Observatory");
2426
2427 // Satellite names - a few famous objects only
2428 // TRANSLATORS: Satellite name: International Space Station
2429 N_("ISS (ZARYA)");
2430 // TRANSLATORS: Satellite name: International Space Station
2431 N_("ISS");
2432 // TRANSLATORS: Satellite name: Hubble Space Telescope
2433 N_("HST");
2434 // TRANSLATORS: Satellite name: Spektr-R Space Observatory (or RadioAstron)
2435 N_("SPEKTR-R");
2436 // TRANSLATORS: Satellite name: International Gamma-Ray Astrophysics Laboratory (INTEGRAL)
2437 N_("INTEGRAL");
2438 // TRANSLATORS: Satellite name: China's first space station name
2439 N_("TIANGONG 1");
2440 // TRANSLATORS: Satellite name: name of China's space station module
2441 N_("TIANHE");
2442 // TRANSLATORS: Satellite name: China's space station name (with name of base module)
2443 N_("TIANGONG (TIANHE)");
2444
2445 // Satellites visibility
2446 N_("The satellite and the observer are in sunlight");
2447 N_("The satellite is visible");
2448 N_("The satellite is eclipsed");
2449 N_("The satellite is not visible");
2450
2451 #endif
2452 }
2453