1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-22 08:04:49 -05:00
sdrangel/plugins/feature/satellitetracker/satellitetrackersgp4.cpp
srcejon 24d80f8ab0 ADS-B: Allow plugin to be compiled as static lib. Allow building without texttospeech, location or process.
VOR localizer: Allow plugin to be compiled as static lib.
SID feature: Allow plugin to be compiled as static lib.
Satellite Tracker: Allow building without texttospeech or timezone.
Map: Allow plugin to be compiled as static lib. Allow building without SSL.
APT: Allow plugin to be compiled as static lib.
Radio Astronomy: Allow plugin to be compiled as static lib. Allow building without process.
ChannelWebAPIUtils: Fix memory leaks.
DeviceSampleSource: Add signals that indicate when the position or direction of a device (or it's antenna) has changed. This is to support per device position, which is useful for remote devices not at My Position.
2024-09-17 09:47:45 +01:00

540 lines
20 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <cmath>
#include <CoordTopocentric.h>
#include <CoordGeodetic.h>
#include <Observer.h>
#include <SGP4.h>
#include <QTimeZone>
#include "util/units.h"
#include "satellitetrackersgp4.h"
using namespace libsgp4;
// Convert QGP4 DateTime to Qt QDataTime
static QDateTime dateTimeToQDateTime(DateTime dt)
{
QDateTime qdt(QDate(dt.Year(), dt.Month(), dt.Day()), QTime(dt.Hour(), dt.Minute(), dt.Second(), (int)(dt.Microsecond()/1000.0)), Qt::UTC);
return qdt;
}
// Convert Qt QDataTime to QGP4 DateTime
static DateTime qDateTimeToDateTime(QDateTime qdt)
{
QDateTime utc = qdt.toUTC();
QDate date = utc.date();
QTime time = utc.time();
DateTime dt;
dt.Initialise(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second(), time.msec() * 1000);
return dt;
}
// Get ground track
// Throws SatelliteException, DecayedException and TleException
void getGroundTrack(QDateTime dateTime,
const QString& tle0, const QString& tle1, const QString& tle2,
int steps, bool forward,
QList<QGeoCoordinate *>& coordinates,
QList<QDateTime *>& coordinateDateTimes)
{
Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString());
SGP4 sgp4(tle);
OrbitalElements ele(tle);
double periodMins;
double timeStep;
// For 3D map, we want to quantize to minutes, so we replace previous
// position data, rather than insert additional positions alongside the old
// which can result is the camera view jumping around
#if QT_CONFIG(timezone)
dateTime = QDateTime(dateTime.date(), QTime(dateTime.time().hour(), dateTime.time().minute()), dateTime.timeZone());
#else
dateTime = QDateTime(dateTime.date(), QTime(dateTime.time().hour(), dateTime.time().minute()));
#endif
// Note 2D map doesn't support paths wrapping around Earth several times
// So we just have a slight overlap here, with the future track being longer
DateTime currentTime = qDateTimeToDateTime(dateTime);
DateTime endTime;
if (forward)
{
periodMins = ele.Period() * 0.9;
endTime = currentTime.AddMinutes(periodMins);
timeStep = periodMins / (steps * 0.9);
}
else
{
periodMins = ele.Period() * 0.4;
endTime = currentTime.AddMinutes(-periodMins);
timeStep = -periodMins / (steps * 0.4);
}
// Quantize time step to 30 seconds
timeStep *= 2.0;
if (timeStep > 0.0) {
timeStep = std::max(timeStep, 1.0);
} else if (timeStep < 0.0) {
timeStep = std::min(timeStep, -1.0);
}
timeStep = round(timeStep);
timeStep /= 2.0;
while ((forward && (currentTime < endTime)) || (!forward && (currentTime > endTime)))
{
// Calculate satellite position
Eci eci = sgp4.FindPosition(currentTime);
// Convert satellite position to geodetic coordinates (lat and long)
CoordGeodetic geo = eci.ToGeodetic();
QGeoCoordinate *coord = new QGeoCoordinate(Units::radiansToDegrees(geo.latitude),
Units::radiansToDegrees(geo.longitude),
geo.altitude * 1000.0);
coordinates.append(coord);
QDateTime *coordDateTime = new QDateTime(dateTimeToQDateTime(currentTime));
coordinateDateTimes.append(coordDateTime);
// 2D map is stretched at poles, so use finer steps
if (std::abs(Units::radiansToDegrees(geo.latitude)) >= 70)
currentTime = currentTime.AddMinutes(timeStep/4);
else
currentTime = currentTime.AddMinutes(timeStep);
}
}
// Find azimuth and elevation points during a pass
void getPassAzEl(QLineSeries* azimuth, QLineSeries* elevation, QLineSeries* polar,
const QString& tle0, const QString& tle1, const QString& tle2,
double latitude, double longitude, double altitude,
const QDateTime& aos, const QDateTime& los)
{
try
{
Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString());
SGP4 sgp4(tle);
Observer obs(latitude, longitude, altitude);
DateTime aosTime = qDateTimeToDateTime(aos);
DateTime losTime = qDateTimeToDateTime(los);
DateTime currentTime(aosTime);
int steps = 150; // Needs to be high enough, so rotator intersect with satellite position
double timeStep = (losTime - aosTime).TotalSeconds() / steps;
if (timeStep <= 0.0)
{
qDebug() << "getPassAzEl: AOS is the same as or after LOS";
return;
}
while (currentTime <= losTime)
{
// Calculate satellite position
Eci eci = sgp4.FindPosition(currentTime);
// Calculate angle to satellite from antenna
CoordTopocentric topo = obs.GetLookAngle(eci);
// Save azimuth and elevation in series
QDateTime qdt = dateTimeToQDateTime(currentTime);
if (azimuth != nullptr)
azimuth->append(qdt.toMSecsSinceEpoch(), Units::radiansToDegrees(topo.azimuth));
if (elevation != nullptr)
elevation->append(qdt.toMSecsSinceEpoch(), Units::radiansToDegrees(topo.elevation));
if (polar != nullptr)
polar->append(Units::radiansToDegrees(topo.azimuth), 90.0-Units::radiansToDegrees(topo.elevation));
currentTime = currentTime.AddSeconds(timeStep);
}
}
catch (SatelliteException& se)
{
qDebug() << se.what();
}
catch (DecayedException& de)
{
qDebug() << de.what();
}
catch (TleException& tlee)
{
qDebug() << tlee.what();
}
}
// Get whether a pass passes through 0 degrees
bool getPassesThrough0Deg(const QString& tle0, const QString& tle1, const QString& tle2,
double latitude, double longitude, double altitude,
QDateTime& aos, QDateTime& los)
{
try
{
Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString());
SGP4 sgp4(tle);
Observer obs(latitude, longitude, altitude);
DateTime aosTime = qDateTimeToDateTime(aos);
DateTime losTime = qDateTimeToDateTime(los);
DateTime currentTime(aosTime);
int steps = 20;
double timeStep = (losTime - aosTime).TotalSeconds() / steps;
double prevAz;
for (int i = 0; i < steps; i++)
{
// Calculate satellite position
Eci eci = sgp4.FindPosition(currentTime);
// Calculate angle to satellite from antenna
CoordTopocentric topo = obs.GetLookAngle(eci);
double az = Units::radiansToDegrees(topo.azimuth);
if (i == 0)
prevAz = az;
// Does it cross 0 degrees?
if (((prevAz > 270.0) && (az < 90.0)) || ((prevAz < 90.0) && (az >= 270.0)))
return true;
prevAz = az;
currentTime = currentTime.AddSeconds(timeStep);
}
}
catch (SatelliteException& se)
{
qDebug() << se.what();
}
catch (DecayedException& de)
{
qDebug() << de.what();
}
catch (TleException& tlee)
{
qDebug() << tlee.what();
}
return false;
}
// Find maximum elevation in a pass
static double findMaxElevation(Observer& obs1, SGP4& sgp4, const DateTime& aos, const DateTime& los)
{
Observer obs(obs1.GetLocation());
bool running;
double timeStep = (los - aos).TotalSeconds() / 9.0;
DateTime currentTime(aos);
DateTime time1(aos);
DateTime time2(los);
double maxElevation;
do
{
running = true;
maxElevation = -INFINITY;
while (running && (currentTime < time2))
{
Eci eci = sgp4.FindPosition(currentTime);
CoordTopocentric topo = obs.GetLookAngle(eci);
if (topo.elevation > maxElevation)
{
maxElevation = topo.elevation;
currentTime = currentTime.AddSeconds(timeStep);
if (currentTime > time2)
currentTime = time2;
}
else
running = false;
}
time1 = currentTime.AddSeconds(-2.0 * timeStep);
time2 = currentTime;
currentTime = time1;
timeStep = (time2 - time1).TotalSeconds() / 9.0;
}
while (timeStep > 1.0);
return Units::radiansToDegrees(maxElevation);
}
// Find the time at which the satellite crossed the minimum elevation required for AOS or LOS
static DateTime findCrossingPoint(Observer& obs, SGP4& sgp4, const DateTime& initialTime1, const DateTime& initialTime2, double minElevation, bool findingAOS)
{
bool running;
int cnt;
DateTime time1(initialTime1);
DateTime time2(initialTime2);
DateTime middleTime;
running = true;
cnt = 0;
while (running && (cnt++ < 16))
{
middleTime = time1.AddSeconds((time2 - time1).TotalSeconds() / 2.0);
Eci eci = sgp4.FindPosition(middleTime);
CoordTopocentric topo = obs.GetLookAngle(eci);
if (topo.elevation > minElevation)
{
if (findingAOS)
time2 = middleTime;
else
time1 = middleTime;
}
else
{
if (findingAOS)
time1 = middleTime;
else
time2 = middleTime;
}
if ((time2 - time1).TotalSeconds() < 1.0)
{
running = false;
int us = middleTime.Microsecond();
middleTime = middleTime.AddMicroseconds(-us);
middleTime = middleTime.AddSeconds(findingAOS ? 1 : -1);
}
}
running = true;
cnt = 0;
while (running && (cnt++ < 6))
{
Eci eci = sgp4.FindPosition(middleTime);
CoordTopocentric topo = obs.GetLookAngle(eci);
if (topo.elevation > minElevation)
middleTime = middleTime.AddSeconds(findingAOS ? -1 : 1);
else
running = false;
}
return middleTime;
}
// Find when AOS occurred, by stepping backwards
static DateTime findAOSBackwards(Observer& obs, SGP4& sgp4, DateTime& startTime,
int predictionPeriod, double minElevation, bool& aosUnknown)
{
DateTime previousTime(startTime);
DateTime currentTime(startTime);
DateTime endTime(startTime.AddDays(-predictionPeriod));
while (currentTime >= endTime)
{
Eci eci = sgp4.FindPosition(currentTime);
CoordTopocentric topo = obs.GetLookAngle(eci);
if (topo.elevation < minElevation)
{
aosUnknown = false;
return findCrossingPoint(obs, sgp4, currentTime, previousTime, minElevation, true);
}
previousTime = currentTime;
currentTime = currentTime - TimeSpan(0, 0, 180);
}
aosUnknown = true;
return currentTime;
}
bool inPassWindow(DateTime dateTime, QTime passStartTime, QTime passEndTime, bool utc)
{
// Don't compare seconds as not currently settable in GUI
QDateTime qdt = dateTimeToQDateTime(dateTime);
if (!utc)
qdt = qdt.toLocalTime();
QTime qt(qdt.time().hour(), qdt.time().minute());
passStartTime = QTime(passStartTime.hour(), passStartTime.minute());
passEndTime = QTime(passEndTime.hour(), passEndTime.minute());
// If passEndTime is before passStartTime, then we allow overnight passes
if (passEndTime > passStartTime)
{
return (qt >= passStartTime) && (qt <= passEndTime);
}
else
{
return (qt <= passEndTime) || (qt >= passStartTime);
}
}
// Create a list of satellite passes, between the given start and end times, that exceed the specified minimum elevation
// We return an uninitialised QDateTime if AOS or LOS is outside of predictionPeriod
static QList<SatellitePass> createPassList(Observer& obs, SGP4& sgp4, DateTime& startTime,
int predictionPeriod, double minAOSElevation, double minPassElevationDeg,
QTime passStartTime, QTime passEndTime, bool utc,
int noOfPasses)
{
QList<SatellitePass> passes;
bool aos = false;
bool aosUnknown = true;
double aosAz;
double losAz;
DateTime previousTime(startTime);
DateTime currentTime(startTime);
DateTime endTime(startTime.AddDays(predictionPeriod));
DateTime aosTime;
DateTime losTime;
while (currentTime < endTime)
{
bool endOfPass = false;
Eci eci = sgp4.FindPosition(currentTime);
CoordTopocentric topo = obs.GetLookAngle(eci);
if (!aos && (topo.elevation > minAOSElevation))
{
if (startTime == currentTime)
{
// AOS is before startTime
aosTime = findAOSBackwards(obs, sgp4, startTime, predictionPeriod, minAOSElevation, aosUnknown);
}
else
{
aosTime = findCrossingPoint(obs, sgp4, previousTime, currentTime, minAOSElevation, true);
aosUnknown = false;
}
aos = true;
eci = sgp4.FindPosition(aosTime);
topo = obs.GetLookAngle(eci);
aosAz = Units::radiansToDegrees(topo.azimuth);
}
else if (aos && (topo.elevation < minAOSElevation))
{
aos = false;
endOfPass = true;
losTime = findCrossingPoint(obs, sgp4, previousTime, currentTime, minAOSElevation, false);
eci = sgp4.FindPosition(losTime);
topo = obs.GetLookAngle(eci);
losAz = Units::radiansToDegrees(topo.azimuth);
double maxElevationDeg = findMaxElevation(obs, sgp4, aosTime, losTime);
if ((maxElevationDeg >= minPassElevationDeg)
&& inPassWindow(aosTime, passStartTime, passEndTime, utc)
&& inPassWindow(losTime, passStartTime, passEndTime, utc))
{
SatellitePass pass;
pass.m_aos = aosUnknown ? QDateTime() : dateTimeToQDateTime(aosTime);
pass.m_los = dateTimeToQDateTime(losTime);
pass.m_maxElevation = maxElevationDeg;
pass.m_aosAzimuth = aosAz;
pass.m_losAzimuth = losAz;
pass.m_northToSouth = std::min(360.0-aosAz, aosAz-0.0) < std::min(360.0-losAz, losAz-0.0);
passes.append(pass);
noOfPasses--;
if (noOfPasses <= 0)
return passes;
}
}
previousTime = currentTime;
if (endOfPass)
currentTime = currentTime + TimeSpan(0, 30, 0); // 30 minutes - no orbit likely to be that fast
else
currentTime = currentTime + TimeSpan(0, 0, 180);
if (currentTime > endTime)
currentTime = endTime;
}
if (aos)
{
// Pass still in progress at end time
Eci eci = sgp4.FindPosition(currentTime);
CoordTopocentric topo = obs.GetLookAngle(eci);
losAz = Units::radiansToDegrees(topo.azimuth);
double maxElevationDeg = findMaxElevation(obs, sgp4, aosTime, losTime);
if ((maxElevationDeg >= minPassElevationDeg)
&& inPassWindow(aosTime, passStartTime, passEndTime, utc)
&& inPassWindow(losTime, passStartTime, passEndTime, utc))
{
SatellitePass pass;
pass.m_aos = aosUnknown ? QDateTime() : dateTimeToQDateTime(aosTime);
pass.m_los = QDateTime();
pass.m_aosAzimuth = aosAz;
pass.m_losAzimuth = losAz;
pass.m_maxElevation = maxElevationDeg;
pass.m_northToSouth = std::min(360.0-aosAz, aosAz-0.0) < std::min(360.0-losAz, losAz-0.0);
passes.append(pass);
}
}
return passes;
}
void getSatelliteState(QDateTime dateTime,
const QString& tle0, const QString& tle1, const QString& tle2,
double latitude, double longitude, double altitude,
int predictionPeriod, int minAOSElevationDeg, int minPassElevationDeg,
QTime passStartTime, QTime passFinishTime, bool utc,
int noOfPasses, int groundTrackSteps, SatelliteState *satState)
{
try
{
Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString());
SGP4 sgp4(tle);
Observer obs(latitude, longitude, altitude);
DateTime dt = qDateTimeToDateTime(dateTime);
// Calculate satellite position
Eci eci = sgp4.FindPosition(dt);
// Calculate angle to satellite from antenna
CoordTopocentric topo = obs.GetLookAngle(eci);
// Convert satellite position to geodetic coordinates (lat and long)
CoordGeodetic geo = eci.ToGeodetic();
satState->m_latitude = Units::radiansToDegrees(geo.latitude);
satState->m_longitude = Units::radiansToDegrees(geo.longitude);
satState->m_altitude = geo.altitude;
satState->m_azimuth = Units::radiansToDegrees(topo.azimuth);
satState->m_elevation = Units::radiansToDegrees(topo.elevation);
satState->m_range = topo.range;
satState->m_rangeRate = topo.range_rate;
OrbitalElements ele(tle);
satState->m_speed = eci.Velocity().Magnitude();
satState->m_period = ele.Period();
if (noOfPasses > 0)
{
satState->m_passes.clear();
satState->m_passes = createPassList(obs, sgp4, dt, predictionPeriod,
Units::degreesToRadians((double)minAOSElevationDeg),
minPassElevationDeg,
passStartTime, passFinishTime, utc,
noOfPasses);
}
qDeleteAll(satState->m_groundTrack);
satState->m_groundTrack.clear();
qDeleteAll(satState->m_groundTrackDateTime);
satState->m_groundTrackDateTime.clear();
qDeleteAll(satState->m_predictedGroundTrack);
satState->m_predictedGroundTrack.clear();
qDeleteAll(satState->m_predictedGroundTrackDateTime);
satState->m_predictedGroundTrackDateTime.clear();
getGroundTrack(dateTime, tle0, tle1, tle2, groundTrackSteps, false, satState->m_groundTrack, satState->m_groundTrackDateTime);
getGroundTrack(dateTime, tle0, tle1, tle2, groundTrackSteps, true, satState->m_predictedGroundTrack, satState->m_predictedGroundTrackDateTime);
}
catch (SatelliteException& se)
{
qDebug() << "getSatelliteState:SatelliteException " << satState->m_name << ": " << se.what();
}
catch (DecayedException& de)
{
qDebug() << "getSatelliteState:DecayedException " << satState->m_name << ": " << de.what();
}
catch (TleException& tlee)
{
qDebug() << "getSatelliteState:TleException " << satState->m_name << ": " << tlee.what() << "\n" << tle0 << "\n" << tle1 << "\n" << tle2;
}
}