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 — %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