#include "RinexNavigation.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr double GpsMu = 3.986005e14; constexpr double EarthRotation = 7.2921151467e-5; constexpr double GpsWeekSeconds = 604800.0; constexpr double HalfGpsWeek = GpsWeekSeconds / 2.0; constexpr double CurrentGpsUtcOffsetSeconds = 18.0; constexpr int PositionUpdateIntervalMs = 60000; double parseField(const QString &line, int field) { QString text = line.mid(4 + field * 19, 19).trimmed(); text.replace(QChar('D'), QChar('E'), Qt::CaseInsensitive); bool ok = false; const double value = text.toDouble(&ok); return ok ? value : std::numeric_limits::quiet_NaN(); } bool finiteFields(std::initializer_list values) { return std::all_of(values.begin(), values.end(), [](double value) { return std::isfinite(value); }); } QByteArray gunzip(const QByteArray &input) { if (input.size() < 2 || static_cast(input[0]) != 0x1f || static_cast(input[1]) != 0x8b) { return input; } z_stream stream{}; stream.next_in = reinterpret_cast(const_cast(input.constData())); stream.avail_in = static_cast(input.size()); if (inflateInit2(&stream, 16 + MAX_WBITS) != Z_OK) { return {}; } QByteArray output; char buffer[64 * 1024]; int result = Z_OK; while (result == Z_OK) { stream.next_out = reinterpret_cast(buffer); stream.avail_out = sizeof(buffer); result = inflate(&stream, Z_NO_FLUSH); output.append(buffer, static_cast(sizeof(buffer) - stream.avail_out)); } inflateEnd(&stream); return result == Z_STREAM_END ? output : QByteArray{}; } QDateTime gpsEpoch() { return QDateTime(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC); } } // namespace RinexNavigation::RinexNavigation(QObject *parent) : QObject(parent) { m_positionTimer.setInterval(PositionUpdateIntervalMs); connect(&m_positionTimer, &QTimer::timeout, this, &RinexNavigation::updateCurrentPositions); m_positionTimer.start(); } void RinexNavigation::refresh() { if (m_loading) return; if (m_ephemerides.isEmpty()) { loadCache(); } setLoading(true); emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false); requestDay(QDateTime::currentDateTimeUtc().date(), 0); } void RinexNavigation::requestDay(const QDate &date, int fallbackIndex) { const QString year = QString::number(date.year()); const QString day = QStringLiteral("%1").arg(date.dayOfYear(), 3, 10, QLatin1Char('0')); // BKG's stream-derived file is updated during the UTC day; the final IGS // combination is normally published later and is therefore unsuitable here. const QString file = QStringLiteral("BRDC00WRD_S_") + year + day + QStringLiteral("0000_01D_MN.rnx.gz"); const QUrl url(QStringLiteral("https://igs.bkg.bund.de/root_ftp/IGS/BRDC/%1/%2/%3") .arg(year, day, file)); QNetworkRequest request(url); request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("ANKASYS-LocationPlotter/1.0 (RINEX client)")); request.setTransferTimeout(15000); QNetworkReply *reply = m_network.get(request); connect(reply, &QNetworkReply::finished, this, [this, reply, date, fallbackIndex] { const bool ok = reply->error() == QNetworkReply::NoError; const QByteArray data = ok ? reply->readAll() : QByteArray{}; reply->deleteLater(); if (!ok || data.isEmpty()) { if (fallbackIndex < 2) { requestDay(date.addDays(-1), fallbackIndex + 1); } else { setLoading(false); if (!m_ephemerides.isEmpty()) { publishCurrentMotion(); emit statusChanged( QStringLiteral("RINEX indirilemedi; mevcut efemeris kullaniliyor"), true); } else if (!loadCache()) { emit statusChanged(QStringLiteral("RINEX indirilemedi; internet baglantisini kontrol edin"), true); } } return; } handleDownload(data, date, fallbackIndex); }); } void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex) { const QByteArray rinex = gunzip(compressed); QString error; QVector ephemerides = parseEphemerides(rinex, &error); const QDateTime now = QDateTime::currentDateTimeUtc(); const QVector positions = propagateEphemerides( selectEphemerides(ephemerides, now), now); if (positions.isEmpty()) { if (fallbackIndex < 2) { requestDay(date.addDays(-1), fallbackIndex + 1); return; } setLoading(false); emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true); return; } QFile cache(cachePath()); QDir().mkpath(QFileInfo(cache).absolutePath()); if (cache.open(QIODevice::WriteOnly)) cache.write(rinex); m_ephemerides = std::move(ephemerides); setLoading(false); publishCurrentMotion(); emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 · %3") .arg(positions.size()) .arg(date.toString(QStringLiteral("dd.MM.yyyy"))) .arg(now.toLocalTime().toString( QStringLiteral("HH:mm:ss t"))), false); } bool RinexNavigation::loadCache() { QFile cache(cachePath()); if (!cache.open(QIODevice::ReadOnly)) return false; QString error; QVector ephemerides = parseEphemerides(cache.readAll(), &error); const QDateTime now = QDateTime::currentDateTimeUtc(); const QVector positions = propagateEphemerides( selectEphemerides(ephemerides, now), now); if (positions.isEmpty()) return false; m_ephemerides = std::move(ephemerides); publishCurrentMotion(); emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX · %2") .arg(positions.size()) .arg(now.toLocalTime().toString( QStringLiteral("HH:mm:ss t"))), true); return true; } QString RinexNavigation::cachePath() const { return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/latest-navigation.rnx"); } void RinexNavigation::updateCurrentPositions() { if (m_ephemerides.isEmpty()) return; publishCurrentMotion(); } void RinexNavigation::publishCurrentMotion() { if (m_ephemerides.isEmpty()) return; const QDateTime now = QDateTime::currentDateTimeUtc(); const QVector selected = selectEphemerides(m_ephemerides, now); const QVector current = propagateEphemerides(selected, now); if (current.isEmpty()) return; const QVector next = propagateEphemerides( selected, now.addMSecs(PositionUpdateIntervalMs)); emit satellitesUpdated(current); if (!next.isEmpty()) { emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs); } } void RinexNavigation::setLoading(bool loading) { if (m_loading == loading) return; m_loading = loading; emit loadingChanged(loading); } QVector RinexNavigation::parseEphemerides( const QByteArray &rinex, QString *error) { const QString text = QString::fromLatin1(rinex); const QStringList lines = text.split('\n'); int index = 0; while (index < lines.size() && !lines[index].contains(QStringLiteral("END OF HEADER"))) ++index; if (index >= lines.size()) { if (error) *error = QStringLiteral("END OF HEADER bulunamadi"); return {}; } ++index; QVector result; result.reserve(512); while (index < lines.size()) { const QString first = lines[index]; if (first.size() < 3) { ++index; continue; } const QChar system = first[0]; if (!system.isLetter()) { ++index; continue; } const int blockLines = (system == QLatin1Char('R') || system == QLatin1Char('S')) ? 4 : 8; if (index + blockLines > lines.size()) break; if (system != QLatin1Char('G')) { index += blockLines; continue; } const QStringList epoch = first.mid(3, 20).simplified().split(' '); if (epoch.size() < 6) { index += 8; continue; } bool dateOk = false; const int year = epoch[0].toInt(&dateOk); const QDateTime toc(QDate(year, epoch[1].toInt(), epoch[2].toInt()), QTime(epoch[3].toInt(), epoch[4].toInt(), static_cast(epoch[5].toDouble())), Qt::UTC); if (!dateOk || !toc.isValid()) { index += 8; continue; } Ephemeris eph; eph.prn = first.left(3).trimmed().toUpper(); eph.toc = toc; eph.crs = parseField(lines[index + 1], 1); eph.deltaN = parseField(lines[index + 1], 2); eph.m0 = parseField(lines[index + 1], 3); eph.cuc = parseField(lines[index + 2], 0); eph.eccentricity = parseField(lines[index + 2], 1); eph.cus = parseField(lines[index + 2], 2); eph.sqrtA = parseField(lines[index + 2], 3); eph.toe = parseField(lines[index + 3], 0); eph.cic = parseField(lines[index + 3], 1); eph.omega0 = parseField(lines[index + 3], 2); eph.cis = parseField(lines[index + 3], 3); eph.i0 = parseField(lines[index + 4], 0); eph.crc = parseField(lines[index + 4], 1); eph.omega = parseField(lines[index + 4], 2); eph.omegaDot = parseField(lines[index + 4], 3); eph.idot = parseField(lines[index + 5], 0); eph.gpsWeek = static_cast(std::llround(parseField(lines[index + 5], 2))); const double health = parseField(lines[index + 6], 1); eph.healthy = !std::isfinite(health) || std::llround(health) == 0; if (finiteFields({eph.crs, eph.deltaN, eph.m0, eph.cuc, eph.eccentricity, eph.cus, eph.sqrtA, eph.toe, eph.cic, eph.omega0, eph.cis, eph.i0, eph.crc, eph.omega, eph.omegaDot, eph.idot}) && eph.sqrtA > 4000.0 && eph.gpsWeek > 0) { result.push_back(eph); } index += 8; } if (result.isEmpty() && error) *error = QStringLiteral("GPS efemerisi bulunamadi"); return result; } QVector RinexNavigation::selectEphemerides( const QVector &ephemerides, const QDateTime &whenUtc) { QHash closest; QHash closestAge; // GPS navigation epochs are labelled in GPS system time, not UTC. const QDateTime whenGps = whenUtc.addSecs( static_cast(CurrentGpsUtcOffsetSeconds)); for (const Ephemeris &eph : ephemerides) { const qint64 age = std::abs(eph.toc.msecsTo(whenGps)); if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) { closest[eph.prn] = eph; closestAge[eph.prn] = age; } } QVector result; result.reserve(closest.size()); for (const Ephemeris &eph : closest) result.push_back(eph); std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return a.prn < b.prn; }); return result; } QVector RinexNavigation::propagateEphemerides( const QVector &ephemerides, const QDateTime &whenUtc) { QVector result; result.reserve(ephemerides.size()); for (const Ephemeris &eph : ephemerides) result.push_back(propagate(eph, whenUtc)); std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return a.prn < b.prn; }); return result; } SatellitePosition RinexNavigation::propagate(const Ephemeris &eph, const QDateTime &whenUtc) { const double gpsSeconds = gpsEpoch().msecsTo(whenUtc) / 1000.0 + CurrentGpsUtcOffsetSeconds; const double absoluteToe = eph.gpsWeek * GpsWeekSeconds + eph.toe; double tk = gpsSeconds - absoluteToe; while (tk > HalfGpsWeek) tk -= GpsWeekSeconds; while (tk < -HalfGpsWeek) tk += GpsWeekSeconds; const double a = eph.sqrtA * eph.sqrtA; const double meanMotion = std::sqrt(GpsMu / (a * a * a)) + eph.deltaN; const double meanAnomaly = eph.m0 + meanMotion * tk; double eccentricAnomaly = meanAnomaly; for (int iteration = 0; iteration < 12; ++iteration) { eccentricAnomaly = meanAnomaly + eph.eccentricity * std::sin(eccentricAnomaly); } const double trueAnomaly = std::atan2( std::sqrt(1.0 - eph.eccentricity * eph.eccentricity) * std::sin(eccentricAnomaly), std::cos(eccentricAnomaly) - eph.eccentricity); const double phi = trueAnomaly + eph.omega; const double twoPhi = 2.0 * phi; const double u = phi + eph.cus * std::sin(twoPhi) + eph.cuc * std::cos(twoPhi); const double radius = a * (1.0 - eph.eccentricity * std::cos(eccentricAnomaly)) + eph.crs * std::sin(twoPhi) + eph.crc * std::cos(twoPhi); const double inclination = eph.i0 + eph.idot * tk + eph.cis * std::sin(twoPhi) + eph.cic * std::cos(twoPhi); const double orbitalX = radius * std::cos(u); const double orbitalY = radius * std::sin(u); const double ascendingNode = eph.omega0 + (eph.omegaDot - EarthRotation) * tk - EarthRotation * eph.toe; SatellitePosition result; result.prn = eph.prn; result.x = orbitalX * std::cos(ascendingNode) - orbitalY * std::cos(inclination) * std::sin(ascendingNode); result.y = orbitalX * std::sin(ascendingNode) + orbitalY * std::cos(inclination) * std::cos(ascendingNode); result.z = orbitalY * std::sin(inclination); result.healthy = eph.healthy; return result; } QVector RinexNavigation::parseGpsNavigation( const QByteArray &rinex, const QDateTime &whenUtc, QString *error) { const QVector ephemerides = parseEphemerides(rinex, error); return propagateEphemerides(selectEphemerides(ephemerides, whenUtc), whenUtc); }