Merge GUI updates into macOS branch
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled

# Conflicts:
#	src/GlobeWidget.cpp
#	src/GlobeWidget.h
#	src/MainWindow.h
#	src/MapWidget.cpp
#	src/MapWidget.h
#	src/RinexNavigation.cpp
This commit is contained in:
2026-07-24 00:14:47 +03:00
14 changed files with 672 additions and 329 deletions
+7 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20) 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 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -62,6 +62,9 @@ add_executable(location_parser_tests
src/TelemetryPoint.h src/TelemetryPoint.h
) )
target_link_libraries(location_parser_tests PRIVATE Qt6::Core Qt6::Network ZLIB::ZLIB) 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_test(NAME location_parser_tests COMMAND location_parser_tests)
add_executable(location_main_window_tests add_executable(location_main_window_tests
@@ -82,6 +85,9 @@ add_executable(location_main_window_tests
src/TelemetryPoint.h src/TelemetryPoint.h
) )
target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB) 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" qt_add_resources(location_main_window_tests "test_app_resources"
PREFIX "/" PREFIX "/"
FILES resources/ankasys-logo.png resources/land-110m.json resources/bluemarble-2048.png FILES resources/ankasys-logo.png resources/land-110m.json resources/bluemarble-2048.png
+16 -5
View File
@@ -55,10 +55,14 @@ into a single plotted point.
The GPS firmware's level-1 paired protocol is source-aware. Every received The GPS firmware's level-1 paired protocol is source-aware. Every received
`source=gps` location is added to the blue GPS series and every `source=gps` location is added to the blue GPS series and every
`source=imu_corrected` location is immediately added to the red corrected series. `source=imu_corrected` location is added to the red corrected series at the
firmware-configured UART cadence.
Paths are built independently, so alternating UART lines do not create GPS-to-IMU Paths are built independently, so alternating UART lines do not create GPS-to-IMU
zigzags. When corrected points exist, CEP50, average and scatter calculations use zigzags. A corrected point newer than one second is also preferred for map
only that corrected series; legacy input without a source keeps the previous behavior. auto-follow, the current-position label and the globe receiver marker. Raw GPS
outliers remain visible in blue for diagnosis without pulling the whole view away.
When corrected points exist, CEP50, average and scatter calculations use only that
corrected series; legacy input without a source keeps the previous behavior.
Synthetic `source=trajectory` points form a third independent green ground-truth Synthetic `source=trajectory` points form a third independent green ground-truth
path. The map legend identifies all three series. path. The map legend identifies all three series.
@@ -187,7 +191,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 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/cu.*` ve `/dev/tty.*` aygitlarini listeler. Hic aygit algilanmazsa Linux'ta
`/dev/ttyUSB2`, macOS'ta `/dev/cu.usbserial` duzenlenebilir bir varsayilan yol olarak `/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: If the port cannot be opened, make sure the user has serial access:
@@ -218,7 +223,13 @@ without logging out.
mosaic (NASA SVS visualization 2915), projected locally without a runtime network request. mosaic (NASA SVS visualization 2915), projected locally without a runtime network request.
- RINEX download starts at every launch, repeats every 15 minutes, and falls back through the - 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 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, - 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. 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 - The main content width and the right-side dashboard/UART height are both adjustable by
+137 -79
View File
@@ -17,6 +17,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <utility>
namespace { namespace {
constexpr double Pi = 3.14159265358979323846; constexpr double Pi = 3.14159265358979323846;
@@ -222,8 +223,10 @@ GlobeWidget::GlobeWidget(QWidget *parent)
setMouseTracking(true); setMouseTracking(true);
setCursor(Qt::OpenHandCursor); setCursor(Qt::OpenHandCursor);
setToolTip(QStringLiteral("Dunyayi dondurmek icin surukleyin, yakinlastirmak icin tekerlegi kullanin")); setToolTip(QStringLiteral("Dunyayi dondurmek icin surukleyin, yakinlastirmak icin tekerlegi kullanin"));
m_satelliteAnimationTimer.setInterval(33); // GNSS satellites move only a few screen pixels per minute. Ten frames per
m_satelliteAnimationTimer.setTimerType(Qt::PreciseTimer); // 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, connect(&m_satelliteAnimationTimer, &QTimer::timeout,
this, &GlobeWidget::updateSatelliteAnimation); this, &GlobeWidget::updateSatelliteAnimation);
} }
@@ -246,8 +249,29 @@ void GlobeWidget::setSatelliteMotion(const QVector<SatellitePosition> &current,
// Continue from the currently rendered points when a fresh ephemeris arrives, // Continue from the currently rendered points when a fresh ephemeris arrives,
// avoiding even a small visual jump at the update boundary. // avoiding even a small visual jump at the update boundary.
m_satelliteMotionStart = m_satellites.isEmpty() ? current : m_satellites; const QVector<SatellitePosition> &candidateStart =
m_satelliteMotionTarget = next; 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_satelliteMotionDurationMs = durationMs;
m_satelliteMotionClock.restart(); m_satelliteMotionClock.restart();
m_satelliteAnimationTimer.start(); m_satelliteAnimationTimer.start();
@@ -261,18 +285,11 @@ void GlobeWidget::updateSatelliteAnimation()
const double progress = std::clamp( const double progress = std::clamp(
static_cast<double>(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs, static_cast<double>(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs,
0.0, 1.0); 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; QVector<SatellitePosition> interpolated;
interpolated.reserve(m_satelliteMotionStart.size()); interpolated.reserve(m_satelliteMotionStart.size());
for (const SatellitePosition &start : m_satelliteMotionStart) { for (qsizetype i = 0; i < m_satelliteMotionStart.size(); ++i) {
const auto targetIt = targets.constFind(start.prn); const SatellitePosition &start = m_satelliteMotionStart[i];
if (targetIt == targets.cend()) continue; const SatellitePosition &target = m_satelliteMotionTarget[i];
const SatellitePosition &target = targetIt.value();
SatellitePosition point = start; SatellitePosition point = start;
point.x += (target.x - start.x) * progress; point.x += (target.x - start.x) * progress;
point.y += (target.y - start.y) * progress; point.y += (target.y - start.y) * progress;
@@ -347,6 +364,78 @@ void GlobeWidget::paintEvent(QPaintEvent *)
QPainter painter(this); QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::Antialiasing, true);
const qreal devicePixelRatio = devicePixelRatioF();
const qint64 lightingMinute =
QDateTime::currentDateTimeUtc().toSecsSinceEpoch() / 60;
if (m_staticLayer.isNull() || m_staticLayerSize != size()
|| !qFuzzyCompare(m_staticLayerDevicePixelRatio, devicePixelRatio)
|| m_staticLayerYaw != m_yaw || m_staticLayerPitch != m_pitch
|| m_staticLayerZoom != m_zoom
|| m_staticLayerLightingMinute != lightingMinute) {
rebuildStaticLayer();
}
painter.drawPixmap(0, 0, m_staticLayer);
drawSatelliteCoverage(painter);
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()); QLinearGradient background(0, 0, width(), height());
background.setColorAt(0.0, QColor(3, 10, 21)); background.setColorAt(0.0, QColor(3, 10, 21));
background.setColorAt(0.55, QColor(8, 24, 43)); background.setColorAt(0.55, QColor(8, 24, 43));
@@ -401,59 +490,16 @@ void GlobeWidget::paintEvent(QPaintEvent *)
painter.restore(); painter.restore();
for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0); for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0);
drawSatelliteCoverage(painter); painter.end();
if (m_hasReceiver) drawVisibilityCone(painter);
if (m_hasReceiver) drawReceiver(painter);
drawSatellites(painter);
painter.setPen(QColor(229, 240, 249)); m_staticLayer = std::move(layer);
QFont titleFont = painter.font(); m_staticLayerSize = size();
titleFont.setPixelSize(18); m_staticLayerDevicePixelRatio = devicePixelRatio;
titleFont.setBold(true); m_staticLayerYaw = m_yaw;
painter.setFont(titleFont); m_staticLayerPitch = m_pitch;
painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter, m_staticLayerZoom = m_zoom;
QStringLiteral("GNSS YORUNGE GORUNUMU")); m_staticLayerLightingMinute =
QFont smallFont = painter.font(); QDateTime::currentDateTimeUtc().toSecsSinceEpoch() / 60;
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);
painter.setPen(QColor(105, 143, 166, 145));
painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignRight | Qt::AlignVCenter,
QStringLiteral("Earth: NASA/GSFC Blue Marble"));
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"));
}
} }
void GlobeWidget::drawBlueMarble(QPainter &painter) const void GlobeWidget::drawBlueMarble(QPainter &painter) const
@@ -558,9 +604,10 @@ void GlobeWidget::drawBlueMarble(QPainter &painter) const
void GlobeWidget::drawContinents(QPainter &painter) const void GlobeWidget::drawContinents(QPainter &painter) const
{ {
const double radius = std::min(width(), height()) * 0.255 * m_zoom; 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( 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 QPointF center(width() * 0.5, height() * 0.52);
const QRectF target(center.x() - radius, center.y() - radius, const QRectF target(center.x() - radius, center.y() - radius,
radius * 2.0, radius * 2.0); radius * 2.0, radius * 2.0);
@@ -960,33 +1007,41 @@ void GlobeWidget::drawReceiver(QPainter &painter) const
painter.drawText(p.point + QPointF(10, -8), QStringLiteral("ALICI")); 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; 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; QVector<Item> items;
items.reserve(m_satellites.size());
for (const auto &satellite : m_satellites) { for (const auto &satellite : m_satellites) {
const double length = std::sqrt(satellite.x * satellite.x + satellite.y * satellite.y const double length = std::sqrt(satellite.x * satellite.x + satellite.y * satellite.y
+ satellite.z * satellite.z); + satellite.z * satellite.z);
if (length <= 0.0) continue; if (length <= 0.0) continue;
items.push_back({satellite, project(satellite.x / length, satellite.y / length, 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) { std::sort(items.begin(), items.end(), [](const Item &a, const Item &b) {
return a.projected.depth < b.projected.depth; return a.projected.depth < b.projected.depth;
}); });
for (const Item &item : items) {
const bool found = m_observedPrns.contains(item.satellite.prn);
const bool inElevationMask = satelliteElevation(item.satellite) >= m_elevationMaskDegrees;
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 double earthRadius = std::min(width(), height()) * 0.255 * m_zoom;
const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude); const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude);
const auto receiverPoint = project(receiver.x, receiver.y, receiver.z, const auto receiverPoint = project(receiver.x, receiver.y, receiver.z,
earthRadius * 1.02); earthRadius * 1.02);
int visibleCount = 0;
for (const Item &item : items) {
const bool found = m_observedPrns.contains(item.satellite.prn);
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) {
QColor sightLine(70, 224, 231, item.projected.depth < 0.0 ? 25 : 80); QColor sightLine(70, 224, 231, item.projected.depth < 0.0 ? 25 : 80);
painter.setPen(QPen(sightLine, 0.8, Qt::DashLine)); painter.setPen(QPen(sightLine, 0.8, Qt::DashLine));
painter.drawLine(receiverPoint.point, item.projected.point); painter.drawLine(receiverPoint.point, item.projected.point);
@@ -1002,6 +1057,7 @@ void GlobeWidget::drawSatellites(QPainter &painter) const
painter.drawText(item.projected.point + QPointF(7, -6), item.satellite.prn); painter.drawText(item.projected.point + QPointF(7, -6), item.satellite.prn);
} }
} }
return visibleCount;
} }
double GlobeWidget::satelliteElevation(const SatellitePosition &satellite) const double GlobeWidget::satelliteElevation(const SatellitePosition &satellite) const
@@ -1047,7 +1103,9 @@ void GlobeWidget::mouseReleaseEvent(QMouseEvent *event)
{ {
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {
m_dragging = false; m_dragging = false;
m_staticLayer = QPixmap();
setCursor(Qt::OpenHandCursor); setCursor(Qt::OpenHandCursor);
update();
} }
} }
+11 -1
View File
@@ -5,6 +5,7 @@
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QImage> #include <QImage>
#include <QPoint> #include <QPoint>
#include <QPixmap>
#include <QSet> #include <QSet>
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>
@@ -23,6 +24,7 @@ public:
void setReceiverLocation(double latitude, double longitude); void setReceiverLocation(double latitude, double longitude);
void clearReceiverLocation(); void clearReceiverLocation();
void setRinexStatus(const QString &status, bool error); void setRinexStatus(const QString &status, bool error);
[[nodiscard]] QSet<QString> observedPrns() const { return m_observedPrns; }
public slots: public slots:
void setElevationMask(double degrees); void setElevationMask(double degrees);
@@ -50,9 +52,10 @@ private:
void drawVisibilityCone(QPainter &painter) const; void drawVisibilityCone(QPainter &painter) const;
void drawGraticule(QPainter &painter) const; void drawGraticule(QPainter &painter) const;
void drawReceiver(QPainter &painter) const; void drawReceiver(QPainter &painter) const;
void drawSatellites(QPainter &painter) const; int drawSatellites(QPainter &painter) const;
double satelliteElevation(const SatellitePosition &satellite) const; double satelliteElevation(const SatellitePosition &satellite) const;
void updateSatelliteAnimation(); void updateSatelliteAnimation();
void rebuildStaticLayer();
QVector<SatellitePosition> m_satellites; QVector<SatellitePosition> m_satellites;
QVector<SatellitePosition> m_satelliteMotionStart; QVector<SatellitePosition> m_satelliteMotionStart;
@@ -64,6 +67,13 @@ private:
mutable double m_landTextureYaw = 0.0; mutable double m_landTextureYaw = 0.0;
mutable double m_landTexturePitch = 0.0; mutable double m_landTexturePitch = 0.0;
mutable qint64 m_landTextureLightingMinute = -1; mutable qint64 m_landTextureLightingMinute = -1;
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;
qint64 m_staticLayerLightingMinute = -1;
QSet<QString> m_observedPrns; QSet<QString> m_observedPrns;
QString m_status = QStringLiteral("Konum bekleniyor"); QString m_status = QStringLiteral("Konum bekleniyor");
bool m_statusError = false; bool m_statusError = false;
+20
View File
@@ -103,6 +103,26 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
telemetry.source = LocationSource::Gps; 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 hadCoordinateField = false;
bool hadLatitudeField = false; bool hadLatitudeField = false;
bool hadLongitudeField = false; bool hadLongitudeField = false;
+76 -7
View File
@@ -312,7 +312,7 @@ public:
int columnCount(const QModelIndex &parent = QModelIndex()) const override 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 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
@@ -332,6 +332,14 @@ public:
case 0: case 0:
return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz")); return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz"));
case 1: 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); return locationSourceText(point.source);
case 2: case 2:
return coordinateText(point.latitude, point.latitudeText); return coordinateText(point.latitude, point.latitudeText);
@@ -343,6 +351,10 @@ public:
return optionalIntText(point.satellites); return optionalIntText(point.satellites);
case 6: case 6:
return optionalDopText(point.dop.pdop); return optionalDopText(point.dop.pdop);
case 7:
return point.gpsAgeMs.has_value()
? QStringLiteral("%1 ms").arg(*point.gpsAgeMs)
: QStringLiteral("-");
default: default:
return {}; return {};
} }
@@ -356,7 +368,8 @@ public:
} }
static const QStringList headers = { static const QStringList headers = {
QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"), 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(); return section >= 0 && section < headers.size() ? headers[section] : QVariant();
} }
@@ -405,6 +418,14 @@ MainWindow::MainWindow(QWidget *parent)
} }
statusBar()->showMessage(status, error ? 8000 : 4000); 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(); m_rinex->refresh();
auto *rinexRefreshTimer = new QTimer(this); auto *rinexRefreshTimer = new QTimer(this);
rinexRefreshTimer->setInterval(15 * 60 * 1000); rinexRefreshTimer->setInterval(15 * 60 * 1000);
@@ -630,7 +651,12 @@ void MainWindow::handleSerialLine(const QString &line)
static const QRegularExpression solveStartRegex( static const QRegularExpression solveStartRegex(
QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"), QStringLiteral(R"((?:^|\[NAV\]\s*)nav\s+solve\s+(?:start|period)\b)"),
QRegularExpression::CaseInsensitiveOption); 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 solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch();
if (solveStarted) { if (solveStarted) {
// Never combine a coordinate fragment left by the previous navigation // Never combine a coordinate fragment left by the previous navigation
// period with fields from the new one. A complete pending fix is still // period with fields from the new one. A complete pending fix is still
@@ -643,10 +669,14 @@ void MainWindow::handleSerialLine(const QString &line)
m_observedPrns.clear(); m_observedPrns.clear();
} }
const QSet<QString> linePrns = LocationParser::parseGpsPrns(line); 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); m_observedPrns.unite(linePrns);
} }
if (solveStarted || !linePrns.isEmpty()) { if (solveStarted || trackingSnapshot || !linePrns.isEmpty()) {
m_globe->setObservedPrns(m_observedPrns); m_globe->setObservedPrns(m_observedPrns);
} }
@@ -953,7 +983,7 @@ void MainWindow::buildUi()
for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) { for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) {
m_baudCombo->addItem(QString::number(baud)); m_baudCombo->addItem(QString::number(baud));
} }
m_baudCombo->setCurrentText(QStringLiteral("115200")); m_baudCombo->setCurrentText(QStringLiteral("460800"));
m_maxPointsSpin = new QSpinBox(this); m_maxPointsSpin = new QSpinBox(this);
m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput")); m_maxPointsSpin->setObjectName(QStringLiteral("maxPointsInput"));
@@ -1006,7 +1036,25 @@ void MainWindow::buildUi()
auto *viewTabs = new QTabWidget(this); auto *viewTabs = new QTabWidget(this);
viewTabs->setObjectName(QStringLiteral("mainViewTabs")); viewTabs->setObjectName(QStringLiteral("mainViewTabs"));
viewTabs->addTab(m_map, QStringLiteral("2B Harita")); 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); auto *sidePanel = new QWidget(this);
sidePanel->setMinimumWidth(330); sidePanel->setMinimumWidth(330);
@@ -1259,6 +1307,8 @@ void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
{ {
TelemetryPoint point; TelemetryPoint point;
point.source = telemetry.source; point.source = telemetry.source;
point.fusionMode = telemetry.fusionMode;
point.gpsAgeMs = telemetry.gpsAgeMs;
point.latitude = *telemetry.latitude; point.latitude = *telemetry.latitude;
point.longitude = *telemetry.longitude; point.longitude = *telemetry.longitude;
point.latitudeText = telemetry.latitudeText; point.latitudeText = telemetry.latitudeText;
@@ -1270,8 +1320,21 @@ void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
point.rawLine = telemetry.rawLine; point.rawLine = telemetry.rawLine;
point.receivedAt = QDateTime::currentDateTimeUtc(); point.receivedAt = QDateTime::currentDateTimeUtc();
constexpr qint64 CorrectedPreferenceWindowMs = 1000;
bool correctedPositionFresh = false;
for (auto existing = m_points.crbegin(); existing != m_points.crend(); ++existing) {
if (existing->source == LocationSource::ImuCorrected) {
correctedPositionFresh =
existing->receivedAt.msecsTo(point.receivedAt)
<= CorrectedPreferenceWindowMs;
break;
}
}
m_points.push_back(point); m_points.push_back(point);
if (point.source == LocationSource::ImuCorrected || !correctedPositionFresh) {
m_globe->setReceiverLocation(point.latitude, point.longitude); m_globe->setReceiverLocation(point.latitude, point.longitude);
}
if (m_locationTimeoutTimer != nullptr && m_serial->isOpen()) { if (m_locationTimeoutTimer != nullptr && m_serial->isOpen()) {
m_locationTimeoutTimer->start(); m_locationTimeoutTimer->start();
} }
@@ -1363,6 +1426,12 @@ void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry)
commitPendingTelemetry(); 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()) { if (telemetry.latitude.has_value()) {
m_pendingTelemetry.source = telemetry.source; m_pendingTelemetry.source = telemetry.source;
m_pendingTelemetry.latitude = telemetry.latitude; m_pendingTelemetry.latitude = telemetry.latitude;
@@ -1444,5 +1513,5 @@ int MainWindow::selectedBaudRate() const
{ {
bool ok = false; bool ok = false;
const int baudRate = m_baudCombo->currentText().toInt(&ok); const int baudRate = m_baudCombo->currentText().toInt(&ok);
return ok ? baudRate : 115200; return ok ? baudRate : 460800;
} }
+1
View File
@@ -82,6 +82,7 @@ private:
QPushButton *m_clearButton = nullptr; QPushButton *m_clearButton = nullptr;
QPushButton *m_openLogButton = nullptr; QPushButton *m_openLogButton = nullptr;
QPushButton *m_rinexUpdateButton = nullptr; QPushButton *m_rinexUpdateButton = nullptr;
QPushButton *m_refreshRinexButton = nullptr;
QCheckBox *m_autoFollowCheck = nullptr; QCheckBox *m_autoFollowCheck = nullptr;
QCheckBox *m_showTelemetryCheck = nullptr; QCheckBox *m_showTelemetryCheck = nullptr;
QCheckBox *m_showRecentPointsCheck = nullptr; QCheckBox *m_showRecentPointsCheck = nullptr;
+13 -5
View File
@@ -224,6 +224,11 @@ void MapWidget::clearCepMarker()
update(); update();
} }
QPointF MapWidget::centerCoordinate() const
{
return QPointF(m_centerLat, m_centerLon);
}
void MapWidget::setAutoFollow(bool enabled) void MapWidget::setAutoFollow(bool enabled)
{ {
if (m_autoFollow == enabled) { if (m_autoFollow == enabled) {
@@ -779,11 +784,14 @@ void MapWidget::disableAutoFollowFromUserAction()
int MapWidget::preferredFocusIndex() const int MapWidget::preferredFocusIndex() const
{ {
// Once corrected ESKF output is present, both camera and detail bubble constexpr qint64 CorrectedPreferenceWindowMs = 1000;
// stay locked to its newest point. Raw GPS and truth records may arrive const int newestIndex = m_points.size() - 1;
// later in an interleaved stream and must not pull the camera away.
for (int index = m_points.size() - 1; index >= 0; --index) { for (int index = m_points.size() - 1; index >= 0; --index) {
if (m_points[index].source == LocationSource::ImuCorrected) return index; if (m_points[index].source == LocationSource::ImuCorrected) {
const qint64 ageMs = m_points[index].receivedAt.msecsTo(
m_points[newestIndex].receivedAt);
return ageMs <= CorrectedPreferenceWindowMs ? index : newestIndex;
} }
return m_points.isEmpty() ? -1 : m_points.size() - 1; }
return newestIndex;
} }
+1 -1
View File
@@ -27,7 +27,7 @@ public:
void setCepMarker(double latitude, double longitude, void setCepMarker(double latitude, double longitude,
const std::optional<double> &radiusMeters); const std::optional<double> &radiusMeters);
void clearCepMarker(); void clearCepMarker();
[[nodiscard]] QPointF centerCoordinate() const { return {m_centerLat, m_centerLon}; } [[nodiscard]] QPointF centerCoordinate() const;
public slots: public slots:
void setAutoFollow(bool enabled); void setAutoFollow(bool enabled);
+257 -227
View File
@@ -2,6 +2,7 @@
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QHash>
#include <QNetworkReply> #include <QNetworkReply>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QStandardPaths> #include <QStandardPaths>
@@ -13,6 +14,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <utility>
namespace { namespace {
@@ -23,29 +25,6 @@ constexpr double HalfGpsWeek = GpsWeekSeconds / 2.0;
constexpr double CurrentGpsUtcOffsetSeconds = 18.0; constexpr double CurrentGpsUtcOffsetSeconds = 18.0;
constexpr int PositionUpdateIntervalMs = 60000; 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) double parseField(const QString &line, int field)
{ {
QString text = line.mid(4 + field * 19, 19).trimmed(); QString text = line.mid(4 + field * 19, 19).trimmed();
@@ -91,11 +70,261 @@ QByteArray gunzip(const QByteArray &input)
QDateTime gpsEpoch() QDateTime gpsEpoch()
{ {
return QDateTime(QDate(1980, 1, 6), QTime(0, 0), return QDateTime(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC);
QTimeZone::fromSecondsAheadOfUtc(0));
} }
SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc) } // 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<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;
}
setLoading(false);
if (!loadCache()) {
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 UTC")
.arg(positions.size())
.arg(date.toString(QStringLiteral("dd.MM.yyyy")))
.arg(now.time().toString(QStringLiteral("HH:mm:ss"))), false);
}
bool RinexNavigation::loadCache()
{
QFile cache(cachePath());
if (!cache.open(QIODevice::ReadOnly)) return false;
QString 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 · %2 UTC")
.arg(positions.size())
.arg(now.time().toString(QStringLiteral("HH:mm:ss"))), false);
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<Ephemeris> selected = selectEphemerides(m_ephemerides, now);
const QVector<SatellitePosition> current = propagateEphemerides(selected, now);
if (current.isEmpty()) return;
const QVector<SatellitePosition> 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::Ephemeris> 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<Ephemeris> 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<int>(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<int>(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::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 const double gpsSeconds = gpsEpoch().msecsTo(whenUtc) / 1000.0
+ CurrentGpsUtcOffsetSeconds; + CurrentGpsUtcOffsetSeconds;
@@ -138,208 +367,9 @@ SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc)
return result; return result;
} }
} // 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_latestRinex.isEmpty()) {
loadCache();
}
m_loading = 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 {
m_loading = false;
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;
const QVector<SatellitePosition> positions = parseGpsNavigation(
rinex, QDateTime::currentDateTimeUtc(), &error);
if (positions.isEmpty()) {
if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1);
return;
}
m_loading = false;
if (!loadCache()) {
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_latestRinex = rinex;
m_loading = false;
publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 (UTC)")
.arg(positions.size()).arg(date.toString(QStringLiteral("dd.MM.yyyy"))), false);
}
bool RinexNavigation::loadCache()
{
QFile cache(cachePath());
if (!cache.open(QIODevice::ReadOnly)) return false;
QString error;
const QByteArray cachedRinex = cache.readAll();
const QVector<SatellitePosition> positions = parseGpsNavigation(
cachedRinex, QDateTime::currentDateTimeUtc(), &error);
if (positions.isEmpty()) return false;
m_latestRinex = cachedRinex;
publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX").arg(positions.size()), false);
return true;
}
QString RinexNavigation::cachePath() const
{
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+ QStringLiteral("/latest-navigation.rnx");
}
void RinexNavigation::updateCurrentPositions()
{
if (m_latestRinex.isEmpty()) return;
publishCurrentMotion();
}
void RinexNavigation::publishCurrentMotion()
{
if (m_latestRinex.isEmpty()) return;
const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> current = parseGpsNavigation(m_latestRinex, now);
if (current.isEmpty()) return;
const QVector<SatellitePosition> next = parseGpsNavigation(
m_latestRinex, now.addMSecs(PositionUpdateIntervalMs));
emit satellitesUpdated(current);
if (!next.isEmpty()) {
emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs);
}
}
QVector<SatellitePosition> RinexNavigation::parseGpsNavigation( QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
const QByteArray &rinex, const QDateTime &whenUtc, QString *error) const QByteArray &rinex, const QDateTime &whenUtc, QString *error)
{ {
const QString text = QString::fromLatin1(rinex); const QVector<Ephemeris> ephemerides = parseEphemerides(rinex, error);
const QStringList lines = text.split('\n'); return propagateEphemerides(selectEphemerides(ephemerides, whenUtc), whenUtc);
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;
QHash<QString, Ephemeris> closest;
QHash<QString, qint64> closestAge;
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<int>(epoch[5].toDouble())),
QTimeZone::fromSecondsAheadOfUtc(0));
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<int>(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) {
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;
}
}
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;
} }
+33 -1
View File
@@ -26,17 +26,49 @@ signals:
const QVector<SatellitePosition> &next, const QVector<SatellitePosition> &next,
int durationMs); int durationMs);
void statusChanged(const QString &status, bool error); void statusChanged(const QString &status, bool error);
void loadingChanged(bool loading);
private: 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 requestDay(const QDate &date, int fallbackIndex);
void handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex); void handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex);
bool loadCache(); bool loadCache();
QString cachePath() const; QString cachePath() const;
void updateCurrentPositions(); void updateCurrentPositions();
void publishCurrentMotion(); 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; QNetworkAccessManager m_network;
QTimer m_positionTimer; QTimer m_positionTimer;
QByteArray m_latestRinex; QVector<Ephemeris> m_ephemerides;
bool m_loading = false; bool m_loading = false;
}; };
+10
View File
@@ -12,6 +12,12 @@ enum class LocationSource {
Trajectory Trajectory
}; };
enum class FusionMode {
Unknown,
GpsAided,
DeadReckoning
};
inline QString locationSourceText(LocationSource source) inline QString locationSourceText(LocationSource source)
{ {
switch (source) { switch (source) {
@@ -43,6 +49,8 @@ struct DopValues {
struct TelemetryPoint { struct TelemetryPoint {
LocationSource source = LocationSource::Generic; LocationSource source = LocationSource::Generic;
FusionMode fusionMode = FusionMode::Unknown;
std::optional<qint64> gpsAgeMs;
double latitude = 0.0; double latitude = 0.0;
double longitude = 0.0; double longitude = 0.0;
QString latitudeText; QString latitudeText;
@@ -56,6 +64,8 @@ struct TelemetryPoint {
struct ParsedTelemetry { struct ParsedTelemetry {
LocationSource source = LocationSource::Generic; LocationSource source = LocationSource::Generic;
FusionMode fusionMode = FusionMode::Unknown;
std::optional<qint64> gpsAgeMs;
std::optional<double> latitude; std::optional<double> latitude;
std::optional<double> longitude; std::optional<double> longitude;
QString latitudeText; QString latitudeText;
+36
View File
@@ -1,4 +1,5 @@
#include "../src/MainWindow.h" #include "../src/MainWindow.h"
#include "../src/GlobeWidget.h"
#include "../src/MapWidget.h" #include "../src/MapWidget.h"
#include <QApplication> #include <QApplication>
@@ -54,6 +55,8 @@ int main(int argc, char **argv)
auto *showRecentPoints = window.findChild<QCheckBox *>(QStringLiteral("showRecentPointsCheck")); auto *showRecentPoints = window.findChild<QCheckBox *>(QStringLiteral("showRecentPointsCheck"));
auto *showAverage = window.findChild<QCheckBox *>(QStringLiteral("showAverageCheck")); auto *showAverage = window.findChild<QCheckBox *>(QStringLiteral("showAverageCheck"));
auto *portSelector = window.findChild<QComboBox *>(QStringLiteral("portSelector")); auto *portSelector = window.findChild<QComboBox *>(QStringLiteral("portSelector"));
auto *refreshRinexButton =
window.findChild<QPushButton *>(QStringLiteral("refreshRinexButton"));
auto *map = window.findChild<MapWidget *>(); auto *map = window.findChild<MapWidget *>();
auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs")); auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs"));
auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter")); auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter"));
@@ -76,6 +79,9 @@ int main(int argc, char **argv)
assert(showRecentPoints != nullptr); assert(showRecentPoints != nullptr);
assert(showAverage != nullptr); assert(showAverage != nullptr);
assert(portSelector != nullptr); assert(portSelector != nullptr);
assert(refreshRinexButton != nullptr);
assert(refreshRinexButton->text() == QStringLiteral("RINEX Guncelle")
|| refreshRinexButton->text() == QStringLiteral("RINEX indiriliyor..."));
assert(portSelector->isEditable()); assert(portSelector->isEditable());
assert(portSelector->count() >= 1); assert(portSelector->count() >= 1);
assert(portSelector->currentText() == QStringLiteral("/dev/test-usb-uart")); assert(portSelector->currentText() == QStringLiteral("/dev/test-usb-uart"));
@@ -115,6 +121,15 @@ int main(int argc, char **argv)
assert(std::abs(map->centerCoordinate().y() - 29.5) < 0.000001); assert(std::abs(map->centerCoordinate().y() - 29.5) < 0.000001);
map->clearPoints(); map->clearPoints();
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, assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection,
Q_ARG(double, 40.25), Q_ARG(double, 29.5))); Q_ARG(double, 40.25), Q_ARG(double, 29.5)));
assert(referenceLatInput->text() == QStringLiteral("40.250000000000000")); assert(referenceLatInput->text() == QStringLiteral("40.250000000000000"));
@@ -236,6 +251,27 @@ int main(int argc, char **argv)
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection)); assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
maxPointsInput->setValue(10); maxPointsInput->setValue(10);
{
MapWidget followMap;
TelemetryPoint corrected;
corrected.source = LocationSource::ImuCorrected;
corrected.latitude = 40.1;
corrected.longitude = 29.1;
corrected.receivedAt = QDateTime::currentDateTimeUtc();
TelemetryPoint rawGps;
rawGps.source = LocationSource::Gps;
rawGps.latitude = 41.0;
rawGps.longitude = 30.0;
rawGps.receivedAt = corrected.receivedAt.addMSecs(100);
followMap.setPoints({corrected, rawGps});
assert(followMap.centerCoordinate() == QPointF(40.1, 29.1));
rawGps.receivedAt = corrected.receivedAt.addMSecs(1100);
followMap.setPoints({corrected, rawGps});
assert(followMap.centerCoordinate() == QPointF(41.0, 30.0));
}
const QByteArray screenshotPath = qgetenv("LOCATION_PLOTTER_TEST_SCREENSHOT"); const QByteArray screenshotPath = qgetenv("LOCATION_PLOTTER_TEST_SCREENSHOT");
if (!screenshotPath.isEmpty()) { if (!screenshotPath.isEmpty()) {
sendLine(window, QStringLiteral("NAV solve start sats:5")); sendLine(window, QStringLiteral("NAV solve start sats:5"));
+55 -3
View File
@@ -9,9 +9,39 @@
namespace { 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 } // namespace
@@ -181,11 +211,14 @@ int main(int argc, char **argv)
{ {
const auto parsed = LocationParser::parseLine(QStringLiteral( 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")); "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.has_value());
assert(parsed->hasLocation()); assert(parsed->hasLocation());
assert(parsed->source == LocationSource::ImuCorrected); assert(parsed->source == LocationSource::ImuCorrected);
assert(parsed->fusionMode == FusionMode::DeadReckoning);
assert(parsed->gpsAgeMs == 3180);
assert(parsed->satellites == 6); assert(parsed->satellites == 6);
assert(closeTo(*parsed->dop.hdop, 2.7)); assert(closeTo(*parsed->dop.hdop, 2.7));
} }
@@ -207,6 +240,25 @@ int main(int argc, char **argv)
assert(parsed->satellites == 4); 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"); const QString rinexPath = qEnvironmentVariable("LOCATION_PLOTTER_TEST_RINEX");
if (!rinexPath.isEmpty()) { if (!rinexPath.isEmpty()) {
QFile file(rinexPath); QFile file(rinexPath);