Add fusion trajectory replay view
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled

This commit is contained in:
2026-07-16 23:49:55 +03:00
parent 2856c5a560
commit 851642296a
10 changed files with 348 additions and 29 deletions
+20 -1
View File
@@ -49,6 +49,10 @@ QString normalizeKey(QString key)
return "satellites";
}
if (key == "tracked") {
return "satellites";
}
if (key == "gdop" || key == "pdop" || key == "hdop" || key == "vdop" || key == "tdop") {
return key;
}
@@ -76,7 +80,7 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
}
static const QRegularExpression labeledValueRegex(
QStringLiteral(R"((^|[^A-Za-z0-9_])(?<key>lat(?:t?itude)?|long(?:itude)?|lon|lng|hheight|height|alt(?:itude)?|active[_ -]?sat(?:ellite)?s?|num(?:ber)?[_ -]?sat(?:ellite)?s?|sat(?:ellite)?s?[_ -]?active|sat(?:ellite)?s?|sats|uydu(?:lar)?|[gphvt][_ -]?dop)(?:\s*(?:=|:|->)?\s*)(?<value>[+-]?(?:\d+(?:[\.,]\d+)?|[\.,]\d+)(?:[eE][+-]?\d+)?))"),
QStringLiteral(R"((^|[^A-Za-z0-9_])(?<key>lat(?:t?itude)?|long(?:itude)?|lon|lng|hheight|height|alt(?:itude)?|active[_ -]?sat(?:ellite)?s?|num(?:ber)?[_ -]?sat(?:ellite)?s?|sat(?:ellite)?s?[_ -]?active|sat(?:ellite)?s?|sats|tracked|uydu(?:lar)?|[gphvt][_ -]?dop)(?:\s*(?:=|:|->)?\s*)(?<value>[+-]?(?:\d+(?:[\.,]\d+)?|[\.,]\d+)(?:[eE][+-]?\d+)?))"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression satelliteCountPhraseRegex(
QStringLiteral(R"((^|[^A-Za-z0-9_\.])(?<value>\d+)\s+(?:active\s+)?(?:sat(?:ellite)?s?|uydu(?:lar)?)(?=$|[\s,.;)\]]))"),
@@ -84,6 +88,21 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
ParsedTelemetry telemetry;
telemetry.rawLine = trimmed;
static const QRegularExpression sourceRegex(
QStringLiteral(R"((?:^|\s)source\s*=\s*(?<source>gps|imu[_-]?corrected|trajectory|ground[_-]?truth)(?=$|\s))"),
QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch sourceMatch = sourceRegex.match(trimmed);
if (sourceMatch.hasMatch()) {
const QString source = sourceMatch.captured(QStringLiteral("source"));
if (source.startsWith(QStringLiteral("imu"), Qt::CaseInsensitive)) {
telemetry.source = LocationSource::ImuCorrected;
} else if (source.startsWith(QStringLiteral("trajectory"), Qt::CaseInsensitive)
|| source.startsWith(QStringLiteral("ground"), Qt::CaseInsensitive)) {
telemetry.source = LocationSource::Trajectory;
} else {
telemetry.source = LocationSource::Gps;
}
}
bool hadCoordinateField = false;
bool hadLatitudeField = false;
bool hadLongitudeField = false;
+76 -9
View File
@@ -14,6 +14,8 @@
#include <QDir>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QFile>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QHeaderView>
@@ -155,6 +157,17 @@ LocationAnalysis analyzeLocations(const QVector<TelemetryPoint> &points)
return analysis;
}
QVector<TelemetryPoint> preferredAnalysisPoints(const QVector<TelemetryPoint> &points)
{
QVector<TelemetryPoint> corrected;
for (const TelemetryPoint &point : points) {
if (point.source == LocationSource::ImuCorrected) {
corrected.push_back(point);
}
}
return corrected.isEmpty() ? points : corrected;
}
QString metersText(double meters)
{
if (!std::isfinite(meters)) {
@@ -255,7 +268,7 @@ public:
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
return parent.isValid() ? 0 : 6;
return parent.isValid() ? 0 : 7;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
@@ -275,14 +288,16 @@ public:
case 0:
return point.receivedAt.toLocalTime().toString(QStringLiteral("HH:mm:ss.zzz"));
case 1:
return coordinateText(point.latitude, point.latitudeText);
return locationSourceText(point.source);
case 2:
return coordinateText(point.longitude, point.longitudeText);
return coordinateText(point.latitude, point.latitudeText);
case 3:
return optionalDoubleText(point.hheight, 2);
return coordinateText(point.longitude, point.longitudeText);
case 4:
return optionalIntText(point.satellites);
return optionalDoubleText(point.hheight, 2);
case 5:
return optionalIntText(point.satellites);
case 6:
return optionalDopText(point.dop.pdop);
default:
return {};
@@ -296,7 +311,7 @@ public:
return {};
}
static const QStringList headers = {
QStringLiteral("Saat"), QStringLiteral("Lat"), QStringLiteral("Long"),
QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"),
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")};
return section >= 0 && section < headers.size() ? headers[section] : QVariant();
}
@@ -366,6 +381,50 @@ void MainWindow::clearPoints()
resetTelemetryState();
}
bool MainWindow::loadTelemetryLog(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
const QString message = QStringLiteral("Log acilamadi: %1").arg(file.errorString());
m_statusLabel->setText(message);
statusBar()->showMessage(message, 8000);
return false;
}
const QList<QByteArray> lines = file.readAll().split('\n');
resetTelemetryState();
const int replayCapacity = static_cast<int>(std::min<qsizetype>(10000, lines.size()));
m_maxPointsSpin->setValue(std::max(10, replayCapacity));
int parsedLocations = 0;
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
if (line.isEmpty() || line.startsWith(QLatin1Char('#'))) {
continue;
}
const auto telemetry = LocationParser::parseLine(line);
if (telemetry.has_value() && telemetry->hasLocation()) {
++parsedLocations;
}
handleSerialLine(line);
}
flushUiUpdates();
m_map->fitToPoints();
const QString message = QStringLiteral("Replay yuklendi: %1 konum - %2")
.arg(parsedLocations).arg(filePath);
m_statusLabel->setText(message);
statusBar()->showMessage(message, 8000);
return parsedLocations > 0;
}
void MainWindow::openTelemetryLog()
{
const QString filePath = QFileDialog::getOpenFileName(
this, QStringLiteral("Sentetik telemetri logunu ac"), QString(),
QStringLiteral("Telemetry logs (*.log *.txt);;All files (*)"));
if (!filePath.isEmpty()) {
loadTelemetryLog(filePath);
}
}
void MainWindow::handleSerialLine(const QString &line)
{
static const QRegularExpression solveStartRegex(
@@ -698,6 +757,9 @@ void MainWindow::buildUi()
m_connectButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
m_clearButton = new QPushButton(QStringLiteral("Temizle"), this);
m_clearButton->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
m_openLogButton = new QPushButton(QStringLiteral("Senaryo Logu Ac"), this);
m_openLogButton->setObjectName(QStringLiteral("openLogButton"));
m_openLogButton->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton));
m_autoFollowCheck = new QCheckBox(QStringLiteral("Kamerayi takip et"), this);
m_autoFollowCheck->setChecked(true);
@@ -716,6 +778,7 @@ void MainWindow::buildUi()
toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this));
toolbarLayout->addWidget(m_maxPointsSpin);
toolbarLayout->addWidget(m_connectButton);
toolbarLayout->addWidget(m_openLogButton);
toolbarLayout->addWidget(m_clearButton);
toolbarLayout->addWidget(m_autoFollowCheck);
toolbarLayout->addWidget(zoomOutButton);
@@ -932,6 +995,7 @@ void MainWindow::buildUi()
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
&MainWindow::trimToMaxPoints);
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
@@ -974,6 +1038,7 @@ void MainWindow::updateConnectionControls()
void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
{
TelemetryPoint point;
point.source = telemetry.source;
point.latitude = *telemetry.latitude;
point.longitude = *telemetry.longitude;
point.latitudeText = telemetry.latitudeText;
@@ -1028,7 +1093,8 @@ void MainWindow::updateDopLabels()
void MainWindow::updateAnalysis()
{
const LocationAnalysis analysis = analyzeLocations(m_points);
const QVector<TelemetryPoint> analysisPoints = preferredAnalysisPoints(m_points);
const LocationAnalysis analysis = analyzeLocations(analysisPoints);
if (!analysis.valid) {
m_cepLabel->setText(QStringLiteral("-"));
m_meanLocationLabel->setText(QStringLiteral("-"));
@@ -1051,11 +1117,11 @@ void MainWindow::updateAnalysis()
m_map->setAverageMarker(analysis.averageLat, analysis.averageLon,
analysis.rmsScatterMeters);
const double cep50 = calculateCep50(m_points, analysis.averageLat,
const double cep50 = calculateCep50(analysisPoints, analysis.averageLat,
analysis.averageLon);
m_cepLabel->setText(QStringLiteral("%1 (P50, N=%2)")
.arg(metersText(cep50))
.arg(m_points.size()));
.arg(analysisPoints.size()));
m_map->setCepMarker(analysis.averageLat, analysis.averageLon, cep50);
}
@@ -1078,6 +1144,7 @@ void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry)
}
if (telemetry.latitude.has_value()) {
m_pendingTelemetry.source = telemetry.source;
m_pendingTelemetry.latitude = telemetry.latitude;
m_pendingTelemetry.latitudeText = telemetry.latitudeText;
}
+3
View File
@@ -30,6 +30,7 @@ class MainWindow : public QMainWindow {
public:
explicit MainWindow(QWidget *parent = nullptr);
bool loadTelemetryLog(const QString &filePath);
private slots:
void toggleSerial();
@@ -41,6 +42,7 @@ private slots:
void trimToMaxPoints();
void updateReferenceLocation();
void flushUiUpdates();
void openTelemetryLog();
private:
void buildUi();
@@ -70,6 +72,7 @@ private:
QLineEdit *m_referenceLonEdit = nullptr;
QPushButton *m_connectButton = nullptr;
QPushButton *m_clearButton = nullptr;
QPushButton *m_openLogButton = nullptr;
QCheckBox *m_autoFollowCheck = nullptr;
QCheckBox *m_showTelemetryCheck = nullptr;
QCheckBox *m_showRecentPointsCheck = nullptr;
+100 -17
View File
@@ -108,6 +108,41 @@ void MapWidget::clearPoints()
update();
}
void MapWidget::fitToPoints()
{
if (m_points.isEmpty()) {
return;
}
double minimumLatitude = m_points.first().latitude;
double maximumLatitude = minimumLatitude;
double minimumLongitude = m_points.first().longitude;
double maximumLongitude = minimumLongitude;
for (const TelemetryPoint &point : m_points) {
minimumLatitude = std::min(minimumLatitude, point.latitude);
maximumLatitude = std::max(maximumLatitude, point.latitude);
minimumLongitude = std::min(minimumLongitude, point.longitude);
maximumLongitude = std::max(maximumLongitude, point.longitude);
}
m_centerLat = 0.5 * (minimumLatitude + maximumLatitude);
m_centerLon = 0.5 * (minimumLongitude + maximumLongitude);
const double availableWidth = std::max(100.0, width() * 0.82);
const double availableHeight = std::max(100.0, height() * 0.82);
for (int zoom = MaxZoom; zoom >= MinZoom; --zoom) {
const QPointF topLeft = coordinateToWorld(maximumLatitude, minimumLongitude, zoom);
const QPointF bottomRight = coordinateToWorld(minimumLatitude, maximumLongitude, zoom);
if (std::fabs(bottomRight.x() - topLeft.x()) <= availableWidth
&& std::fabs(bottomRight.y() - topLeft.y()) <= availableHeight) {
m_zoom = zoom;
break;
}
}
if (m_autoFollow) {
m_autoFollow = false;
emit autoFollowChanged(false);
}
update();
}
void MapWidget::setAverageMarker(double latitude, double longitude, double radiusMeters)
{
m_hasAverageMarker = true;
@@ -216,6 +251,7 @@ void MapWidget::paintEvent(QPaintEvent *)
painter.fillRect(rect(), QColor(219, 225, 220));
drawTiles(painter);
drawTelemetry(painter);
drawLegend(painter);
if (m_points.isEmpty()) {
drawEmptyState(painter);
@@ -387,23 +423,31 @@ void MapWidget::drawTelemetry(QPainter &painter)
drawCepOverlay(painter);
QPainterPath path;
for (int i = 0; i < m_points.size(); ++i) {
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
if (i == 0) {
path.moveTo(screen);
} else {
path.lineTo(screen);
const auto drawSourcePath = [this, &painter](LocationSource source, const QColor &color) {
QPainterPath path;
bool havePoint = false;
int pointCount = 0;
for (const TelemetryPoint &point : m_points) {
if (point.source != source) {
continue;
}
const QPointF screen = coordinateToScreen(point.latitude, point.longitude);
havePoint ? path.lineTo(screen) : path.moveTo(screen);
havePoint = true;
++pointCount;
}
}
if (m_showRecentPoints && m_points.size() > 1) {
QPen pathPen(QColor(210, 105, 30), 3.0);
pathPen.setCapStyle(Qt::RoundCap);
pathPen.setJoinStyle(Qt::RoundJoin);
painter.setPen(pathPen);
painter.drawPath(path);
}
if (m_showRecentPoints && pointCount > 1) {
QPen pathPen(color, 3.0);
pathPen.setCapStyle(Qt::RoundCap);
pathPen.setJoinStyle(Qt::RoundJoin);
painter.setPen(pathPen);
painter.drawPath(path);
}
};
drawSourcePath(LocationSource::Trajectory, QColor(22, 163, 74));
drawSourcePath(LocationSource::Generic, QColor(210, 105, 30));
drawSourcePath(LocationSource::Gps, QColor(37, 99, 235));
drawSourcePath(LocationSource::ImuCorrected, QColor(220, 38, 38));
drawAverageOverlay(painter);
drawCepMarker(painter);
@@ -415,8 +459,15 @@ void MapWidget::drawTelemetry(QPainter &painter)
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
const double radius = isLast ? 8.0 : 5.0;
const QColor pointColor = m_points[i].source == LocationSource::ImuCorrected
? QColor(220, 38, 38)
: (m_points[i].source == LocationSource::Gps
? QColor(37, 99, 235)
: (m_points[i].source == LocationSource::Trajectory
? QColor(22, 163, 74)
: (isLast ? QColor(210, 105, 30) : QColor(234, 88, 12))));
painter.setPen(QPen(Qt::white, isLast ? 3.0 : 2.0));
painter.setBrush(isLast ? QColor(31, 150, 82) : QColor(37, 99, 235));
painter.setBrush(pointColor);
painter.drawEllipse(screen, radius, radius);
if (drawNumbers) {
@@ -436,6 +487,7 @@ void MapWidget::drawTelemetry(QPainter &painter)
? QString::number(last.longitude, 'f', 15) : last.longitudeText;
lines << QStringLiteral("Lat: %1").arg(latitudeText);
lines << QStringLiteral("Long: %1").arg(longitudeText);
lines << QStringLiteral("Kaynak: %1").arg(locationSourceText(last.source));
if (last.hheight.has_value()) {
lines << QStringLiteral("HHeight: %1").arg(*last.hheight, 0, 'f', 2);
}
@@ -466,6 +518,37 @@ void MapWidget::drawTelemetry(QPainter &painter)
painter.drawText(bubble.adjusted(9, 7, -9, -7), Qt::AlignLeft | Qt::AlignVCenter, label);
}
void MapWidget::drawLegend(QPainter &painter)
{
if (m_points.isEmpty() || !m_showTelemetryOverlay) {
return;
}
const struct {
const char *text;
QColor color;
} entries[] = {
{"Trajectory (truth)", QColor(22, 163, 74)},
{"GPS (4 satellite)", QColor(37, 99, 235)},
{"IMU + ESKF corrected", QColor(220, 38, 38)}
};
const QRectF box(12.0, 12.0, 190.0, 76.0);
painter.setPen(QPen(QColor(50, 58, 65, 180), 1.0));
painter.setBrush(QColor(255, 255, 255, 232));
painter.drawRoundedRect(box, 6.0, 6.0);
painter.setFont(QFont(painter.font().family(), 9, QFont::DemiBold));
for (int index = 0; index < 3; ++index) {
const double y = 27.0 + index * 22.0;
painter.setPen(QPen(entries[index].color, 3.0));
painter.drawLine(QPointF(23.0, y), QPointF(43.0, y));
painter.setBrush(entries[index].color);
painter.drawEllipse(QPointF(33.0, y), 4.0, 4.0);
painter.setPen(QColor(25, 32, 38));
painter.drawText(QRectF(50.0, y - 9.0, 142.0, 18.0),
Qt::AlignLeft | Qt::AlignVCenter,
QString::fromLatin1(entries[index].text));
}
}
void MapWidget::drawCepOverlay(QPainter &painter)
{
if (!m_hasCepMarker || !m_cepRadiusMeters.has_value() || *m_cepRadiusMeters <= 0.0) {
+2
View File
@@ -20,6 +20,7 @@ public:
explicit MapWidget(QWidget *parent = nullptr);
void setPoints(const QVector<TelemetryPoint> &points);
void fitToPoints();
void clearPoints();
void setAverageMarker(double latitude, double longitude, double radiusMeters);
void clearAverageMarker();
@@ -56,6 +57,7 @@ private:
void drawTiles(QPainter &painter);
void drawTelemetry(QPainter &painter);
void drawLegend(QPainter &painter);
void drawAverageOverlay(QPainter &painter);
void drawCepOverlay(QPainter &painter);
void drawCepMarker(QPainter &painter);
+24
View File
@@ -5,6 +5,28 @@
#include <optional>
enum class LocationSource {
Generic,
Gps,
ImuCorrected,
Trajectory
};
inline QString locationSourceText(LocationSource source)
{
switch (source) {
case LocationSource::Gps:
return QStringLiteral("GPS");
case LocationSource::ImuCorrected:
return QStringLiteral("IMU corrected");
case LocationSource::Trajectory:
return QStringLiteral("Trajectory (truth)");
case LocationSource::Generic:
default:
return QStringLiteral("Generic");
}
}
struct DopValues {
std::optional<double> gdop;
std::optional<double> pdop;
@@ -20,6 +42,7 @@ struct DopValues {
};
struct TelemetryPoint {
LocationSource source = LocationSource::Generic;
double latitude = 0.0;
double longitude = 0.0;
QString latitudeText;
@@ -32,6 +55,7 @@ struct TelemetryPoint {
};
struct ParsedTelemetry {
LocationSource source = LocationSource::Generic;
std::optional<double> latitude;
std::optional<double> longitude;
QString latitudeText;
+4
View File
@@ -1,6 +1,7 @@
#include "MainWindow.h"
#include <QApplication>
#include <QFileInfo>
int main(int argc, char *argv[])
{
@@ -9,6 +10,9 @@ int main(int argc, char *argv[])
QApplication::setOrganizationName(QStringLiteral("rocket"));
MainWindow window;
if (argc == 3 && QString::fromLocal8Bit(argv[1]) == QStringLiteral("--replay")) {
window.loadTelemetryLog(QFileInfo(QString::fromLocal8Bit(argv[2])).absoluteFilePath());
}
window.show();
return QApplication::exec();