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