Compare commits

...

2 Commits

Author SHA1 Message Date
onur 8b7256ed9d Prefer corrected position for map following
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
2026-07-23 23:55:25 +03:00
onur 24b08ee518 Improve satellite view and fused location telemetry 2026-07-23 23:19:52 +03:00
14 changed files with 560 additions and 202 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 FILES resources/ankasys-logo.png resources/land-110m.json
+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:
@@ -216,7 +221,13 @@ without logging out.
receiver marker without resetting a globe orientation chosen by dragging. 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 - 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
+129 -73
View File
@@ -15,6 +15,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <utility>
namespace { namespace {
constexpr double Pi = 3.14159265358979323846; constexpr double Pi = 3.14159265358979323846;
@@ -211,8 +212,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);
} }
@@ -235,8 +238,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();
@@ -250,18 +274,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;
@@ -336,6 +353,74 @@ void GlobeWidget::paintEvent(QPaintEvent *)
QPainter painter(this); QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true); 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()); 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));
@@ -374,63 +459,23 @@ 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);
if (m_hasReceiver) drawVisibilityCone(painter); painter.end();
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"));
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"));
}
} }
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);
@@ -715,33 +760,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;
}); });
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) { for (const Item &item : items) {
const bool found = m_observedPrns.contains(item.satellite.prn); 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); QColor color = found ? QColor(42, 224, 137) : QColor(255, 79, 95);
if (item.projected.depth < 0.0) color.setAlpha(75); if (item.projected.depth < 0.0) color.setAlpha(75);
const double markerRadius = found ? 6.5 : 5.0; const double markerRadius = found ? 6.5 : 5.0;
if (inElevationMask) { 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); 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);
@@ -757,6 +810,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
@@ -802,7 +856,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();
} }
} }
+10 -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);
@@ -47,9 +49,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;
@@ -60,6 +63,12 @@ private:
mutable QImage m_landTexture; mutable QImage m_landTexture;
mutable double m_landTextureYaw = 0.0; mutable double m_landTextureYaw = 0.0;
mutable double m_landTexturePitch = 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; 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;
+77 -8
View File
@@ -307,7 +307,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
@@ -327,6 +327,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);
@@ -338,6 +346,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 {};
} }
@@ -351,7 +363,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();
} }
@@ -386,6 +399,14 @@ MainWindow::MainWindow(QWidget *parent)
m_globe->setRinexStatus(status, error); m_globe->setRinexStatus(status, error);
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);
@@ -497,15 +518,24 @@ 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) {
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);
} }
@@ -809,7 +839,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"));
@@ -856,7 +886,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(390); sidePanel->setMinimumWidth(390);
@@ -1105,6 +1153,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;
@@ -1116,8 +1166,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);
m_globe->setReceiverLocation(point.latitude, point.longitude); if (point.source == LocationSource::ImuCorrected || !correctedPositionFresh) {
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();
} }
@@ -1209,6 +1272,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;
@@ -1290,5 +1359,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
@@ -74,6 +74,7 @@ private:
QPushButton *m_connectButton = nullptr; QPushButton *m_connectButton = nullptr;
QPushButton *m_clearButton = nullptr; QPushButton *m_clearButton = nullptr;
QPushButton *m_openLogButton = nullptr; QPushButton *m_openLogButton = 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;
+31 -10
View File
@@ -52,6 +52,20 @@ double metersPerPixel(double latitude, int zoom)
return latitudeScale * 2.0 * Pi * WebMercatorRadiusMeters / scale; return latitudeScale * 2.0 * Pi * WebMercatorRadiusMeters / scale;
} }
int preferredCurrentPointIndex(const QVector<TelemetryPoint> &points)
{
constexpr qint64 CorrectedPreferenceWindowMs = 1000;
const int newestIndex = points.size() - 1;
for (int index = points.size() - 1; index >= 0; --index) {
if (points[index].source == LocationSource::ImuCorrected) {
const qint64 ageMs = points[index].receivedAt.msecsTo(
points[newestIndex].receivedAt);
return ageMs <= CorrectedPreferenceWindowMs ? index : newestIndex;
}
}
return newestIndex;
}
} // namespace } // namespace
MapWidget::MapWidget(QWidget *parent) MapWidget::MapWidget(QWidget *parent)
@@ -90,9 +104,9 @@ void MapWidget::setPoints(const QVector<TelemetryPoint> &points)
m_points = points; m_points = points;
if (!m_points.isEmpty() && m_autoFollow) { if (!m_points.isEmpty() && m_autoFollow) {
const TelemetryPoint &last = m_points.last(); const TelemetryPoint &current = m_points[preferredCurrentPointIndex(m_points)];
m_centerLat = last.latitude; m_centerLat = current.latitude;
m_centerLon = last.longitude; m_centerLon = current.longitude;
if (wasEmpty) { if (wasEmpty) {
m_zoom = MaxZoom; m_zoom = MaxZoom;
} }
@@ -191,6 +205,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) {
@@ -199,9 +218,9 @@ void MapWidget::setAutoFollow(bool enabled)
m_autoFollow = enabled; m_autoFollow = enabled;
if (m_autoFollow && !m_points.isEmpty()) { if (m_autoFollow && !m_points.isEmpty()) {
const TelemetryPoint &last = m_points.last(); const TelemetryPoint &current = m_points[preferredCurrentPointIndex(m_points)];
m_centerLat = last.latitude; m_centerLat = current.latitude;
m_centerLon = last.longitude; m_centerLon = current.longitude;
update(); update();
} }
emit autoFollowChanged(m_autoFollow); emit autoFollowChanged(m_autoFollow);
@@ -453,9 +472,11 @@ void MapWidget::drawTelemetry(QPainter &painter)
drawCepMarker(painter); drawCepMarker(painter);
const bool drawNumbers = m_points.size() <= 150; const bool drawNumbers = m_points.size() <= 150;
const int firstPoint = m_showRecentPoints ? 0 : m_points.size() - 1; const int currentPointIndex = preferredCurrentPointIndex(m_points);
for (int i = firstPoint; i < m_points.size(); ++i) { const int firstPoint = m_showRecentPoints ? 0 : currentPointIndex;
const bool isLast = i == m_points.size() - 1; const int endPoint = m_showRecentPoints ? m_points.size() : currentPointIndex + 1;
for (int i = firstPoint; i < endPoint; ++i) {
const bool isLast = i == currentPointIndex;
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude); const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
const double radius = isLast ? 8.0 : 5.0; const double radius = isLast ? 8.0 : 5.0;
@@ -478,7 +499,7 @@ void MapWidget::drawTelemetry(QPainter &painter)
} }
} }
const TelemetryPoint &last = m_points.last(); const TelemetryPoint &last = m_points[currentPointIndex];
const QPointF lastScreen = coordinateToScreen(last.latitude, last.longitude); const QPointF lastScreen = coordinateToScreen(last.latitude, last.longitude);
QStringList lines; QStringList lines;
const QString latitudeText = last.latitudeText.isEmpty() const QString latitudeText = last.latitudeText.isEmpty()
+1
View File
@@ -27,6 +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;
public slots: public slots:
void setAutoFollow(bool enabled); void setAutoFollow(bool enabled);
+133 -100
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>
@@ -12,6 +13,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <utility>
namespace { namespace {
@@ -22,29 +24,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();
@@ -93,49 +72,6 @@ QDateTime gpsEpoch()
return QDateTime(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC); 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 } // namespace
RinexNavigation::RinexNavigation(QObject *parent) RinexNavigation::RinexNavigation(QObject *parent)
@@ -150,10 +86,10 @@ RinexNavigation::RinexNavigation(QObject *parent)
void RinexNavigation::refresh() void RinexNavigation::refresh()
{ {
if (m_loading) return; if (m_loading) return;
if (m_latestRinex.isEmpty()) { if (m_ephemerides.isEmpty()) {
loadCache(); loadCache();
} }
m_loading = true; setLoading(true);
emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false); emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false);
requestDay(QDateTime::currentDateTimeUtc().date(), 0); requestDay(QDateTime::currentDateTimeUtc().date(), 0);
} }
@@ -181,8 +117,13 @@ void RinexNavigation::requestDay(const QDate &date, int fallbackIndex)
if (fallbackIndex < 2) { if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1); requestDay(date.addDays(-1), fallbackIndex + 1);
} else { } else {
m_loading = false; setLoading(false);
if (!loadCache()) { 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); 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); const QByteArray rinex = gunzip(compressed);
QString error; QString error;
const QVector<SatellitePosition> positions = parseGpsNavigation( QVector<Ephemeris> ephemerides = parseEphemerides(rinex, &error);
rinex, QDateTime::currentDateTimeUtc(), &error); const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> positions = propagateEphemerides(
selectEphemerides(ephemerides, now), now);
if (positions.isEmpty()) { if (positions.isEmpty()) {
if (fallbackIndex < 2) { if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1); requestDay(date.addDays(-1), fallbackIndex + 1);
return; return;
} }
m_loading = false; setLoading(false);
emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true); emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true);
return; return;
} }
@@ -212,11 +155,13 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
QFile cache(cachePath()); QFile cache(cachePath());
QDir().mkpath(QFileInfo(cache).absolutePath()); QDir().mkpath(QFileInfo(cache).absolutePath());
if (cache.open(QIODevice::WriteOnly)) cache.write(rinex); if (cache.open(QIODevice::WriteOnly)) cache.write(rinex);
m_latestRinex = rinex; m_ephemerides = std::move(ephemerides);
m_loading = false; setLoading(false);
publishCurrentMotion(); publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 (UTC)") emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 · %3 UTC")
.arg(positions.size()).arg(date.toString(QStringLiteral("dd.MM.yyyy"))), false); .arg(positions.size())
.arg(date.toString(QStringLiteral("dd.MM.yyyy")))
.arg(now.time().toString(QStringLiteral("HH:mm:ss"))), false);
} }
bool RinexNavigation::loadCache() bool RinexNavigation::loadCache()
@@ -224,12 +169,16 @@ bool RinexNavigation::loadCache()
QFile cache(cachePath()); QFile cache(cachePath());
if (!cache.open(QIODevice::ReadOnly)) return false; if (!cache.open(QIODevice::ReadOnly)) return false;
QString error; QString error;
m_latestRinex = cache.readAll(); QVector<Ephemeris> ephemerides = parseEphemerides(cache.readAll(), &error);
const QVector<SatellitePosition> positions = parseGpsNavigation( const QDateTime now = QDateTime::currentDateTimeUtc();
m_latestRinex, QDateTime::currentDateTimeUtc(), &error); const QVector<SatellitePosition> positions = propagateEphemerides(
selectEphemerides(ephemerides, now), now);
if (positions.isEmpty()) return false; if (positions.isEmpty()) return false;
m_ephemerides = std::move(ephemerides);
publishCurrentMotion(); 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; return true;
} }
@@ -241,28 +190,36 @@ QString RinexNavigation::cachePath() const
void RinexNavigation::updateCurrentPositions() void RinexNavigation::updateCurrentPositions()
{ {
if (m_latestRinex.isEmpty()) return; if (m_ephemerides.isEmpty()) return;
publishCurrentMotion(); publishCurrentMotion();
} }
void RinexNavigation::publishCurrentMotion() void RinexNavigation::publishCurrentMotion()
{ {
if (m_latestRinex.isEmpty()) return; if (m_ephemerides.isEmpty()) return;
const QDateTime now = QDateTime::currentDateTimeUtc(); 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; if (current.isEmpty()) return;
const QVector<SatellitePosition> next = parseGpsNavigation( const QVector<SatellitePosition> next = propagateEphemerides(
m_latestRinex, now.addMSecs(PositionUpdateIntervalMs)); selected, now.addMSecs(PositionUpdateIntervalMs));
emit satellitesUpdated(current); emit satellitesUpdated(current);
if (!next.isEmpty()) { if (!next.isEmpty()) {
emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs); emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs);
} }
} }
QVector<SatellitePosition> RinexNavigation::parseGpsNavigation( void RinexNavigation::setLoading(bool loading)
const QByteArray &rinex, const QDateTime &whenUtc, QString *error) {
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 QString text = QString::fromLatin1(rinex);
const QStringList lines = text.split('\n'); const QStringList lines = text.split('\n');
@@ -274,8 +231,8 @@ QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
} }
++index; ++index;
QHash<QString, Ephemeris> closest; QVector<Ephemeris> result;
QHash<QString, qint64> closestAge; result.reserve(512);
while (index < lines.size()) { while (index < lines.size()) {
const QString first = lines[index]; const QString first = lines[index];
if (first.size() < 3) { ++index; continue; } 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.cus, eph.sqrtA, eph.toe, eph.cic, eph.omega0, eph.cis,
eph.i0, eph.crc, eph.omega, eph.omegaDot, eph.idot}) eph.i0, eph.crc, eph.omega, eph.omegaDot, eph.idot})
&& eph.sqrtA > 4000.0 && eph.gpsWeek > 0) { && eph.sqrtA > 4000.0 && eph.gpsWeek > 0) {
const qint64 age = std::abs(toc.secsTo(whenUtc)); result.push_back(eph);
if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) {
closest[eph.prn] = eph;
closestAge[eph.prn] = age;
}
} }
index += 8; 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"); if (result.isEmpty() && error) *error = QStringLiteral("GPS efemerisi bulunamadi");
return result; 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, 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;
+37
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>
@@ -9,6 +10,7 @@
#include <QLineEdit> #include <QLineEdit>
#include <QMetaObject> #include <QMetaObject>
#include <QSpinBox> #include <QSpinBox>
#include <QPushButton>
#include <QTableView> #include <QTableView>
#include <QTabWidget> #include <QTabWidget>
#include <QSplitter> #include <QSplitter>
@@ -51,6 +53,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"));
@@ -69,6 +73,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"));
@@ -82,6 +89,15 @@ int main(int argc, char **argv)
assert(sideSplitter != nullptr); assert(sideSplitter != nullptr);
assert(sideSplitter->orientation() == Qt::Vertical); 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, 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"));
@@ -185,6 +201,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);