Improve satellite view and fused location telemetry

This commit is contained in:
2026-07-23 23:19:52 +03:00
parent 049f6301e8
commit 24b08ee518
12 changed files with 486 additions and 188 deletions
+7 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20)
project(location_plotter VERSION 0.2.0 LANGUAGES CXX)
project(location_plotter VERSION 0.3.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -62,6 +62,9 @@ add_executable(location_parser_tests
src/TelemetryPoint.h
)
target_link_libraries(location_parser_tests PRIVATE Qt6::Core Qt6::Network ZLIB::ZLIB)
target_compile_options(location_parser_tests PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
)
add_test(NAME location_parser_tests COMMAND location_parser_tests)
add_executable(location_main_window_tests
@@ -82,6 +85,9 @@ add_executable(location_main_window_tests
src/TelemetryPoint.h
)
target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
target_compile_options(location_main_window_tests PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
)
qt_add_resources(location_main_window_tests "test_app_resources"
PREFIX "/"
FILES resources/ankasys-logo.png resources/land-110m.json
+9 -2
View File
@@ -187,7 +187,8 @@ ancak son gecerli RINEX verisi ve harita onbellegiyle cevrimdisi kullanim da des
Port secici Linux'ta `/dev/ttyUSB*` ve `/dev/ttyACM*`; macOS'ta USB ile ilgili
`/dev/cu.*` ve `/dev/tty.*` aygitlarini listeler. Hic aygit algilanmazsa Linux'ta
`/dev/ttyUSB2`, macOS'ta `/dev/cu.usbserial` duzenlenebilir bir varsayilan yol olarak
gosterilir. Varsayilan baud rate `115200`'dur.
gosterilir. 50 Hz IMU-duzeltilmis konum akisini kayipsiz tasimak icin varsayilan baud
rate `460800`'dur.
If the port cannot be opened, make sure the user has serial access:
@@ -216,7 +217,13 @@ without logging out.
receiver marker without resetting a globe orientation chosen by dragging.
- RINEX download starts at every launch, repeats every 15 minutes, and falls back through the
previous two UTC days if today's stream file is unavailable. Current and next-minute satellite
positions are propagated from the cached ephemeris, then smoothly interpolated at 30 FPS.
positions are propagated from the cached ephemeris, then smoothly interpolated at 10 FPS.
- The 3D Earth tab includes a `RINEX Guncelle` button for an immediate manual refresh. Its status
line shows the UTC time of the last successful download or cache load.
- Complete UART channel snapshots such as `tracked satellites=5 prns=[...]` replace the previous
connected-PRN set, so satellites that were lost no longer remain green.
- RINEX text is parsed once per download and retained as compact ephemeris records. The static
globe is cached between camera changes, reducing CPU and allocation load during orbit animation.
- The globe uses an embedded Natural Earth coastline mask with terrain-like land colouring,
shallow-water edges, atmospheric shading, and a cached Earth-fixed texture for smooth rotation.
- The main content width and the right-side dashboard/UART height are both adjustable by
+129 -73
View File
@@ -15,6 +15,7 @@
#include <algorithm>
#include <cmath>
#include <utility>
namespace {
constexpr double Pi = 3.14159265358979323846;
@@ -211,8 +212,10 @@ GlobeWidget::GlobeWidget(QWidget *parent)
setMouseTracking(true);
setCursor(Qt::OpenHandCursor);
setToolTip(QStringLiteral("Dunyayi dondurmek icin surukleyin, yakinlastirmak icin tekerlegi kullanin"));
m_satelliteAnimationTimer.setInterval(33);
m_satelliteAnimationTimer.setTimerType(Qt::PreciseTimer);
// GNSS satellites move only a few screen pixels per minute. Ten frames per
// second is visually smooth here and avoids repainting a static globe at 30 FPS.
m_satelliteAnimationTimer.setInterval(100);
m_satelliteAnimationTimer.setTimerType(Qt::CoarseTimer);
connect(&m_satelliteAnimationTimer, &QTimer::timeout,
this, &GlobeWidget::updateSatelliteAnimation);
}
@@ -235,8 +238,29 @@ void GlobeWidget::setSatelliteMotion(const QVector<SatellitePosition> &current,
// Continue from the currently rendered points when a fresh ephemeris arrives,
// avoiding even a small visual jump at the update boundary.
m_satelliteMotionStart = m_satellites.isEmpty() ? current : m_satellites;
m_satelliteMotionTarget = next;
const QVector<SatellitePosition> &candidateStart =
m_satellites.isEmpty() ? current : m_satellites;
QHash<QString, SatellitePosition> starts;
starts.reserve(candidateStart.size());
for (const SatellitePosition &satellite : candidateStart) {
starts.insert(satellite.prn, satellite);
}
m_satelliteMotionStart.clear();
m_satelliteMotionTarget.clear();
m_satelliteMotionStart.reserve(next.size());
m_satelliteMotionTarget.reserve(next.size());
for (const SatellitePosition &target : next) {
const auto start = starts.constFind(target.prn);
if (start == starts.cend()) continue;
m_satelliteMotionStart.push_back(start.value());
m_satelliteMotionTarget.push_back(target);
}
if (m_satelliteMotionStart.isEmpty()) {
setSatellites(current);
m_satelliteAnimationTimer.stop();
return;
}
m_satelliteMotionDurationMs = durationMs;
m_satelliteMotionClock.restart();
m_satelliteAnimationTimer.start();
@@ -250,18 +274,11 @@ void GlobeWidget::updateSatelliteAnimation()
const double progress = std::clamp(
static_cast<double>(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs,
0.0, 1.0);
QHash<QString, SatellitePosition> targets;
targets.reserve(m_satelliteMotionTarget.size());
for (const SatellitePosition &satellite : m_satelliteMotionTarget) {
targets.insert(satellite.prn, satellite);
}
QVector<SatellitePosition> interpolated;
interpolated.reserve(m_satelliteMotionStart.size());
for (const SatellitePosition &start : m_satelliteMotionStart) {
const auto targetIt = targets.constFind(start.prn);
if (targetIt == targets.cend()) continue;
const SatellitePosition &target = targetIt.value();
for (qsizetype i = 0; i < m_satelliteMotionStart.size(); ++i) {
const SatellitePosition &start = m_satelliteMotionStart[i];
const SatellitePosition &target = m_satelliteMotionTarget[i];
SatellitePosition point = start;
point.x += (target.x - start.x) * progress;
point.y += (target.y - start.y) * progress;
@@ -336,6 +353,74 @@ void GlobeWidget::paintEvent(QPaintEvent *)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
const qreal devicePixelRatio = devicePixelRatioF();
if (m_staticLayer.isNull() || m_staticLayerSize != size()
|| !qFuzzyCompare(m_staticLayerDevicePixelRatio, devicePixelRatio)
|| m_staticLayerYaw != m_yaw || m_staticLayerPitch != m_pitch
|| m_staticLayerZoom != m_zoom) {
rebuildStaticLayer();
}
painter.drawPixmap(0, 0, m_staticLayer);
if (m_hasReceiver) drawVisibilityCone(painter);
if (m_hasReceiver) drawReceiver(painter);
const int visibleCount = drawSatellites(painter);
painter.setPen(QColor(229, 240, 249));
QFont titleFont = painter.font();
titleFont.setPixelSize(18);
titleFont.setBold(true);
painter.setFont(titleFont);
painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter,
QStringLiteral("GNSS YORUNGE GORUNUMU"));
QFont smallFont = painter.font();
smallFont.setPixelSize(11);
smallFont.setBold(false);
painter.setFont(smallFont);
painter.setPen(m_statusError ? QColor(255, 177, 77) : QColor(132, 177, 204));
painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignLeft | Qt::AlignVCenter, m_status);
const int legendY = height() - 25;
const int legendX = width() / 2 - 125;
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(42, 224, 137));
painter.drawEllipse(QPointF(legendX, legendY - 3), 4, 4);
painter.setPen(QColor(101, 239, 176));
painter.drawText(legendX + 9, legendY + 1, QStringLiteral("BULUNDU"));
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(255, 79, 95));
painter.drawEllipse(QPointF(legendX + 78, legendY - 3), 4, 4);
painter.setPen(QColor(255, 132, 143));
painter.drawText(legendX + 87, legendY + 1, QStringLiteral("BULUNAMADI"));
painter.setPen(QColor(138, 183, 207));
painter.drawText(legendX + 181, legendY + 1,
QStringLiteral("%1/%2 PRN · ELEV≥%3°: %4")
.arg(m_observedPrns.size()).arg(m_satellites.size())
.arg(m_elevationMaskDegrees, 0, 'f', 1).arg(visibleCount));
if (!m_hasReceiver) {
painter.setPen(QColor(219, 232, 241));
QFont waitingFont = painter.font();
waitingFont.setPixelSize(16);
waitingFont.setBold(true);
painter.setFont(waitingFont);
painter.drawText(rect().adjusted(30, 70, -30, -50), Qt::AlignCenter,
QStringLiteral("Ilk gecerli konum bekleniyor"));
}
}
void GlobeWidget::rebuildStaticLayer()
{
const qreal devicePixelRatio = devicePixelRatioF();
const QSize pixelSize(
std::max(1, qRound(width() * devicePixelRatio)),
std::max(1, qRound(height() * devicePixelRatio)));
QPixmap layer(pixelSize);
layer.setDevicePixelRatio(devicePixelRatio);
layer.fill(Qt::transparent);
QPainter painter(&layer);
painter.setRenderHint(QPainter::Antialiasing, true);
QLinearGradient background(0, 0, width(), height());
background.setColorAt(0.0, QColor(3, 10, 21));
background.setColorAt(0.55, QColor(8, 24, 43));
@@ -374,63 +459,23 @@ void GlobeWidget::paintEvent(QPaintEvent *)
painter.restore();
for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0);
if (m_hasReceiver) drawVisibilityCone(painter);
if (m_hasReceiver) drawReceiver(painter);
drawSatellites(painter);
painter.end();
painter.setPen(QColor(229, 240, 249));
QFont titleFont = painter.font();
titleFont.setPixelSize(18);
titleFont.setBold(true);
painter.setFont(titleFont);
painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter,
QStringLiteral("GNSS YORUNGE GORUNUMU"));
QFont smallFont = painter.font();
smallFont.setPixelSize(11);
smallFont.setBold(false);
painter.setFont(smallFont);
painter.setPen(m_statusError ? QColor(255, 177, 77) : QColor(132, 177, 204));
painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignLeft | Qt::AlignVCenter, m_status);
const int legendY = height() - 25;
const int legendX = width() / 2 - 125;
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(42, 224, 137));
painter.drawEllipse(QPointF(legendX, legendY - 3), 4, 4);
painter.setPen(QColor(101, 239, 176));
painter.drawText(legendX + 9, legendY + 1, QStringLiteral("BULUNDU"));
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(255, 79, 95));
painter.drawEllipse(QPointF(legendX + 78, legendY - 3), 4, 4);
painter.setPen(QColor(255, 132, 143));
painter.drawText(legendX + 87, legendY + 1, QStringLiteral("BULUNAMADI"));
painter.setPen(QColor(138, 183, 207));
int visibleCount = 0;
for (const auto &satellite : m_satellites) {
if (satelliteElevation(satellite) >= m_elevationMaskDegrees) ++visibleCount;
}
painter.drawText(legendX + 181, legendY + 1,
QStringLiteral("%1/%2 PRN · ELEV≥%3°: %4")
.arg(m_observedPrns.size()).arg(m_satellites.size())
.arg(m_elevationMaskDegrees, 0, 'f', 1).arg(visibleCount));
if (!m_hasReceiver) {
painter.setPen(QColor(219, 232, 241));
QFont waitingFont = painter.font();
waitingFont.setPixelSize(16);
waitingFont.setBold(true);
painter.setFont(waitingFont);
painter.drawText(rect().adjusted(30, 70, -30, -50), Qt::AlignCenter,
QStringLiteral("Ilk gecerli konum bekleniyor"));
}
m_staticLayer = std::move(layer);
m_staticLayerSize = size();
m_staticLayerDevicePixelRatio = devicePixelRatio;
m_staticLayerYaw = m_yaw;
m_staticLayerPitch = m_pitch;
m_staticLayerZoom = m_zoom;
}
void GlobeWidget::drawContinents(QPainter &painter) const
{
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
const double pixelRatio = std::min(devicePixelRatioF(), 1.5);
const double pixelRatio = m_dragging ? 0.75 : std::min(devicePixelRatioF(), 1.5);
const int textureSize = std::clamp(
static_cast<int>(std::ceil(radius * 2.0 * pixelRatio)), 320, 900);
static_cast<int>(std::ceil(radius * 2.0 * pixelRatio)),
m_dragging ? 240 : 320, m_dragging ? 480 : 900);
const QPointF center(width() * 0.5, height() * 0.52);
const QRectF target(center.x() - radius, center.y() - radius,
radius * 2.0, radius * 2.0);
@@ -715,33 +760,41 @@ void GlobeWidget::drawReceiver(QPainter &painter) const
painter.drawText(p.point + QPointF(10, -8), QStringLiteral("ALICI"));
}
void GlobeWidget::drawSatellites(QPainter &painter) const
int GlobeWidget::drawSatellites(QPainter &painter) const
{
if (!m_hasReceiver) return;
if (!m_hasReceiver) return 0;
const double orbitRadius = std::min(width(), height()) * 0.255 * m_zoom * 1.47;
struct Item { SatellitePosition satellite; ProjectedPoint projected; };
struct Item {
SatellitePosition satellite;
ProjectedPoint projected;
double elevation = -90.0;
};
QVector<Item> items;
items.reserve(m_satellites.size());
for (const auto &satellite : m_satellites) {
const double length = std::sqrt(satellite.x * satellite.x + satellite.y * satellite.y
+ satellite.z * satellite.z);
if (length <= 0.0) continue;
items.push_back({satellite, project(satellite.x / length, satellite.y / length,
satellite.z / length, orbitRadius)});
satellite.z / length, orbitRadius),
satelliteElevation(satellite)});
}
std::sort(items.begin(), items.end(), [](const Item &a, const Item &b) {
return a.projected.depth < b.projected.depth;
});
const double earthRadius = std::min(width(), height()) * 0.255 * m_zoom;
const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude);
const auto receiverPoint = project(receiver.x, receiver.y, receiver.z,
earthRadius * 1.02);
int visibleCount = 0;
for (const Item &item : items) {
const bool found = m_observedPrns.contains(item.satellite.prn);
const bool inElevationMask = satelliteElevation(item.satellite) >= m_elevationMaskDegrees;
const bool inElevationMask = item.elevation >= m_elevationMaskDegrees;
if (inElevationMask) ++visibleCount;
QColor color = found ? QColor(42, 224, 137) : QColor(255, 79, 95);
if (item.projected.depth < 0.0) color.setAlpha(75);
const double markerRadius = found ? 6.5 : 5.0;
if (inElevationMask) {
const double earthRadius = std::min(width(), height()) * 0.255 * m_zoom;
const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude);
const auto receiverPoint = project(receiver.x, receiver.y, receiver.z,
earthRadius * 1.02);
QColor sightLine(70, 224, 231, item.projected.depth < 0.0 ? 25 : 80);
painter.setPen(QPen(sightLine, 0.8, Qt::DashLine));
painter.drawLine(receiverPoint.point, item.projected.point);
@@ -757,6 +810,7 @@ void GlobeWidget::drawSatellites(QPainter &painter) const
painter.drawText(item.projected.point + QPointF(7, -6), item.satellite.prn);
}
}
return visibleCount;
}
double GlobeWidget::satelliteElevation(const SatellitePosition &satellite) const
@@ -802,7 +856,9 @@ void GlobeWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_dragging = false;
m_staticLayer = QPixmap();
setCursor(Qt::OpenHandCursor);
update();
}
}
+10 -1
View File
@@ -5,6 +5,7 @@
#include <QElapsedTimer>
#include <QImage>
#include <QPoint>
#include <QPixmap>
#include <QSet>
#include <QTimer>
#include <QWidget>
@@ -23,6 +24,7 @@ public:
void setReceiverLocation(double latitude, double longitude);
void clearReceiverLocation();
void setRinexStatus(const QString &status, bool error);
[[nodiscard]] QSet<QString> observedPrns() const { return m_observedPrns; }
public slots:
void setElevationMask(double degrees);
@@ -47,9 +49,10 @@ private:
void drawVisibilityCone(QPainter &painter) const;
void drawGraticule(QPainter &painter) const;
void drawReceiver(QPainter &painter) const;
void drawSatellites(QPainter &painter) const;
int drawSatellites(QPainter &painter) const;
double satelliteElevation(const SatellitePosition &satellite) const;
void updateSatelliteAnimation();
void rebuildStaticLayer();
QVector<SatellitePosition> m_satellites;
QVector<SatellitePosition> m_satelliteMotionStart;
@@ -60,6 +63,12 @@ private:
mutable QImage m_landTexture;
mutable double m_landTextureYaw = 0.0;
mutable double m_landTexturePitch = 0.0;
QPixmap m_staticLayer;
QSize m_staticLayerSize;
qreal m_staticLayerDevicePixelRatio = 0.0;
double m_staticLayerYaw = 0.0;
double m_staticLayerPitch = 0.0;
double m_staticLayerZoom = 0.0;
QSet<QString> m_observedPrns;
QString m_status = QStringLiteral("Konum bekleniyor");
bool m_statusError = false;
+20
View File
@@ -103,6 +103,26 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
telemetry.source = LocationSource::Gps;
}
}
static const QRegularExpression fusionModeRegex(
QStringLiteral(R"((?:^|\s)mode\s*=\s*(?<mode>gps[_-]?aided|dead[_-]?reckoning)(?=$|\s))"),
QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch fusionModeMatch = fusionModeRegex.match(trimmed);
if (fusionModeMatch.hasMatch()) {
telemetry.fusionMode = fusionModeMatch.captured(QStringLiteral("mode")).startsWith(
QStringLiteral("dead"), Qt::CaseInsensitive)
? FusionMode::DeadReckoning : FusionMode::GpsAided;
}
static const QRegularExpression gpsAgeRegex(
QStringLiteral(R"((?:^|\s)gps[_-]?age[_-]?ms\s*=\s*(?<age>\d+)(?=$|\s))"),
QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch gpsAgeMatch = gpsAgeRegex.match(trimmed);
if (gpsAgeMatch.hasMatch()) {
bool ok = false;
const qint64 ageMs = gpsAgeMatch.captured(QStringLiteral("age")).toLongLong(&ok);
if (ok) {
telemetry.gpsAgeMs = ageMs;
}
}
bool hadCoordinateField = false;
bool hadLatitudeField = false;
bool hadLongitudeField = false;
+63 -7
View File
@@ -307,7 +307,7 @@ public:
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
return parent.isValid() ? 0 : 7;
return parent.isValid() ? 0 : 8;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
@@ -327,6 +327,14 @@ public:
case 0:
return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz"));
case 1:
if (point.source == LocationSource::ImuCorrected
&& point.fusionMode == FusionMode::DeadReckoning) {
return QStringLiteral("IMU corrected (DR)");
}
if (point.source == LocationSource::ImuCorrected
&& point.fusionMode == FusionMode::GpsAided) {
return QStringLiteral("IMU corrected (GPS)");
}
return locationSourceText(point.source);
case 2:
return coordinateText(point.latitude, point.latitudeText);
@@ -338,6 +346,10 @@ public:
return optionalIntText(point.satellites);
case 6:
return optionalDopText(point.dop.pdop);
case 7:
return point.gpsAgeMs.has_value()
? QStringLiteral("%1 ms").arg(*point.gpsAgeMs)
: QStringLiteral("-");
default:
return {};
}
@@ -351,7 +363,8 @@ public:
}
static const QStringList headers = {
QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"),
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")};
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP"),
QStringLiteral("GPS yas")};
return section >= 0 && section < headers.size() ? headers[section] : QVariant();
}
@@ -386,6 +399,14 @@ MainWindow::MainWindow(QWidget *parent)
m_globe->setRinexStatus(status, error);
statusBar()->showMessage(status, error ? 8000 : 4000);
});
connect(m_rinex, &RinexNavigation::loadingChanged, this, [this](bool loading) {
m_refreshRinexButton->setEnabled(!loading);
m_refreshRinexButton->setText(
loading ? QStringLiteral("RINEX indiriliyor...")
: QStringLiteral("RINEX Guncelle"));
});
connect(m_refreshRinexButton, &QPushButton::clicked,
m_rinex, &RinexNavigation::refresh);
m_rinex->refresh();
auto *rinexRefreshTimer = new QTimer(this);
rinexRefreshTimer->setInterval(15 * 60 * 1000);
@@ -497,15 +518,24 @@ void MainWindow::handleSerialLine(const QString &line)
static const QRegularExpression solveStartRegex(
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression trackingSnapshotRegex(
QStringLiteral(
R"(\btracked\s+satellites\s*=\s*\d+\s+(?:prns?|svns?)\s*=\s*\[[^\]]*\])"),
QRegularExpression::CaseInsensitiveOption);
const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch();
if (solveStarted) {
m_observedPrns.clear();
}
const QSet<QString> linePrns = LocationParser::parseGpsPrns(line);
if (!linePrns.isEmpty()) {
if (trackingSnapshot) {
// This line is a complete point-in-time channel snapshot. Replacing
// prevents satellites from older solves remaining green indefinitely.
m_observedPrns = linePrns;
} else if (!linePrns.isEmpty()) {
m_observedPrns.unite(linePrns);
}
if (solveStarted || !linePrns.isEmpty()) {
if (solveStarted || trackingSnapshot || !linePrns.isEmpty()) {
m_globe->setObservedPrns(m_observedPrns);
}
@@ -809,7 +839,7 @@ void MainWindow::buildUi()
for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) {
m_baudCombo->addItem(QString::number(baud));
}
m_baudCombo->setCurrentText(QStringLiteral("115200"));
m_baudCombo->setCurrentText(QStringLiteral("460800"));
m_maxPointsSpin = new QSpinBox(this);
m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput"));
@@ -856,7 +886,25 @@ void MainWindow::buildUi()
auto *viewTabs = new QTabWidget(this);
viewTabs->setObjectName(QStringLiteral("mainViewTabs"));
viewTabs->addTab(m_map, QStringLiteral("2B Harita"));
viewTabs->addTab(m_globe, QStringLiteral("3B Dunya · Uydular"));
auto *globeTab = new QWidget(viewTabs);
auto *globeTabLayout = new QVBoxLayout(globeTab);
globeTabLayout->setContentsMargins(0, 0, 0, 0);
globeTabLayout->setSpacing(6);
auto *rinexToolbar = new QWidget(globeTab);
auto *rinexToolbarLayout = new QHBoxLayout(rinexToolbar);
rinexToolbarLayout->setContentsMargins(10, 6, 10, 0);
rinexToolbarLayout->addWidget(new QLabel(
QStringLiteral("GPS yayin efemerisi · otomatik: 15 dk"), rinexToolbar));
rinexToolbarLayout->addStretch(1);
m_refreshRinexButton = new QPushButton(QStringLiteral("RINEX Guncelle"), rinexToolbar);
m_refreshRinexButton->setObjectName(QStringLiteral("refreshRinexButton"));
m_refreshRinexButton->setToolTip(
QStringLiteral("Guncel RINEX navigasyon dosyasini simdi indir"));
rinexToolbarLayout->addWidget(m_refreshRinexButton);
globeTabLayout->addWidget(rinexToolbar);
globeTabLayout->addWidget(m_globe, 1);
viewTabs->addTab(globeTab, QStringLiteral("3B Dunya · Uydular"));
auto *sidePanel = new QWidget(this);
sidePanel->setMinimumWidth(390);
@@ -1105,6 +1153,8 @@ void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
{
TelemetryPoint point;
point.source = telemetry.source;
point.fusionMode = telemetry.fusionMode;
point.gpsAgeMs = telemetry.gpsAgeMs;
point.latitude = *telemetry.latitude;
point.longitude = *telemetry.longitude;
point.latitudeText = telemetry.latitudeText;
@@ -1209,6 +1259,12 @@ void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry)
commitPendingTelemetry();
}
if (telemetry.fusionMode != FusionMode::Unknown) {
m_pendingTelemetry.fusionMode = telemetry.fusionMode;
}
if (telemetry.gpsAgeMs.has_value()) {
m_pendingTelemetry.gpsAgeMs = telemetry.gpsAgeMs;
}
if (telemetry.latitude.has_value()) {
m_pendingTelemetry.source = telemetry.source;
m_pendingTelemetry.latitude = telemetry.latitude;
@@ -1290,5 +1346,5 @@ int MainWindow::selectedBaudRate() const
{
bool ok = false;
const int baudRate = m_baudCombo->currentText().toInt(&ok);
return ok ? baudRate : 115200;
return ok ? baudRate : 460800;
}
+1
View File
@@ -74,6 +74,7 @@ private:
QPushButton *m_connectButton = nullptr;
QPushButton *m_clearButton = nullptr;
QPushButton *m_openLogButton = nullptr;
QPushButton *m_refreshRinexButton = nullptr;
QCheckBox *m_autoFollowCheck = nullptr;
QCheckBox *m_showTelemetryCheck = nullptr;
QCheckBox *m_showRecentPointsCheck = nullptr;
+133 -100
View File
@@ -2,6 +2,7 @@
#include <QDir>
#include <QFile>
#include <QHash>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QStandardPaths>
@@ -12,6 +13,7 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <utility>
namespace {
@@ -22,29 +24,6 @@ constexpr double HalfGpsWeek = GpsWeekSeconds / 2.0;
constexpr double CurrentGpsUtcOffsetSeconds = 18.0;
constexpr int PositionUpdateIntervalMs = 60000;
struct Ephemeris {
QString prn;
QDateTime toc;
double crs = 0.0;
double deltaN = 0.0;
double m0 = 0.0;
double cuc = 0.0;
double eccentricity = 0.0;
double cus = 0.0;
double sqrtA = 0.0;
double toe = 0.0;
double cic = 0.0;
double omega0 = 0.0;
double cis = 0.0;
double i0 = 0.0;
double crc = 0.0;
double omega = 0.0;
double omegaDot = 0.0;
double idot = 0.0;
int gpsWeek = 0;
bool healthy = true;
};
double parseField(const QString &line, int field)
{
QString text = line.mid(4 + field * 19, 19).trimmed();
@@ -93,49 +72,6 @@ QDateTime gpsEpoch()
return QDateTime(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC);
}
SatellitePosition 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;
}
} // namespace
RinexNavigation::RinexNavigation(QObject *parent)
@@ -150,10 +86,10 @@ RinexNavigation::RinexNavigation(QObject *parent)
void RinexNavigation::refresh()
{
if (m_loading) return;
if (m_latestRinex.isEmpty()) {
if (m_ephemerides.isEmpty()) {
loadCache();
}
m_loading = true;
setLoading(true);
emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false);
requestDay(QDateTime::currentDateTimeUtc().date(), 0);
}
@@ -181,8 +117,13 @@ void RinexNavigation::requestDay(const QDate &date, int fallbackIndex)
if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1);
} else {
m_loading = false;
if (!loadCache()) {
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);
}
}
@@ -197,14 +138,16 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
{
const QByteArray rinex = gunzip(compressed);
QString error;
const QVector<SatellitePosition> positions = parseGpsNavigation(
rinex, QDateTime::currentDateTimeUtc(), &error);
QVector<Ephemeris> ephemerides = parseEphemerides(rinex, &error);
const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> positions = propagateEphemerides(
selectEphemerides(ephemerides, now), now);
if (positions.isEmpty()) {
if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1);
return;
}
m_loading = false;
setLoading(false);
emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true);
return;
}
@@ -212,11 +155,13 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
QFile cache(cachePath());
QDir().mkpath(QFileInfo(cache).absolutePath());
if (cache.open(QIODevice::WriteOnly)) cache.write(rinex);
m_latestRinex = rinex;
m_loading = false;
m_ephemerides = std::move(ephemerides);
setLoading(false);
publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 (UTC)")
.arg(positions.size()).arg(date.toString(QStringLiteral("dd.MM.yyyy"))), false);
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 · %3 UTC")
.arg(positions.size())
.arg(date.toString(QStringLiteral("dd.MM.yyyy")))
.arg(now.time().toString(QStringLiteral("HH:mm:ss"))), false);
}
bool RinexNavigation::loadCache()
@@ -224,12 +169,16 @@ bool RinexNavigation::loadCache()
QFile cache(cachePath());
if (!cache.open(QIODevice::ReadOnly)) return false;
QString error;
m_latestRinex = cache.readAll();
const QVector<SatellitePosition> positions = parseGpsNavigation(
m_latestRinex, QDateTime::currentDateTimeUtc(), &error);
QVector<Ephemeris> ephemerides = parseEphemerides(cache.readAll(), &error);
const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> 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").arg(positions.size()), true);
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX · %2 UTC")
.arg(positions.size())
.arg(now.time().toString(QStringLiteral("HH:mm:ss"))), true);
return true;
}
@@ -241,28 +190,36 @@ QString RinexNavigation::cachePath() const
void RinexNavigation::updateCurrentPositions()
{
if (m_latestRinex.isEmpty()) return;
if (m_ephemerides.isEmpty()) return;
publishCurrentMotion();
}
void RinexNavigation::publishCurrentMotion()
{
if (m_latestRinex.isEmpty()) return;
if (m_ephemerides.isEmpty()) return;
const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> current = parseGpsNavigation(m_latestRinex, now);
const QVector<Ephemeris> selected = selectEphemerides(m_ephemerides, now);
const QVector<SatellitePosition> current = propagateEphemerides(selected, now);
if (current.isEmpty()) return;
const QVector<SatellitePosition> next = parseGpsNavigation(
m_latestRinex, now.addMSecs(PositionUpdateIntervalMs));
const QVector<SatellitePosition> next = propagateEphemerides(
selected, now.addMSecs(PositionUpdateIntervalMs));
emit satellitesUpdated(current);
if (!next.isEmpty()) {
emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs);
}
}
QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
const QByteArray &rinex, const QDateTime &whenUtc, QString *error)
void RinexNavigation::setLoading(bool loading)
{
if (m_loading == loading) return;
m_loading = loading;
emit loadingChanged(loading);
}
QVector<RinexNavigation::Ephemeris> RinexNavigation::parseEphemerides(
const QByteArray &rinex, QString *error)
{
const QString text = QString::fromLatin1(rinex);
const QStringList lines = text.split('\n');
@@ -274,8 +231,8 @@ QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
}
++index;
QHash<QString, Ephemeris> closest;
QHash<QString, qint64> closestAge;
QVector<Ephemeris> result;
result.reserve(512);
while (index < lines.size()) {
const QString first = lines[index];
if (first.size() < 3) { ++index; continue; }
@@ -321,19 +278,95 @@ QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
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) {
const qint64 age = std::abs(toc.secsTo(whenUtc));
if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) {
closest[eph.prn] = eph;
closestAge[eph.prn] = age;
}
result.push_back(eph);
}
index += 8;
}
QVector<SatellitePosition> result;
result.reserve(closest.size());
for (const Ephemeris &eph : closest) result.push_back(propagate(eph, whenUtc));
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return a.prn < b.prn; });
if (result.isEmpty() && error) *error = QStringLiteral("GPS efemerisi bulunamadi");
return result;
}
QVector<RinexNavigation::Ephemeris> RinexNavigation::selectEphemerides(
const QVector<Ephemeris> &ephemerides, const QDateTime &whenUtc)
{
QHash<QString, Ephemeris> closest;
QHash<QString, qint64> closestAge;
// GPS navigation epochs are labelled in GPS system time, not UTC.
const QDateTime whenGps = whenUtc.addSecs(
static_cast<qint64>(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<Ephemeris> 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<SatellitePosition> RinexNavigation::propagateEphemerides(
const QVector<Ephemeris> &ephemerides, const QDateTime &whenUtc)
{
QVector<SatellitePosition> 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<SatellitePosition> RinexNavigation::parseGpsNavigation(
const QByteArray &rinex, const QDateTime &whenUtc, QString *error)
{
const QVector<Ephemeris> ephemerides = parseEphemerides(rinex, error);
return propagateEphemerides(selectEphemerides(ephemerides, whenUtc), whenUtc);
}
+33 -1
View File
@@ -26,17 +26,49 @@ signals:
const QVector<SatellitePosition> &next,
int durationMs);
void statusChanged(const QString &status, bool error);
void loadingChanged(bool loading);
private:
struct Ephemeris {
QString prn;
QDateTime toc;
double crs = 0.0;
double deltaN = 0.0;
double m0 = 0.0;
double cuc = 0.0;
double eccentricity = 0.0;
double cus = 0.0;
double sqrtA = 0.0;
double toe = 0.0;
double cic = 0.0;
double omega0 = 0.0;
double cis = 0.0;
double i0 = 0.0;
double crc = 0.0;
double omega = 0.0;
double omegaDot = 0.0;
double idot = 0.0;
int gpsWeek = 0;
bool healthy = true;
};
void requestDay(const QDate &date, int fallbackIndex);
void handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex);
bool loadCache();
QString cachePath() const;
void updateCurrentPositions();
void publishCurrentMotion();
void setLoading(bool loading);
static QVector<Ephemeris> parseEphemerides(
const QByteArray &rinex, QString *error = nullptr);
static QVector<Ephemeris> selectEphemerides(
const QVector<Ephemeris> &ephemerides, const QDateTime &whenUtc);
static QVector<SatellitePosition> propagateEphemerides(
const QVector<Ephemeris> &ephemerides, const QDateTime &whenUtc);
static SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc);
QNetworkAccessManager m_network;
QTimer m_positionTimer;
QByteArray m_latestRinex;
QVector<Ephemeris> m_ephemerides;
bool m_loading = false;
};
+10
View File
@@ -12,6 +12,12 @@ enum class LocationSource {
Trajectory
};
enum class FusionMode {
Unknown,
GpsAided,
DeadReckoning
};
inline QString locationSourceText(LocationSource source)
{
switch (source) {
@@ -43,6 +49,8 @@ struct DopValues {
struct TelemetryPoint {
LocationSource source = LocationSource::Generic;
FusionMode fusionMode = FusionMode::Unknown;
std::optional<qint64> gpsAgeMs;
double latitude = 0.0;
double longitude = 0.0;
QString latitudeText;
@@ -56,6 +64,8 @@ struct TelemetryPoint {
struct ParsedTelemetry {
LocationSource source = LocationSource::Generic;
FusionMode fusionMode = FusionMode::Unknown;
std::optional<qint64> gpsAgeMs;
std::optional<double> latitude;
std::optional<double> longitude;
QString latitudeText;
+16
View File
@@ -1,4 +1,5 @@
#include "../src/MainWindow.h"
#include "../src/GlobeWidget.h"
#include "../src/MapWidget.h"
#include <QApplication>
@@ -9,6 +10,7 @@
#include <QLineEdit>
#include <QMetaObject>
#include <QSpinBox>
#include <QPushButton>
#include <QTableView>
#include <QTabWidget>
#include <QSplitter>
@@ -51,6 +53,8 @@ int main(int argc, char **argv)
auto *showRecentPoints = window.findChild<QCheckBox *>(QStringLiteral("showRecentPointsCheck"));
auto *showAverage = window.findChild<QCheckBox *>(QStringLiteral("showAverageCheck"));
auto *portSelector = window.findChild<QComboBox *>(QStringLiteral("portSelector"));
auto *refreshRinexButton =
window.findChild<QPushButton *>(QStringLiteral("refreshRinexButton"));
auto *map = window.findChild<MapWidget *>();
auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs"));
auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter"));
@@ -69,6 +73,9 @@ int main(int argc, char **argv)
assert(showRecentPoints != nullptr);
assert(showAverage != nullptr);
assert(portSelector != nullptr);
assert(refreshRinexButton != nullptr);
assert(refreshRinexButton->text() == QStringLiteral("RINEX Guncelle")
|| refreshRinexButton->text() == QStringLiteral("RINEX indiriliyor..."));
assert(portSelector->isEditable());
assert(portSelector->count() >= 1);
assert(portSelector->currentText() == QStringLiteral("/dev/test-usb-uart"));
@@ -82,6 +89,15 @@ int main(int argc, char **argv)
assert(sideSplitter != nullptr);
assert(sideSplitter->orientation() == Qt::Vertical);
sendLine(window, QStringLiteral("[TRK] tracked satellites=2 prns=[3,8]"));
assert(map != nullptr);
auto *globe = window.findChild<GlobeWidget *>();
assert(globe != nullptr);
assert(globe->observedPrns()
== QSet<QString>({QStringLiteral("G03"), QStringLiteral("G08")}));
sendLine(window, QStringLiteral("[TRK] tracked satellites=1 prns=[14]"));
assert(globe->observedPrns() == QSet<QString>({QStringLiteral("G14")}));
assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection,
Q_ARG(double, 40.25), Q_ARG(double, 29.5)));
assert(referenceLatInput->text() == QStringLiteral("40.250000000000000"));
+55 -3
View File
@@ -9,9 +9,39 @@
namespace {
bool closeTo(double left, double right)
bool closeTo(double left, double right, double tolerance = 0.000001)
{
return std::fabs(left - right) < 0.000001;
return std::fabs(left - right) < tolerance;
}
QString rinexField(double value)
{
return QStringLiteral("%1").arg(value, 19, 'E', 12);
}
QString rinexContinuation(std::initializer_list<double> values)
{
QString line(4, QLatin1Char(' '));
for (double value : values) line += rinexField(value);
return line + QLatin1Char('\n');
}
QByteArray syntheticGpsNavigation(const QDateTime &gpsEpochLabel, int gpsWeek, double toe)
{
QString rinex = QStringLiteral(
" 3.05 N: GNSS NAV DATA M: MIXED RINEX VERSION / TYPE\n"
" END OF HEADER\n");
rinex += QStringLiteral("G01 %1").arg(
gpsEpochLabel.toString(QStringLiteral("yyyy MM dd HH mm ss")));
rinex += rinexField(0.0) + rinexField(0.0) + rinexField(0.0) + QLatin1Char('\n');
rinex += rinexContinuation({1.0, 0.0, 0.0, 0.0});
rinex += rinexContinuation({0.0, 0.0, 0.0, 5153.7954775});
rinex += rinexContinuation({toe, 0.0, 0.0, 0.0});
rinex += rinexContinuation({0.0, 0.0, 0.0, 0.0});
rinex += rinexContinuation({0.0, 0.0, static_cast<double>(gpsWeek), 0.0});
rinex += rinexContinuation({2.0, 0.0, 0.0, 1.0});
rinex += rinexContinuation({0.0, 0.0, 0.0, 0.0});
return rinex.toLatin1();
}
} // namespace
@@ -181,11 +211,14 @@ int main(int argc, char **argv)
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[LOCATION] source=imu_corrected lat=40.7356 lon=31.5985 height=812.7 "
"[LOCATION] source=imu_corrected mode=dead_reckoning "
"lat=40.7356 lon=31.5985 height=812.7 gps_age_ms=3180 "
"gdop=4.7 pdop=4.1 hdop=2.7 vdop=3.0 tdop=2.3 tracked=6 used=5"));
assert(parsed.has_value());
assert(parsed->hasLocation());
assert(parsed->source == LocationSource::ImuCorrected);
assert(parsed->fusionMode == FusionMode::DeadReckoning);
assert(parsed->gpsAgeMs == 3180);
assert(parsed->satellites == 6);
assert(closeTo(*parsed->dop.hdop, 2.7));
}
@@ -207,6 +240,25 @@ int main(int argc, char **argv)
assert(parsed->satellites == 4);
}
{
constexpr int gpsWeek = 2428;
constexpr double toe = 403200.0;
const QDateTime gpsEpoch(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC);
const QDateTime gpsLabel = gpsEpoch.addSecs(
static_cast<qint64>(gpsWeek * 604800.0 + toe));
const QDateTime matchingUtc = gpsLabel.addSecs(-18);
QString error;
const auto satellites = RinexNavigation::parseGpsNavigation(
syntheticGpsNavigation(gpsLabel, gpsWeek, toe), matchingUtc, &error);
assert(error.isEmpty());
assert(satellites.size() == 1);
const double semiMajorAxis = 5153.7954775 * 5153.7954775;
const double node = -7.2921151467e-5 * toe;
assert(closeTo(satellites[0].x, semiMajorAxis * std::cos(node), 0.1));
assert(closeTo(satellites[0].y, semiMajorAxis * std::sin(node), 0.1));
assert(closeTo(satellites[0].z, 0.0, 0.1));
}
const QString rinexPath = qEnvironmentVariable("LOCATION_PLOTTER_TEST_RINEX");
if (!rinexPath.isEmpty()) {
QFile file(rinexPath);