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