1 /*
2  * Stellarium
3  * Copyright (C) 2009, 2012 Matthew Gates
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 "Satellite.hpp"
21 #include "StelObject.hpp"
22 #include "StelPainter.hpp"
23 #include "StelApp.hpp"
24 #include "StelLocation.hpp"
25 #include "StelCore.hpp"
26 #include "StelTexture.hpp"
27 #include "VecMath.hpp"
28 #include "StelUtils.hpp"
29 #include "StelTranslator.hpp"
30 #include "StelModuleMgr.hpp"
31 #include "StelLocaleMgr.hpp"
32 
33 #include <QTextStream>
34 #include <QDebug>
35 #include <QVariant>
36 #include <QSettings>
37 #include <QByteArray>
38 
39 #include <QVector3D>
40 #include <QMatrix4x4>
41 
42 #include "gsatellite/gTime.hpp"
43 #include "gsatellite/stdsat.h"
44 
45 #include <cmath>
46 
47 #define sqr(a) ((a)*(a))
48 
49 const QString Satellite::SATELLITE_TYPE = QStringLiteral("Satellite");
50 
51 // static data members - will be initialised in the Satellites class (the StelObjectMgr)
52 StelTextureSP Satellite::hintTexture;
53 bool Satellite::showLabels = true;
54 float Satellite::hintBrightness = 0.f;
55 float Satellite::hintScale = 1.f;
56 SphericalCap Satellite::viewportHalfspace = SphericalCap();
57 int Satellite::orbitLineSegments = 90;
58 int Satellite::orbitLineFadeSegments = 4;
59 int Satellite::orbitLineSegmentDuration = 20;
60 bool Satellite::orbitLinesFlag = true;
61 bool Satellite::iconicModeFlag = false;
62 bool Satellite::hideInvisibleSatellitesFlag = false;
63 Vec3f Satellite::invisibleSatelliteColor = Vec3f(0.2f,0.2f,0.2f);
64 Vec3f Satellite::transitSatelliteColor = Vec3f(0.f,0.f,0.f);
65 double Satellite::timeRateLimit = 1.0; // one JD per second by default
66 int Satellite::tleEpochAge = 30; // default age of TLE's epoch to mark TLE as outdated (using for filters)
67 
68 #if (SATELLITES_PLUGIN_IRIDIUM == 1)
69 double Satellite::sunReflAngle = 180.;
70 //double Satellite::timeShift = 0.;
71 #endif
72 
Satellite(const QString & identifier,const QVariantMap & map)73 Satellite::Satellite(const QString& identifier, const QVariantMap& map)
74 	: initialized(false)
75 	, displayed(false)
76 	, orbitDisplayed(false)
77 	, userDefined(false)
78 	, newlyAdded(false)
79 	, orbitValid(false)
80 	, jdLaunchYearJan1(0)
81 	, stdMag(99.)
82 	, RCS(-1.)
83 	, status(StatusUnknown)
84 	, height(0.)
85 	, range(0.)
86 	, rangeRate(0.)
87 	, hintColor(0.f,0.f,0.f)
88 	, lastUpdated()
89 	, isISS(false)
90 	, pSatWrapper(Q_NULLPTR)
91 	, visibility(gSatWrapper::UNKNOWN)
92 	, phaseAngle(0.)
93 	, infoColor(0.f,0.f,0.f)
94 	, orbitColor(0.f,0.f,0.f)
95 	, lastEpochCompForOrbit(0.)
96 	, epochTime(0.)
97 {
98 	// return initialized if the mandatory fields are not present
99 	if (identifier.isEmpty())
100 		return;
101 	if (!map.contains("name") || !map.contains("tle1") || !map.contains("tle2"))
102 		return;
103 
104 	id = identifier;
105 	name  = map.value("name").toString();
106 	if (name.isEmpty())
107 		return;
108 
109 	// If there are no such keys, these will be initialized with the default
110 	// values given them above.
111 	description = map.value("description", description).toString().trimmed();
112 	displayed = map.value("visible", displayed).toBool();
113 	orbitDisplayed = map.value("orbitVisible", orbitDisplayed).toBool();
114 	userDefined = map.value("userDefined", userDefined).toBool();
115 	stdMag = map.value("stdMag", 99.).toDouble();
116 	RCS = map.value("rcs", -1.).toDouble();
117 	status = map.value("status", StatusUnknown).toInt();
118 	// Satellite hint color
119 	QVariantList list = map.value("hintColor", QVariantList()).toList();
120 	if (list.count() == 3)
121 	{
122 		hintColor[0] = list.at(0).toFloat();
123 		hintColor[1] = list.at(1).toFloat();
124 		hintColor[2] = list.at(2).toFloat();
125 	}
126 
127 	// Satellite orbit section color
128 	list = map.value("orbitColor", QVariantList()).toList();
129 	if (list.count() == 3)
130 	{
131 		orbitColor[0] = list.at(0).toFloat();
132 		orbitColor[1] = list.at(1).toFloat();
133 		orbitColor[2] = list.at(2).toFloat();
134 	}
135 	else
136 	{
137 		orbitColor = hintColor;
138 	}
139 
140 	// Satellite info color
141 	list = map.value("infoColor", QVariantList()).toList();
142 	if (list.count() == 3)
143 	{
144 		infoColor[0] = list.at(0).toFloat();
145 		infoColor[1] = list.at(1).toFloat();
146 		infoColor[2] = list.at(2).toFloat();
147 	}
148 	else
149 	{
150 		infoColor = hintColor;
151 	}
152 
153 	if (map.contains("comms"))
154 	{
155 		for (const auto& comm : map.value("comms").toList())
156 		{
157 			QVariantMap commMap = comm.toMap();
158 			CommLink c;
159 			if (commMap.contains("frequency")) c.frequency = commMap.value("frequency").toDouble();
160 			if (commMap.contains("modulation")) c.modulation = commMap.value("modulation").toString();
161 			if (commMap.contains("description")) c.description = commMap.value("description").toString();
162 			comms.append(c);
163 		}
164 	}
165 
166 	QVariantList groupList =  map.value("groups", QVariantList()).toList();
167 	if (!groupList.isEmpty())
168 	{
169 		for (const auto& group : qAsConst(groupList))
170 			groups.insert(group.toString());
171 	}
172 
173 	// TODO: Somewhere here - some kind of TLE validation.
174 	QString line1 = map.value("tle1").toString();
175 	QString line2 = map.value("tle2").toString();
176 	setNewTleElements(line1, line2);
177 	// This also sets the international designator and launch year.
178 
179 	QString dateString = map.value("lastUpdated").toString();
180 	if (!dateString.isEmpty())
181 		lastUpdated = QDateTime::fromString(dateString, Qt::ISODate);
182 
183 	orbitValid = true;
184 	initialized = true;
185 	isISS = (name=="ISS" || name=="ISS (ZARYA)");
186 	moon = GETSTELMODULE(SolarSystem)->getMoon();
187 	sun = GETSTELMODULE(SolarSystem)->getSun();
188 
189 	// Please sync text in Satellites.cpp file after adding new types
190 	visibilityDescription={
191 		{ gSatWrapper::RADAR_SUN, "The satellite and the observer are in sunlight" },
192 		{ gSatWrapper::VISIBLE, "The satellite is visible" },
193 		{ gSatWrapper::RADAR_NIGHT, "The satellite is eclipsed" },
194 		{ gSatWrapper::NOT_VISIBLE, "The satellite is not visible" }
195 	};
196 
197 	update(0.);
198 }
199 
~Satellite()200 Satellite::~Satellite()
201 {
202 	if (pSatWrapper != Q_NULLPTR)
203 	{
204 		delete pSatWrapper;
205 		pSatWrapper = Q_NULLPTR;
206 	}
207 }
208 
roundToDp(float n,int dp)209 double Satellite::roundToDp(float n, int dp)
210 {
211 	// round n to dp decimal places
212 	return floor(static_cast<double>(n) * pow(10., dp) + .5) / pow(10., dp);
213 }
214 
getNameI18n() const215 QString Satellite::getNameI18n() const
216 {
217 	return q_(name);
218 }
219 
getMap(void)220 QVariantMap Satellite::getMap(void)
221 {
222 	QVariantMap map;
223 	map["name"] = name;
224 	map["stdMag"] = stdMag;
225 	map["rcs"] = RCS;
226 	map["status"] = status;
227 	map["tle1"] = tleElements.first.data();
228 	map["tle2"] = tleElements.second.data();
229 
230 	if (!description.isEmpty())
231 		map["description"] = description;
232 
233 	map["visible"] = displayed;
234 	map["orbitVisible"] = orbitDisplayed;
235 	if (userDefined)
236 		map.insert("userDefined", userDefined);
237 	QVariantList col, orbitCol, infoCol;
238 	col << roundToDp(hintColor[0],3) << roundToDp(hintColor[1], 3) << roundToDp(hintColor[2], 3);
239 	orbitCol << roundToDp(orbitColor[0], 3) << roundToDp(orbitColor[1], 3) << roundToDp(orbitColor[2],3);
240 	infoCol << roundToDp(infoColor[0], 3) << roundToDp(infoColor[1], 3) << roundToDp(infoColor[2],3);
241 	map["hintColor"] = col;
242 	map["orbitColor"] = orbitCol;
243 	map["infoColor"] = infoCol;
244 	QVariantList commList;
245 	for (const auto& c : qAsConst(comms))
246 	{
247 		QVariantMap commMap;
248 		commMap["frequency"] = c.frequency;
249 		if (!c.modulation.isEmpty()) commMap["modulation"] = c.modulation;
250 		if (!c.description.isEmpty()) commMap["description"] = c.description;
251 		commList << commMap;
252 	}
253 	map["comms"] = commList;
254 	QVariantList groupList;
255 	for (const auto& g : qAsConst(groups))
256 	{
257 		groupList << g;
258 	}
259 	map["groups"] = groupList;
260 
261 	if (!lastUpdated.isNull())
262 	{
263 		// A raw QDateTime is not a recognised JSON data type. --BM
264 		map["lastUpdated"] = lastUpdated.toString(Qt::ISODate);
265 	}
266 
267 	return map;
268 }
269 
getSelectPriority(const StelCore *) const270 float Satellite::getSelectPriority(const StelCore*) const
271 {
272 	return -10.;
273 }
274 
getInfoString(const StelCore * core,const InfoStringGroup & flags) const275 QString Satellite::getInfoString(const StelCore *core, const InfoStringGroup& flags) const
276 {
277 	QString str;
278 	QTextStream oss(&str);
279 	QString degree = QChar(0x00B0);
280 	const bool withDecimalDegree = StelApp::getInstance().getFlagShowDecimalDegrees();
281 
282 	if (flags & Name)
283 	{
284 		oss << "<h2>" << getNameI18n() << "</h2>";
285 		if (!description.isEmpty())
286 		{
287 			// Let's convert possible \n chars into <br/> in description of satellite
288 			oss << q_(description).replace("\n", "<br/>") << "<br/>";
289 		}
290 	}
291 
292 	if (flags & CatalogNumber)
293 	{
294 		QString catalogNumbers;
295 		if (internationalDesignator.isEmpty())
296 			catalogNumbers = QString("NORAD %1")
297 					 .arg(id);
298 		else
299 			catalogNumbers = QString("NORAD %1; %2: %3")
300 					 .arg(id, q_("International Designator"), internationalDesignator);
301 		oss << catalogNumbers << "<br/><br/>";
302 	}
303 
304 	if (flags & ObjectType)
305 		oss << QString("%1: <b>%2</b>").arg(q_("Type"), q_("artificial satellite"))  << "<br/>";
306 
307 	if ((flags & Magnitude) && (stdMag<99. || RCS>0.) && (visibility==gSatWrapper::VISIBLE))
308 	{
309 		const int decimals = 2;
310 		const float airmass = getAirmass(core);
311 		oss << QString("%1: <b>%2</b>").arg(q_("Approx. magnitude"), QString::number(getVMagnitude(core), 'f', decimals));
312 		if (airmass>-1.f) // Don't show extincted magnitude much below horizon where model is meaningless.
313 			oss << QString(" (%1 <b>%2</b> %3 <b>%4</b> %5)").arg(q_("reduced to"), QString::number(getVMagnitudeWithExtinction(core), 'f', decimals), q_("by"), QString::number(airmass, 'f', decimals), q_("Airmasses"));
314 		oss << "<br />";
315 	}
316 
317 	// Ra/Dec etc.
318 	oss << getCommonInfoString(core, flags);
319 
320 	if (flags&Distance)
321 	{
322 		QString km = qc_("km", "distance");
323 		// TRANSLATORS: Slant range: distance between the satellite and the observer
324 		oss << QString("%1: %2 %3").arg(q_("Range")).arg(qRound(range)).arg(km) << "<br/>";
325 		// TRANSLATORS: Rate at which the distance changes
326 		oss << QString("%1: %2 %3").arg(q_("Range rate")).arg(rangeRate, 5, 'f', 3).arg(qc_("km/s", "speed")) << "<br/>";
327 		// TRANSLATORS: Satellite altitude
328 		oss << QString("%1: %2 %3").arg(q_("Altitude")).arg(qRound(height)).arg(km) << "<br/>";
329 		oss << QString("%1: %2 %3 / %4 %5").arg(q_("Perigee/apogee altitudes"))
330 		       .arg(qRound(perigee)).arg(km)
331 		       .arg(qRound(apogee)).arg(km)
332 		<< "<br/>";
333 	}
334 
335 	if (flags&Size && RCS>0.)
336 	{
337 		const double angularSize = getAngularSize(core)*M_PI_180;
338 		QString sizeStr = "";
339 		if (withDecimalDegree)
340 			sizeStr = StelUtils::radToDecDegStr(angularSize, 5, false, true);
341 		else
342 			sizeStr = StelUtils::radToDmsPStr(angularSize, 2);
343 		oss << QString("%1: %2").arg(q_("Approx. angular size"), sizeStr) << "<br />";
344 	}
345 
346 	if (flags & Extra)
347 	{
348 		double orbitalPeriod = pSatWrapper->getOrbitalPeriod();
349 		if (orbitalPeriod>0.0)
350 		{
351 			// TRANSLATORS: Revolutions per day - measurement of the frequency of a rotation
352 			QString rpd = qc_("rpd","frequency");
353 			// TRANSLATORS: minutes - orbital period for artificial satellites
354 			QString mins = qc_("min", "period");
355 			oss << QString("%1: %2 %3 (%4 &mdash; %5 %6)")
356 			       .arg(q_("Orbital period")).arg(orbitalPeriod, 5, 'f', 2)
357 			       .arg(mins).arg(StelUtils::hoursToHmsStr(orbitalPeriod/60.0, true))
358 			       .arg(1440.0/orbitalPeriod, 9, 'f', 5).arg(rpd) << "<br/>";
359 		}
360 		double inclination = pSatWrapper->getOrbitalInclination();
361 		oss << QString("%1: %2 (%3%4)")
362 		       .arg(q_("Inclination"), StelUtils::decDegToDmsStr(inclination),
363 			    QString::number(inclination, 'f', 4), degree)
364 		<< "<br/>";
365 		oss << QString("%1: %2%3/%4%5")
366 		       .arg(q_("SubPoint (Lat./Long.)"))
367 		       .arg(latLongSubPointPosition[0], 5, 'f', 2)
368 		       .arg(QChar(0x00B0))
369 		       .arg(latLongSubPointPosition[1], 5, 'f', 3)
370 		       .arg(QChar(0x00B0));
371 		oss << "<br/>";
372 
373 		//TODO: This one can be done better
374 		const char* xyz = "<b>X:</b> %1, <b>Y:</b> %2, <b>Z:</b> %3";
375 		QString temeCoords = QString(xyz)
376 			.arg(qRound(position[0]))
377 			.arg(qRound(position[1]))
378 			.arg(qRound(position[2]));
379 		// TRANSLATORS: TEME (True Equator, Mean Equinox) is an Earth-centered inertial coordinate system
380 		oss << QString("%1: %2 %3").arg(q_("TEME coordinates")).arg(temeCoords).arg(qc_("km", "distance")) << "<br/>";
381 
382 		QString temeVel = QString(xyz)
383 		        .arg(velocity[0], 5, 'f', 2)
384 		        .arg(velocity[1], 5, 'f', 2)
385 		        .arg(velocity[2], 5, 'f', 2);
386 		// TRANSLATORS: TEME (True Equator, Mean Equinox) is an Earth-centered inertial coordinate system
387 		oss << QString("%1: %2 %3").arg(q_("TEME velocity"), temeVel, qc_("km/s", "speed")) << "<br/>";
388 
389 		QString pha = StelApp::getInstance().getFlagShowDecimalDegrees() ?
390 				StelUtils::radToDecDegStr(phaseAngle,4,false,true) :
391 				StelUtils::radToDmsStr(phaseAngle, true);
392 		oss << QString("%1: %2").arg(q_("Phase angle"), pha) << "<br />";
393 
394 #if (SATELLITES_PLUGIN_IRIDIUM == 1)
395 		if (sunReflAngle>0)
396 		{  // Iridium
397 			oss << QString("%1: %2%3").arg(q_("Sun reflection angle"))
398 			       .arg(sunReflAngle,0,'f',1)
399 			       .arg(degree);
400 			oss << "<br />";
401 		}
402 #endif
403 		QString updDate;
404 		if (!lastUpdated.isValid())
405 			updDate = qc_("unknown", "unknown date");
406 		else
407 		{
408 			QDate sd = lastUpdated.date();
409 			double hours = lastUpdated.time().hour() + lastUpdated.time().minute()/60. + lastUpdated.time().second()/3600.;
410 			updDate = QString("%1 %2 %3 %4 %5").arg(sd.day())
411 					.arg(StelLocaleMgr::longGenitiveMonthName(sd.month())).arg(sd.year())
412 					.arg(qc_("at","at time")).arg(StelUtils::hoursToHmsStr(hours, true));
413 		}
414 		oss << QString("%1: %2").arg(q_("Last updated TLE"), updDate) << "<br />";
415 		oss << QString("%1: %2").arg(q_("Epoch of the TLE"), tleEpoch) << "<br />";
416 		if (RCS>0.)
417 			oss << QString("%1: %2 %3<sup>2</sup>").arg(q_("Radar cross-section (RCS)")).arg(QString::number(RCS, 'f', 3)).arg(qc_("m","distance")) << "<br />";
418 
419 		// Groups of the artificial satellites
420 		QStringList groupList;
421 		for (const auto&g : groups)
422 			groupList << q_(g);
423 
424 		if (!groupList.isEmpty())
425 		{
426 			QString group = groups.count()>1 ? q_("Group") : q_("Groups");
427 			oss << QString("%1: %2").arg(group, groupList.join(", ")) << "<br />";
428 		}
429 
430 		if (status!=StatusUnknown)
431 			oss << QString("%1: %2").arg(q_("Operational status"), getOperationalStatus()) << "<br />";
432 		//Visibility: Full text
433 		oss << q_(visibilityDescription.value(visibility, "")) << "<br />";
434 
435 		if (comms.size() > 0)
436 		{
437 			oss << q_("Radio communication") << ":<br/>";
438 			for (const auto& c : comms)
439 			{
440 				double dop = getDoppler(c.frequency);
441 				double ddop = dop;
442 				QString sign;
443 				if (dop<0.)
444 				{
445 					sign='-';
446 					ddop*=-1;
447 				}
448 				else
449 					sign='+';
450 
451 				if (!c.modulation.isEmpty() && c.modulation != "") oss << "  " << c.modulation;
452 				if (!c.description.isEmpty() && c.description != "") oss << "  " << c.description;
453 				if ((!c.modulation.isEmpty() && c.modulation != "") || (!c.description.isEmpty() && c.description != "")) oss << ": ";
454 				oss << QString("%1 %2 (%3%4 %5)").arg(QString::number(c.frequency, 'f', 3), qc_("MHz", "frequency"), sign, QString::number(ddop, 'f', 3), qc_("kHz", "frequency"));
455 				oss << "<br/>";
456 			}
457 		}
458 	}
459 
460 	postProcessInfoString(str, flags);
461 	return str;
462 }
463 
464 // Calculate perigee and apogee altitudes for mean Earth radius
calculateSatDataFromLine2(QString tle)465 void Satellite::calculateSatDataFromLine2(QString tle)
466 {
467 	// Details: http://www.satobs.org/seesat/Dec-2002/0197.html
468 	const double meanEarthRadius = 6371.0088;
469 	const double k = 8681663.653;
470 	const double meanMotion = tle.left(63).right(11).toDouble();
471 	const double semiMajorAxis = std::cbrt((k/meanMotion)*(k/meanMotion));
472 	eccentricity = QString("0.%1").arg(tle.left(33).right(7)).toDouble();
473 	perigee = semiMajorAxis*(1.0 - eccentricity) - meanEarthRadius;
474 	apogee = semiMajorAxis*(1.0 + eccentricity) - meanEarthRadius;
475 	inclination = QString(tle.left(16).right(8)).toDouble();
476 }
477 
478 // Calculate epoch of TLE
calculateEpochFromLine1(QString tle)479 void Satellite::calculateEpochFromLine1(QString tle)
480 {
481 	QString epochStr;
482 	// Details: https://celestrak.com/columns/v04n03/ or https://en.wikipedia.org/wiki/Two-line_element_set
483 	int year = tle.left(20).right(2).toInt();
484 	if (year>=0 && year<57)
485 		year += 2000;
486 	else
487 		year += 1900;
488 	const double dayOfYear = tle.left(32).right(12).toDouble();
489 	QDate epoch = QDate(year, 1, 1).addDays(dayOfYear - 1);
490 	if (!epoch.isValid())
491 		epochStr = qc_("unknown", "unknown date");
492 	else
493 		epochStr = QString("%1 %2 %3, %4 UTC").arg(epoch.day())
494 				.arg(StelLocaleMgr::longGenitiveMonthName(epoch.month())).arg(year)
495 				.arg(StelUtils::hoursToHmsStr(24.*(dayOfYear-static_cast<int>(dayOfYear)), true));
496 
497 	tleEpoch = epochStr;
498 	tleEpochJD = epoch.toJulianDay();
499 }
500 
getInfoMap(const StelCore * core) const501 QVariantMap Satellite::getInfoMap(const StelCore *core) const
502 {
503 	QVariantMap map = StelObject::getInfoMap(core);
504 
505 	map.insert("description", QString(description).replace("\n", " - "));
506 	map.insert("catalog", id);
507 	map.insert("tle1", tleElements.first.data());
508 	map.insert("tle2", tleElements.second.data());
509 	map.insert("tle-epoch", tleEpoch);
510 
511 	if (!internationalDesignator.isEmpty())
512 		map.insert("international-designator", internationalDesignator);
513 
514 	if (stdMag>98.) // replace whatever has been computed
515 	{
516 		map.insert("vmag", "?");
517 		map.insert("vmage", "?");
518 	}
519 
520 	map.insert("range", range);
521 	map.insert("rangerate", rangeRate);
522 	map.insert("height", height);
523 	map.insert("subpoint-lat", latLongSubPointPosition[0]);
524 	map.insert("subpoint-long", latLongSubPointPosition[1]);
525 	map.insert("TEME-km-X", position[0]);
526 	map.insert("TEME-km-Y", position[1]);
527 	map.insert("TEME-km-Z", position[2]);
528 	map.insert("TEME-speed-X", velocity[0]);
529 	map.insert("TEME-speed-Y", velocity[1]);
530 	map.insert("TEME-speed-Z", velocity[2]);
531 	map.insert("inclination", pSatWrapper->getOrbitalInclination());
532 	map.insert("period", pSatWrapper->getOrbitalPeriod());
533 	map.insert("perigee-altitude", perigee);
534 	map.insert("apogee-altitude", apogee);
535 #if (SATELLITES_PLUGIN_IRIDIUM == 1)
536 	if (sunReflAngle>0.)
537 	{  // Iridium
538 		map.insert("sun-reflection-angle", sunReflAngle);
539 	}
540 #endif
541 	map.insert("operational-status", getOperationalStatus());
542 	map.insert("phase-angle", phaseAngle);
543 	map.insert("phase-angle-dms", StelUtils::radToDmsStr(phaseAngle));
544 	map.insert("phase-angle-deg", StelUtils::radToDecDegStr(phaseAngle));
545 	map.insert("visibility", visibilityDescription.value(visibility, ""));
546 	if (comms.size() > 0)
547 	{
548 		for (const auto& c : comms)
549 		{
550 			double dop = getDoppler(c.frequency);
551 			double ddop = dop;
552 			char sign;
553 			if (dop<0.)
554 			{
555 				sign='-';
556 				ddop*=-1;
557 			}
558 			else
559 				sign='+';
560 
561 			QString commModDesc;
562 			if (!c.modulation.isEmpty() && c.modulation != "") commModDesc=c.modulation;
563 			if ((!c.modulation.isEmpty() && c.modulation != "") || (!c.description.isEmpty() && c.description != "")) commModDesc.append(" ");
564 			if (!c.description.isEmpty() && c.description != "") commModDesc.append(c.description);
565 			if ((!c.modulation.isEmpty() && c.modulation != "") || (!c.description.isEmpty() && c.description != "")) commModDesc.append(": ");
566 			map.insertMulti("comm", QString("%1%2 MHz (%3%4 kHz)")
567 				.arg(commModDesc)
568 				.arg(c.frequency, 8, 'f', 5)
569 				.arg(sign)
570 				.arg(ddop, 6, 'f', 3));
571 		}
572 	}
573 
574 	return map;
575 }
576 
getJ2000EquatorialPos(const StelCore * core) const577 Vec3d Satellite::getJ2000EquatorialPos(const StelCore* core) const
578 {
579 	// Bugfix LP:1654331. I assume the elAzPosition has been computed without refraction! We must say this definitely.
580 	return core->altAzToJ2000(elAzPosition, StelCore::RefractionOff);
581 }
582 
getInfoColor(void) const583 Vec3f Satellite::getInfoColor(void) const
584 {
585 	return infoColor;
586 }
587 
getVMagnitude(const StelCore * core) const588 float Satellite::getVMagnitude(const StelCore* core) const
589 {
590 	Q_UNUSED(core)
591 	float vmag = 7.f; // Optimistic value of magnitude for artificial satellite without data for standard magnitude
592 	if (iconicModeFlag)
593 		vmag = 5.0;
594 
595 	if (!iconicModeFlag && visibility != gSatWrapper::VISIBLE)
596 		vmag = 17.f; // Artificial satellite is invisible and 17 is hypothetical value of magnitude
597 
598 	if (visibility==gSatWrapper::VISIBLE)
599 	{
600 #if(SATELLITES_PLUGIN_IRIDIUM == 1)
601 		sunReflAngle = -1.;
602 #endif
603 		if (pSatWrapper && name.startsWith("STARLINK"))
604 		{
605 			// Calculation of approx. visual magnitude for Starlink satellites
606 			// described here: http://www.satobs.org/seesat/Aug-2020/0079.html
607 			vmag = static_cast<float>(5.93 + 5 * std::log10 ( range / 1000 ));
608 			if (name.contains("DARKSAT", Qt::CaseInsensitive)) // See https://arxiv.org/abs/2006.08422
609 				vmag *= 0.78f;
610 		}
611 		else if (stdMag<99.) // OK, artificial satellite has value for standard magnitude
612 		{
613 			// Calculation of approx. visual magnitude for artificial satellites
614 			// described here: http://www.prismnet.com/~mmccants/tles/mccdesc.html
615 			double fracil = calculateIlluminatedFraction();
616 			if (fracil==0)
617 				fracil = 0.000001;
618 
619 #if(SATELLITES_PLUGIN_IRIDIUM == 1)
620 			if (pSatWrapper && name.startsWith("IRIDIUM"))
621 			{
622 				Vec3d Sun3d = pSatWrapper->getSunECIPos();
623 				QVector3D sun(Sun3d.data()[0],Sun3d.data()[1],Sun3d.data()[2]);
624 				QVector3D sunN = sun; sunN.normalize();
625 
626 				// position, velocity are known
627 				QVector3D Vx(velocity.data()[0],velocity.data()[1],velocity.data()[2]); Vx.normalize();
628 
629 				Vec3d vy = (position^velocity);
630 				QVector3D Vy(vy.data()[0],vy.data()[1],vy.data()[2]); Vy.normalize();
631 				QVector3D Vz = QVector3D::crossProduct(Vx,Vy); Vz.normalize();
632 
633 				// move this to constructor for optimizing
634 				QMatrix4x4 m0;
635 				m0.rotate(40, Vy);
636 				QVector3D Vx0 = m0.mapVector(Vx);
637 
638 				QMatrix4x4 m[3];
639 				//m[2] = m[1] = m[0];
640 				m[0].rotate(0, Vz);
641 				m[1].rotate(120, Vz);
642 				m[2].rotate(-120, Vz);
643 
644 				StelLocation loc   = StelApp::getInstance().getCore()->getCurrentLocation();
645 				const double  radLatitude    = loc.latitude * KDEG2RAD;
646 				const double  theta          = pSatWrapper->getEpoch().toThetaLMST(loc.longitude * KDEG2RAD);
647 				const double sinRadLatitude=sin(radLatitude);
648 				const double cosRadLatitude=cos(radLatitude);
649 				const double sinTheta=sin(theta);
650 				const double cosTheta=cos(theta);
651 
652 				Vec3d observerECIPos;
653 				Vec3d observerECIVel;
654 				pSatWrapper->calcObserverECIPosition(observerECIPos, observerECIVel);
655 
656 				sunReflAngle = 180.;
657 				QVector3D mirror;
658 				for (int i = 0; i<3; i++)
659 				{
660 					mirror = m[i].mapVector(Vx0);
661 					mirror.normalize();
662 
663 					// reflection R = 2*(V dot N)*N - V
664 					QVector3D rsun =  2*QVector3D::dotProduct(sun,mirror)*mirror - sun;
665 					rsun = -rsun;
666 					Vec3d rSun(rsun.x(),rsun.y(),rsun.z());
667 
668 					//Vec3d satECIPos  = getTEMEPos();
669 					Vec3d slantRange = rSun - observerECIPos;
670 					Vec3d topoRSunPos;
671 					//top_s
672 					topoRSunPos[0] = (sinRadLatitude * cosTheta * slantRange[0]
673 							+ sinRadLatitude * sinTheta * slantRange[1]
674 							- cosRadLatitude * slantRange[2]);
675 					//top_e
676 					topoRSunPos[1] = ((-1.0) * sinTheta * slantRange[0]
677 							+ cosTheta * slantRange[1]);
678 
679 					//top_z
680 					topoRSunPos[2] = (cosRadLatitude * cosTheta * slantRange[0]
681 							+ cosRadLatitude * sinTheta * slantRange[1]
682 							+ sinRadLatitude * slantRange[2]);
683 					sunReflAngle = qMin(elAzPosition.angle(topoRSunPos) * KRAD2DEG, sunReflAngle) ;
684 				}
685 
686 				// very simple flare model
687 				double iridiumFlare = 100;
688 				if (sunReflAngle<0.5)
689 					iridiumFlare = -8.92 + sunReflAngle*6;
690 				else	if (sunReflAngle<0.7)
691 					iridiumFlare = -5.92 + (sunReflAngle-0.5)*10;
692 				else
693 					iridiumFlare = -3.92 + (sunReflAngle-0.7)*5;
694 
695 				 vmag = qMin(stdMag, iridiumFlare);
696 			}
697 			else // not Iridium
698 #endif
699 				vmag = stdMag;
700 
701 			vmag = static_cast<float>(vmag - 15.75 + 2.5 * std::log10(range * range / fracil));
702 		}
703 		else if (RCS>0.) // OK, artificial satellite has RCS value and no standard magnitude
704 		{
705 			// Let's try calculate approx. magnitude from RCS value (see DOI: 10.1117/12.2014623)
706 			double albedo = 0.2;
707 			if (0.436<=phaseAngle && phaseAngle<1.745)
708 				albedo = (((((3.1765*phaseAngle - 22.0968)*phaseAngle + 62.182)*phaseAngle - 90.0993)*phaseAngle + 70.3031)*phaseAngle - 27.9227)*phaseAngle + 4.7373;
709 			else if (1.745<=phaseAngle && phaseAngle<=2.618)
710 				albedo = ((0.510905*phaseAngle - 2.72607)*phaseAngle + 4.96646)*phaseAngle - 3.02085;
711 
712 			double rm = range*1000.;
713 			double fdiff = (std::sin(phaseAngle) + (M_PI - phaseAngle)*std::cos(phaseAngle))*(2.*albedo*RCS)/(3.* M_PI * M_PI * rm * rm);
714 
715 			vmag = static_cast<float>(-26.74 - 2.5*std::log10(fdiff));
716 		}
717 	}
718 	return vmag;
719 }
720 
721 // Calculate illumination fraction of artificial satellite
calculateIlluminatedFraction() const722 float Satellite::calculateIlluminatedFraction() const
723 {
724 	return (1.f + cos(static_cast<float>(phaseAngle)))*0.5f;
725 }
726 
getOperationalStatus() const727 QString Satellite::getOperationalStatus() const
728 {
729 	const QMap<int,QString>map={
730 		{ StatusOperational,          qc_("operational", "operational status")},
731 		{ StatusNonoperational,       qc_("non-operational", "operational status")},
732 		{ StatusPartiallyOperational, qc_("partially operational", "operational status")},
733 		{ StatusStandby,              qc_("standby", "operational status")},
734 		{ StatusSpare,                qc_("spare", "operational status")},
735 		{ StatusExtendedMission,      qc_("extended mission", "operational status")},
736 		{ StatusDecayed,              qc_("decayed", "operational status")},
737 	};
738 	return map.value(status,              qc_("unknown", "operational status"));
739 }
740 
getAngularSize(const StelCore *) const741 double Satellite::getAngularSize(const StelCore*) const
742 {
743 	if (RCS>0.)
744 	{
745 		double size = std::sqrt(4*RCS/M_PI); // Let's use spherical satellites
746 		if (isISS)
747 			size = 109.; // Special case: let's use max. size of ISS (109 meters: https://www.nasa.gov/feature/facts-and-figures)
748 		return 2.* std::atan(size/(2000.*range))*M_180_PI; // Computing an angular size of artificial satellite ("size" in meters, "range" in kilometres, so, 2000 is equal 1000*2)
749 	}
750 	else
751 		return 0.00001;
752 }
753 
setNewTleElements(const QString & tle1,const QString & tle2)754 void Satellite::setNewTleElements(const QString& tle1, const QString& tle2)
755 {
756 	if (pSatWrapper)
757 	{
758 		gSatWrapper *old = pSatWrapper;
759 		pSatWrapper = Q_NULLPTR;
760 		delete old;
761 	}
762 
763 	tleElements.first = tle1.toUtf8();
764 	tleElements.second = tle2.toUtf8();
765 
766 	pSatWrapper = new gSatWrapper(id, tle1, tle2);
767 	orbitPoints.clear();
768 	visibilityPoints.clear();
769 
770 	parseInternationalDesignator(tle1);
771 	calculateEpochFromLine1(tle1);
772 	calculateSatDataFromLine2(tle2);
773 }
774 
recomputeSatData()775 void Satellite::recomputeSatData()
776 {
777 	calculateEpochFromLine1(tleElements.first.data());
778 	calculateSatDataFromLine2(tleElements.second.data());
779 }
780 
update(double)781 void Satellite::update(double)
782 {
783 	if (pSatWrapper && orbitValid)
784 	{
785 		StelCore* core = StelApp::getInstance().getCore();
786 		epochTime = core->getJD(); // + timeShift; // We have "true" JD (UTC) from core, satellites don't need JDE!
787 
788 		pSatWrapper->setEpoch(epochTime);
789 		position                 = pSatWrapper->getTEMEPos();
790 		velocity                 = pSatWrapper->getTEMEVel();
791 		latLongSubPointPosition  = pSatWrapper->getSubPoint();
792 		height                   = latLongSubPointPosition[2]; // km
793 		if (height < 70.0)
794 		{
795 			// The orbit is no longer valid.  Causes include very out of date
796 			// TLE, system date and time out of a reasonable range, and orbital
797 			// degradation and re-entry of a satellite.  In any of these cases
798 			// we might end up with a problem - usually a crash of Stellarium
799 			// because of a div/0 or something.  To prevent this, we turn off
800 			// the satellite when the computed height is 70km.
801 			// Low Earth Orbit (LEO):
802 			// A geocentric orbit with an altitude much less than the Earth's radius.
803 			// Satellites in this orbit are between 80 and 2000 kilometres above
804 			// the Earth's surface.
805 			// Source: https://www.nasa.gov/directorates/heo/scan/definitions/glossary/index.html#L
806 			qWarning() << "Satellite has invalid orbit:" << name << id;
807 			orbitValid = false;
808 			displayed = false; // It shouldn't be displayed!
809 			return;
810 		}
811 
812 		elAzPosition = pSatWrapper->getAltAz();
813 		elAzPosition.normalize();
814 		XYZ = getJ2000EquatorialPos(core);
815 
816 		pSatWrapper->getSlantRange(range, rangeRate);
817 		visibility = pSatWrapper->getVisibilityPredict();
818 		phaseAngle = pSatWrapper->getPhaseAngle();
819 
820 		// Compute orbit points to draw orbit line.
821 		if (orbitDisplayed) computeOrbitPoints();
822 	}
823 }
824 
getDoppler(double freq) const825 double Satellite::getDoppler(double freq) const
826 {
827 	return  -freq*((rangeRate*1000.0)/SPEED_OF_LIGHT);
828 }
829 
recalculateOrbitLines(void)830 void Satellite::recalculateOrbitLines(void)
831 {
832 	orbitPoints.clear();
833 	visibilityPoints.clear();
834 }
835 
getFlags() const836 SatFlags Satellite::getFlags() const
837 {
838 	// There's also a faster, but less readable way: treating them as uint.
839 	SatFlags flags;
840 	double orbitalPeriod = pSatWrapper->getOrbitalPeriod();
841 	if (displayed)
842 		flags |= SatDisplayed;
843 	else
844 		flags |= SatNotDisplayed;
845 	if (orbitDisplayed)
846 		flags |= SatOrbit;
847 	if (userDefined)
848 		flags |= SatUser;
849 	if (newlyAdded)
850 		flags |= SatNew;
851 	if (!orbitValid)
852 		flags |= SatError;
853 	if (RCS>0. && RCS <= 0.1)
854 		flags |= SatSmallSize;
855 	if (RCS>0.1 && RCS <= 1.0)
856 		flags |= SatMediumSize;
857 	if (RCS>1.0)
858 		flags |= SatLargeSize;
859 	if (eccentricity < 0.25 && (inclination>=0. && inclination<=180.) && apogee<4400.)
860 		flags |= SatLEO;
861 	if (eccentricity < 0.25 && inclination<25. && (orbitalPeriod>=1100. && orbitalPeriod<=2000.))
862 		flags |= SatGSO;
863 	if (eccentricity < 0.25 && (inclination>=0. && inclination<=180.) && apogee>=4400. && orbitalPeriod<1100.)
864 		flags |= SatMEO;
865 	if (eccentricity >= 0.25 && (inclination>=0. && inclination<=180.) && perigee<=70000. && orbitalPeriod<=14000.)
866 		flags |= SatHEO;
867 	if (eccentricity < 0.25 && (inclination>=25. && inclination<=180.) && (orbitalPeriod>=1100. && orbitalPeriod<=2000.))
868 		flags |= SatHGSO;
869 	if (qAbs(StelApp::getInstance().getCore()->getJD() - tleEpochJD) > tleEpochAge)
870 		flags |= SatOutdatedTLE;
871 	return flags;
872 }
873 
setFlags(const SatFlags & flags)874 void Satellite::setFlags(const SatFlags& flags)
875 {
876 	displayed = flags.testFlag(SatDisplayed);
877 	orbitDisplayed = flags.testFlag(SatOrbit);
878 	userDefined = flags.testFlag(SatUser);
879 }
880 
881 
parseInternationalDesignator(const QString & tle1)882 void Satellite::parseInternationalDesignator(const QString& tle1)
883 {
884 	Q_ASSERT(!tle1.isEmpty());
885 
886 	// The designator is encoded in chunk 3 on the first line.
887 	QStringList tleData = tle1.split(" ");
888 	QString rawString = tleData.at(2);
889 	bool ok;
890 	int year = rawString.left(2).toInt(&ok);
891 	if (!rawString.isEmpty() && ok)
892 	{
893 		// Y2K bug :) I wonder what NORAD will do in 2057. :)
894 		if (year < 57)
895 			year += 2000;
896 		else
897 			year += 1900;
898 		internationalDesignator = QString::number(year) + "-" + rawString.mid(2);
899 	}
900 	else
901 		year = 1957;
902 
903 	StelUtils::getJDFromDate(&jdLaunchYearJan1, year, 1, 1, 0, 0, 0);
904 }
905 
operator <(const Satellite & another) const906 bool Satellite::operator <(const Satellite& another) const
907 {
908 	// If interface strings are used, you'll need QString::localeAwareCompare()
909 	int comp = name.compare(another.name);
910 	if (comp < 0)
911 		return true;
912 	if (comp > 0)
913 		return false;
914 
915 	// If the names are the same, compare IDs, i.e. NORAD numbers.
916 	return (id < another.id);
917 }
918 
draw(StelCore * core,StelPainter & painter)919 void Satellite::draw(StelCore* core, StelPainter& painter)
920 {
921 	// Separated because first test should be very fast.
922 	if (!displayed)
923 		return;
924 
925 	// 1) Do not show satellites before Space Era begins!
926 	// 2) Do not show satellites when time rate is over limit (JD/sec)!
927 	if (core->getJD()<jdLaunchYearJan1 || qAbs(core->getTimeRate())>=timeRateLimit)
928 		return;
929 
930 	Vec3d win;
931 	if (painter.getProjector()->projectCheck(XYZ, win))
932 	{
933 		if (!iconicModeFlag)
934 		{
935 			Vec3f color(1.f,1.f,1.f);
936 			// Special case: crossing of the satellite of the Moon or the Sun
937 			if (XYZ.angle(moon->getJ2000EquatorialPos(core))*M_180_PI <= moon->getSpheroidAngularSize(core) || XYZ.angle(sun->getJ2000EquatorialPos(core))*M_180_PI <= sun->getSpheroidAngularSize(core))
938 			{
939 				painter.setColor(transitSatelliteColor, 1.f);
940 				int screenSizeSat = static_cast<int>((getAngularSize(core)*M_PI_180)*painter.getProjector()->getPixelPerRadAtCenter());
941 				if (screenSizeSat>0)
942 				{
943 					painter.setBlending(true, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
944 					hintTexture->bind();
945 					painter.drawSprite2dMode(XYZ, qMin(screenSizeSat, 15));
946 				}
947 
948 				if (showLabels)
949 				{
950 					if (!core->isBrightDaylight()) // crossing of the Moon
951 						painter.setColor(color, hintBrightness);
952 					painter.drawText(XYZ, name, 0, 10, 10, false);
953 				}
954 			}
955 			else
956 			{
957 				const float magSat = getVMagnitude(core);
958 				StelSkyDrawer* sd = core->getSkyDrawer();
959 				RCMag rcMag;
960 
961 				// Draw the satellite
962 				if (magSat <= sd->getLimitMagnitude())
963 				{
964 					Vec3f vf(XYZ.toVec3f());
965 					Vec3f altAz(vf);
966 					altAz.normalize();
967 					core->j2000ToAltAzInPlaceNoRefraction(&altAz);
968 					sd->preDrawPointSource(&painter);
969 					sd->computeRCMag(magSat, &rcMag);
970 					// allow height-dependent twinkle and suppress twinkling in higher altitudes. Keep 0.1 twinkle amount in zenith.
971 					sd->drawPointSource(&painter, vf, rcMag, color*hintBrightness, true, qMin(1.0f, 1.0f-0.9f*altAz[2]));
972 					sd->postDrawPointSource(&painter);
973 				}
974 
975 				float txtMag = magSat;
976 				if (visibility != gSatWrapper::VISIBLE)
977 				{
978 					txtMag = magSat - 10.f; // Oops... Artificial satellite is invisible, but let's make the label visible
979 					painter.setColor(invisibleSatelliteColor, hintBrightness);
980 				}
981 				else
982 					painter.setColor(color, hintBrightness);
983 
984 				// Draw the label of the satellite when it enabled
985 				if (txtMag <= sd->getLimitMagnitude() && showLabels)
986 					painter.drawText(XYZ, name, 0, 10, 10, false);
987 
988 			}
989 		}
990 		else if (!(hideInvisibleSatellitesFlag && visibility != gSatWrapper::VISIBLE))
991 		{
992 			Vec3f drawColor = (visibility == gSatWrapper::VISIBLE) ? hintColor : invisibleSatelliteColor; // Use hintColor for visible satellites only
993 			painter.setColor(drawColor*hintBrightness, hintBrightness);
994 			if (XYZ.angle(moon->getJ2000EquatorialPos(core))*M_180_PI <= moon->getSpheroidAngularSize(core) || XYZ.angle(sun->getJ2000EquatorialPos(core))*M_180_PI <= sun->getSpheroidAngularSize(core))
995 				painter.setColor(transitSatelliteColor, 1.f);
996 
997 			if (showLabels)
998 				painter.drawText(XYZ, name, 0, 10, 10, false);
999 
1000 			painter.setBlending(true, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
1001 			hintTexture->bind();
1002 			painter.drawSprite2dMode(XYZ, 11);
1003 		}
1004 	}
1005 
1006 	if (orbitDisplayed && Satellite::orbitLinesFlag && orbitValid)
1007 		drawOrbit(core, painter);
1008 }
1009 
drawOrbit(StelCore * core,StelPainter & painter)1010 void Satellite::drawOrbit(StelCore *core, StelPainter& painter)
1011 {
1012 	Vec3d position, onscreen;
1013 	Vec3f drawColor;
1014 	int size = orbitPoints.size();
1015 
1016 	QVector<Vec3d> vertexArray;
1017 	QVector<Vec4f> colorArray;
1018 	StelProjectorP prj = painter.getProjector();
1019 
1020 	vertexArray.resize(size);
1021 	colorArray.resize(size);
1022 
1023 	//Rest of points
1024 	for (int i=1; i<size; i++)
1025 	{
1026 		position = core->altAzToJ2000(orbitPoints[i].toVec3d(), StelCore::RefractionOff);
1027 		position.normalize();
1028 		if (prj->project(position, onscreen)) // check position on the screen
1029 		{
1030 			vertexArray.append(position);
1031 			drawColor = (visibilityPoints[i] == gSatWrapper::VISIBLE) ? orbitColor : invisibleSatelliteColor;
1032 			if (hideInvisibleSatellitesFlag && visibilityPoints[i] != gSatWrapper::VISIBLE)
1033 				colorArray.append(Vec4f(0.f,0.f,0.f,0.f)); // hide invisible part of orbit
1034 			else
1035 				colorArray.append(Vec4f(drawColor, hintBrightness * calculateOrbitSegmentIntensity(i)));
1036 		}
1037 	}
1038 	painter.drawPath(vertexArray, colorArray); // (does client state switching as needed internally)
1039 }
1040 
calculateOrbitSegmentIntensity(int segNum)1041 float Satellite::calculateOrbitSegmentIntensity(int segNum)
1042 {
1043 	int endDist = (orbitLineSegments/2) - abs(segNum-1 - (orbitLineSegments/2) % orbitLineSegments);
1044 	if (endDist > orbitLineFadeSegments)
1045 	{
1046 		return 1.0;
1047 	}
1048 	else
1049 	{
1050 		return (endDist  + 1) / (orbitLineFadeSegments + 1.0);
1051 	}
1052 }
1053 
computeOrbitPoints()1054 void Satellite::computeOrbitPoints()
1055 {
1056 	gTimeSpan computeInterval(0, 0, 0, orbitLineSegmentDuration);
1057 	gTimeSpan orbitSpan(0, 0, 0, orbitLineSegments*orbitLineSegmentDuration/2);
1058 	gTime epochTm;
1059 	gTime epoch(epochTime);
1060 	gTime lastEpochComp(lastEpochCompForOrbit);
1061 	int diffSlots;
1062 
1063 	if (orbitPoints.isEmpty())//Setup orbitPoints
1064 	{
1065 		epochTm  = epoch - orbitSpan;
1066 
1067 		for (int i=0; i<=orbitLineSegments; i++)
1068 		{
1069 			pSatWrapper->setEpoch(epochTm.getGmtTm());
1070 			orbitPoints.append(pSatWrapper->getAltAz());
1071 			visibilityPoints.append(pSatWrapper->getVisibilityPredict());
1072 			epochTm    += computeInterval;
1073 		}
1074 		lastEpochCompForOrbit = epochTime;
1075 	}
1076 	else if (epochTime > lastEpochCompForOrbit)
1077 	{
1078 		// compute next orbit point when clock runs forward
1079 		gTimeSpan diffTime = epoch - lastEpochComp;
1080 		diffSlots          = static_cast<int>(diffTime.getDblSeconds()/orbitLineSegmentDuration);
1081 
1082 		if (diffSlots > 0)
1083 		{
1084 			if (diffSlots > orbitLineSegments)
1085 			{
1086 				diffSlots = orbitLineSegments + 1;
1087 				epochTm  = epoch - orbitSpan;
1088 			}
1089 			else
1090 			{
1091 				epochTm   = lastEpochComp + orbitSpan + computeInterval;
1092 			}
1093 
1094 			for (int i=0; i<diffSlots; i++)
1095 			{
1096 				//remove points at beginning of list and add points at end.
1097 				orbitPoints.removeFirst();
1098 				visibilityPoints.removeFirst();
1099 				pSatWrapper->setEpoch(epochTm.getGmtTm());
1100 				orbitPoints.append(pSatWrapper->getAltAz());
1101 				visibilityPoints.append(pSatWrapper->getVisibilityPredict());
1102 				epochTm    += computeInterval;
1103 			}
1104 
1105 			lastEpochCompForOrbit = epochTime;
1106 		}
1107 	}
1108 	else if (epochTime < lastEpochCompForOrbit)
1109 	{
1110 		// compute next orbit point when clock runs backward
1111 		gTimeSpan diffTime = lastEpochComp - epoch;
1112 		diffSlots          = static_cast<int>(diffTime.getDblSeconds()/orbitLineSegmentDuration);
1113 
1114 		if (diffSlots > 0)
1115 		{
1116 			if (diffSlots > orbitLineSegments)
1117 			{
1118 				diffSlots = orbitLineSegments + 1;
1119 				epochTm   = epoch + orbitSpan;
1120 			}
1121 			else
1122 			{
1123 				epochTm   = epoch - orbitSpan - computeInterval;
1124 			}
1125 			for (int i=0; i<diffSlots; i++)
1126 			{ //remove points at end of list and add points at beginning.
1127 				orbitPoints.removeLast();
1128 				visibilityPoints.removeLast();
1129 				pSatWrapper->setEpoch(epochTm.getGmtTm());
1130 				orbitPoints.push_front(pSatWrapper->getAltAz());
1131 				visibilityPoints.push_front(pSatWrapper->getVisibilityPredict());
1132 				epochTm -= computeInterval;
1133 			}
1134 			lastEpochCompForOrbit = epochTime;
1135 		}
1136 	}
1137 }
1138 
operator <(const SatelliteP & left,const SatelliteP & right)1139 bool operator <(const SatelliteP& left, const SatelliteP& right)
1140 {
1141 	if (left.isNull())
1142 	{
1143 		if (right.isNull())
1144 			return false;
1145 		else
1146 			return true;
1147 	}
1148 	if (right.isNull())
1149 		return false; // No sense to check the left one now
1150 
1151 	return ((*left) < (*right));
1152 }
1153