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
+51
View File
@@ -24,6 +24,9 @@ VDOP : 2.2370935468
TDOP : 2.2526165852 TDOP : 2.2526165852
tracking PRNs: G03, G08, 14, 21 tracking PRNs: G03, G08, 14, 21
SV=G27 SV=G27
[LOCATION] source=gps lat=40.7357 lon=31.5985 height=812.0 gdop=4.7 pdop=4.1 hdop=2.7 vdop=3.0 tdop=2.3 tracked=6 used=5
[LOCATION] source=imu_corrected lat=40.7356 lon=31.5985 height=812.7 gdop=4.7 pdop=4.1 hdop=2.7 vdop=3.0 tdop=2.3 tracked=6 used=5
[LOCATION] source=trajectory lat=40.7358 lon=31.5987 height=812.1 tracked=4 used=4
lattitude : 40.78925034214206846173 lattitude : 40.78925034214206846173
longitude : 29.46277530841727809729 longitude : 29.46277530841727809729
height : -89.05779508221894502640 height : -89.05779508221894502640
@@ -38,6 +41,9 @@ Accepted location labels:
`activeSats`, `satellites_active`, `num_sats`, `uydu` `activeSats`, `satellites_active`, `num_sats`, `uydu`
- DOP: `GDOP`, `PDOP`, `HDOP`, `VDOP`, `TDOP` - DOP: `GDOP`, `PDOP`, `HDOP`, `VDOP`, `TDOP`
- GPS identifiers: `G03`, `PRN: 3`, `PRNs: 3, 8, 14`, `SV=G21`, `SVID: 27` - GPS identifiers: `G03`, `PRN: 3`, `PRNs: 3, 8, 14`, `SV=G21`, `SVID: 27`
- Location source: `source=gps`, `source=imu_corrected`, `source=trajectory`
- `tracked` is accepted as the active satellite count; `used` remains visible in
the raw UART line and denotes satellites used by the WLS solution.
If a satellite count arrives without a location, the active satellite display is updated. If a satellite count arrives without a location, the active satellite display is updated.
If a location arrives without a satellite count, the latest known satellite count is attached If a location arrives without a satellite count, the latest known satellite count is attached
@@ -47,6 +53,48 @@ section and are attached to the next plotted point.
If latitude, longitude, and height arrive as separate consecutive lines, they are combined If latitude, longitude, and height arrive as separate consecutive lines, they are combined
into a single plotted point. 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.
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.
Synthetic `source=trajectory` points form a third independent green ground-truth
path. The map legend identifies all three series.
## Sentetik ESKF senaryolarini açma
GPS deposundan 48 kombinasyonun replay loglarini üretin:
```bash
cd /home/onur/Desktop/gps
make -C soc/software/fusion/tests scenario-logs
```
Sonra GUI'yi açıp `Senaryo Logu Ac` düğmesinden örneğin
`soc/software/fusion/tests/output/scenarios/car_gps200_imu20.log` dosyasını seçin.
GUI logdaki bütün rotayı otomatik sığdırır. Aynı dosya komut satırından da
doğrudan açılabilir:
```bash
cd /home/onur/Desktop/location_plotter
cmake -S . -B build
cmake --build build -j
./build/location_plotter --replay \
/home/onur/Desktop/gps/soc/software/fusion/tests/output/scenarios/car_gps200_imu20.log
```
Tek bir özel kombinasyon üretmek için:
```bash
cd /home/onur/Desktop/gps/soc/software/fusion/tests
make platform_scenario_matrix_test
./platform_scenario_matrix_test --log aircraft 500 19 aircraft.log
```
Desteklenen senaryolar `stationary`, `pedestrian`, `car`, `aircraft`; GNSS
periyotları 200/500/1000 ms, IMU periyotları 19/20/200/300 ms'dir.
## Ubuntu'da kurulum ## Ubuntu'da kurulum
Ubuntu 22.04 ve daha yeni surumlerde gerekli paketleri kurmak, uygulamayi derlemek ve Ubuntu 22.04 ve daha yeni surumlerde gerekli paketleri kurmak, uygulamayi derlemek ve
@@ -136,6 +184,9 @@ without logging out.
marker can be toggled on the map. marker can be toggled on the map.
- The map layers panel can hide all telemetry overlays, show only the newest location instead - The map layers panel can hide all telemetry overlays, show only the newest location instead
of the recent path, and independently toggle the average marker. of the recent path, and independently toggle the average marker.
- GPS points and paths are blue. IMU+ESKF corrected points and their independent
path are red. Synthetic ground-truth trajectory is green; the locations table
shows the parsed source in its `Kaynak` column.
- The application calculates empirical CEP50 as the median horizontal distance of the retained - The application calculates empirical CEP50 as the median horizontal distance of the retained
measurements from their spherical mean, then draws the circle around that measurement center. measurements from their spherical mean, then draws the circle around that measurement center.
This is the radius containing 50 percent of the retained position fixes and is independent of This is the radius containing 50 percent of the retained position fixes and is independent of
+20 -1
View File
@@ -49,6 +49,10 @@ QString normalizeKey(QString key)
return "satellites"; return "satellites";
} }
if (key == "tracked") {
return "satellites";
}
if (key == "gdop" || key == "pdop" || key == "hdop" || key == "vdop" || key == "tdop") { if (key == "gdop" || key == "pdop" || key == "hdop" || key == "vdop" || key == "tdop") {
return key; return key;
} }
@@ -76,7 +80,7 @@ std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
} }
static const QRegularExpression labeledValueRegex( 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); QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression satelliteCountPhraseRegex( static const QRegularExpression satelliteCountPhraseRegex(
QStringLiteral(R"((^|[^A-Za-z0-9_\.])(?<value>\d+)\s+(?:active\s+)?(?:sat(?:ellite)?s?|uydu(?:lar)?)(?=$|[\s,.;)\]]))"), 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; ParsedTelemetry telemetry;
telemetry.rawLine = trimmed; 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 hadCoordinateField = false;
bool hadLatitudeField = false; bool hadLatitudeField = false;
bool hadLongitudeField = false; bool hadLongitudeField = false;
+76 -9
View File
@@ -14,6 +14,8 @@
#include <QDir> #include <QDir>
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
#include <QFormLayout> #include <QFormLayout>
#include <QFile>
#include <QFileDialog>
#include <QGridLayout> #include <QGridLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QHeaderView> #include <QHeaderView>
@@ -155,6 +157,17 @@ LocationAnalysis analyzeLocations(const QVector<TelemetryPoint> &points)
return analysis; 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) QString metersText(double meters)
{ {
if (!std::isfinite(meters)) { if (!std::isfinite(meters)) {
@@ -255,7 +268,7 @@ public:
int columnCount(const QModelIndex &parent = QModelIndex()) const override 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 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
@@ -275,14 +288,16 @@ 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:
return coordinateText(point.latitude, point.latitudeText); return locationSourceText(point.source);
case 2: case 2:
return coordinateText(point.longitude, point.longitudeText); return coordinateText(point.latitude, point.latitudeText);
case 3: case 3:
return optionalDoubleText(point.hheight, 2); return coordinateText(point.longitude, point.longitudeText);
case 4: case 4:
return optionalIntText(point.satellites); return optionalDoubleText(point.hheight, 2);
case 5: case 5:
return optionalIntText(point.satellites);
case 6:
return optionalDopText(point.dop.pdop); return optionalDopText(point.dop.pdop);
default: default:
return {}; return {};
@@ -296,7 +311,7 @@ public:
return {}; return {};
} }
static const QStringList headers = { static const QStringList headers = {
QStringLiteral("Saat"), QStringLiteral("Lat"), QStringLiteral("Long"), QStringLiteral("Saat"), QStringLiteral("Kaynak"), QStringLiteral("Lat"), QStringLiteral("Long"),
QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")}; QStringLiteral("HHeight"), QStringLiteral("Uydu"), QStringLiteral("PDOP")};
return section >= 0 && section < headers.size() ? headers[section] : QVariant(); return section >= 0 && section < headers.size() ? headers[section] : QVariant();
} }
@@ -366,6 +381,50 @@ void MainWindow::clearPoints()
resetTelemetryState(); 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) void MainWindow::handleSerialLine(const QString &line)
{ {
static const QRegularExpression solveStartRegex( static const QRegularExpression solveStartRegex(
@@ -698,6 +757,9 @@ void MainWindow::buildUi()
m_connectButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); m_connectButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
m_clearButton = new QPushButton(QStringLiteral("Temizle"), this); m_clearButton = new QPushButton(QStringLiteral("Temizle"), this);
m_clearButton->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); 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 = new QCheckBox(QStringLiteral("Kamerayi takip et"), this);
m_autoFollowCheck->setChecked(true); m_autoFollowCheck->setChecked(true);
@@ -716,6 +778,7 @@ void MainWindow::buildUi()
toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this)); toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this));
toolbarLayout->addWidget(m_maxPointsSpin); toolbarLayout->addWidget(m_maxPointsSpin);
toolbarLayout->addWidget(m_connectButton); toolbarLayout->addWidget(m_connectButton);
toolbarLayout->addWidget(m_openLogButton);
toolbarLayout->addWidget(m_clearButton); toolbarLayout->addWidget(m_clearButton);
toolbarLayout->addWidget(m_autoFollowCheck); toolbarLayout->addWidget(m_autoFollowCheck);
toolbarLayout->addWidget(zoomOutButton); toolbarLayout->addWidget(zoomOutButton);
@@ -932,6 +995,7 @@ void MainWindow::buildUi()
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial); connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints); connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this, connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
&MainWindow::trimToMaxPoints); &MainWindow::trimToMaxPoints);
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow); connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
@@ -974,6 +1038,7 @@ void MainWindow::updateConnectionControls()
void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry) void MainWindow::addTelemetryPoint(const ParsedTelemetry &telemetry)
{ {
TelemetryPoint point; TelemetryPoint point;
point.source = telemetry.source;
point.latitude = *telemetry.latitude; point.latitude = *telemetry.latitude;
point.longitude = *telemetry.longitude; point.longitude = *telemetry.longitude;
point.latitudeText = telemetry.latitudeText; point.latitudeText = telemetry.latitudeText;
@@ -1028,7 +1093,8 @@ void MainWindow::updateDopLabels()
void MainWindow::updateAnalysis() void MainWindow::updateAnalysis()
{ {
const LocationAnalysis analysis = analyzeLocations(m_points); const QVector<TelemetryPoint> analysisPoints = preferredAnalysisPoints(m_points);
const LocationAnalysis analysis = analyzeLocations(analysisPoints);
if (!analysis.valid) { if (!analysis.valid) {
m_cepLabel->setText(QStringLiteral("-")); m_cepLabel->setText(QStringLiteral("-"));
m_meanLocationLabel->setText(QStringLiteral("-")); m_meanLocationLabel->setText(QStringLiteral("-"));
@@ -1051,11 +1117,11 @@ void MainWindow::updateAnalysis()
m_map->setAverageMarker(analysis.averageLat, analysis.averageLon, m_map->setAverageMarker(analysis.averageLat, analysis.averageLon,
analysis.rmsScatterMeters); analysis.rmsScatterMeters);
const double cep50 = calculateCep50(m_points, analysis.averageLat, const double cep50 = calculateCep50(analysisPoints, analysis.averageLat,
analysis.averageLon); analysis.averageLon);
m_cepLabel->setText(QStringLiteral("%1 (P50, N=%2)") m_cepLabel->setText(QStringLiteral("%1 (P50, N=%2)")
.arg(metersText(cep50)) .arg(metersText(cep50))
.arg(m_points.size())); .arg(analysisPoints.size()));
m_map->setCepMarker(analysis.averageLat, analysis.averageLon, cep50); m_map->setCepMarker(analysis.averageLat, analysis.averageLon, cep50);
} }
@@ -1078,6 +1144,7 @@ void MainWindow::mergePendingTelemetry(const ParsedTelemetry &telemetry)
} }
if (telemetry.latitude.has_value()) { if (telemetry.latitude.has_value()) {
m_pendingTelemetry.source = telemetry.source;
m_pendingTelemetry.latitude = telemetry.latitude; m_pendingTelemetry.latitude = telemetry.latitude;
m_pendingTelemetry.latitudeText = telemetry.latitudeText; m_pendingTelemetry.latitudeText = telemetry.latitudeText;
} }
+3
View File
@@ -30,6 +30,7 @@ class MainWindow : public QMainWindow {
public: public:
explicit MainWindow(QWidget *parent = nullptr); explicit MainWindow(QWidget *parent = nullptr);
bool loadTelemetryLog(const QString &filePath);
private slots: private slots:
void toggleSerial(); void toggleSerial();
@@ -41,6 +42,7 @@ private slots:
void trimToMaxPoints(); void trimToMaxPoints();
void updateReferenceLocation(); void updateReferenceLocation();
void flushUiUpdates(); void flushUiUpdates();
void openTelemetryLog();
private: private:
void buildUi(); void buildUi();
@@ -70,6 +72,7 @@ private:
QLineEdit *m_referenceLonEdit = nullptr; QLineEdit *m_referenceLonEdit = nullptr;
QPushButton *m_connectButton = nullptr; QPushButton *m_connectButton = nullptr;
QPushButton *m_clearButton = nullptr; QPushButton *m_clearButton = nullptr;
QPushButton *m_openLogButton = 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;
+100 -17
View File
@@ -108,6 +108,41 @@ void MapWidget::clearPoints()
update(); 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) void MapWidget::setAverageMarker(double latitude, double longitude, double radiusMeters)
{ {
m_hasAverageMarker = true; m_hasAverageMarker = true;
@@ -216,6 +251,7 @@ void MapWidget::paintEvent(QPaintEvent *)
painter.fillRect(rect(), QColor(219, 225, 220)); painter.fillRect(rect(), QColor(219, 225, 220));
drawTiles(painter); drawTiles(painter);
drawTelemetry(painter); drawTelemetry(painter);
drawLegend(painter);
if (m_points.isEmpty()) { if (m_points.isEmpty()) {
drawEmptyState(painter); drawEmptyState(painter);
@@ -387,23 +423,31 @@ void MapWidget::drawTelemetry(QPainter &painter)
drawCepOverlay(painter); drawCepOverlay(painter);
QPainterPath path; const auto drawSourcePath = [this, &painter](LocationSource source, const QColor &color) {
for (int i = 0; i < m_points.size(); ++i) { QPainterPath path;
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude); bool havePoint = false;
if (i == 0) { int pointCount = 0;
path.moveTo(screen); for (const TelemetryPoint &point : m_points) {
} else { if (point.source != source) {
path.lineTo(screen); continue;
}
const QPointF screen = coordinateToScreen(point.latitude, point.longitude);
havePoint ? path.lineTo(screen) : path.moveTo(screen);
havePoint = true;
++pointCount;
} }
} if (m_showRecentPoints && pointCount > 1) {
QPen pathPen(color, 3.0);
if (m_showRecentPoints && m_points.size() > 1) { pathPen.setCapStyle(Qt::RoundCap);
QPen pathPen(QColor(210, 105, 30), 3.0); pathPen.setJoinStyle(Qt::RoundJoin);
pathPen.setCapStyle(Qt::RoundCap); painter.setPen(pathPen);
pathPen.setJoinStyle(Qt::RoundJoin); painter.drawPath(path);
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); drawAverageOverlay(painter);
drawCepMarker(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 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;
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.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); painter.drawEllipse(screen, radius, radius);
if (drawNumbers) { if (drawNumbers) {
@@ -436,6 +487,7 @@ void MapWidget::drawTelemetry(QPainter &painter)
? QString::number(last.longitude, 'f', 15) : last.longitudeText; ? QString::number(last.longitude, 'f', 15) : last.longitudeText;
lines << QStringLiteral("Lat: %1").arg(latitudeText); lines << QStringLiteral("Lat: %1").arg(latitudeText);
lines << QStringLiteral("Long: %1").arg(longitudeText); lines << QStringLiteral("Long: %1").arg(longitudeText);
lines << QStringLiteral("Kaynak: %1").arg(locationSourceText(last.source));
if (last.hheight.has_value()) { if (last.hheight.has_value()) {
lines << QStringLiteral("HHeight: %1").arg(*last.hheight, 0, 'f', 2); 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); 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) void MapWidget::drawCepOverlay(QPainter &painter)
{ {
if (!m_hasCepMarker || !m_cepRadiusMeters.has_value() || *m_cepRadiusMeters <= 0.0) { if (!m_hasCepMarker || !m_cepRadiusMeters.has_value() || *m_cepRadiusMeters <= 0.0) {
+2
View File
@@ -20,6 +20,7 @@ public:
explicit MapWidget(QWidget *parent = nullptr); explicit MapWidget(QWidget *parent = nullptr);
void setPoints(const QVector<TelemetryPoint> &points); void setPoints(const QVector<TelemetryPoint> &points);
void fitToPoints();
void clearPoints(); void clearPoints();
void setAverageMarker(double latitude, double longitude, double radiusMeters); void setAverageMarker(double latitude, double longitude, double radiusMeters);
void clearAverageMarker(); void clearAverageMarker();
@@ -56,6 +57,7 @@ private:
void drawTiles(QPainter &painter); void drawTiles(QPainter &painter);
void drawTelemetry(QPainter &painter); void drawTelemetry(QPainter &painter);
void drawLegend(QPainter &painter);
void drawAverageOverlay(QPainter &painter); void drawAverageOverlay(QPainter &painter);
void drawCepOverlay(QPainter &painter); void drawCepOverlay(QPainter &painter);
void drawCepMarker(QPainter &painter); void drawCepMarker(QPainter &painter);
+24
View File
@@ -5,6 +5,28 @@
#include <optional> #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 { struct DopValues {
std::optional<double> gdop; std::optional<double> gdop;
std::optional<double> pdop; std::optional<double> pdop;
@@ -20,6 +42,7 @@ struct DopValues {
}; };
struct TelemetryPoint { struct TelemetryPoint {
LocationSource source = LocationSource::Generic;
double latitude = 0.0; double latitude = 0.0;
double longitude = 0.0; double longitude = 0.0;
QString latitudeText; QString latitudeText;
@@ -32,6 +55,7 @@ struct TelemetryPoint {
}; };
struct ParsedTelemetry { struct ParsedTelemetry {
LocationSource source = LocationSource::Generic;
std::optional<double> latitude; std::optional<double> latitude;
std::optional<double> longitude; std::optional<double> longitude;
QString latitudeText; QString latitudeText;
+4
View File
@@ -1,6 +1,7 @@
#include "MainWindow.h" #include "MainWindow.h"
#include <QApplication> #include <QApplication>
#include <QFileInfo>
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
@@ -9,6 +10,9 @@ int main(int argc, char *argv[])
QApplication::setOrganizationName(QStringLiteral("rocket")); QApplication::setOrganizationName(QStringLiteral("rocket"));
MainWindow window; MainWindow window;
if (argc == 3 && QString::fromLocal8Bit(argv[1]) == QStringLiteral("--replay")) {
window.loadTelemetryLog(QFileInfo(QString::fromLocal8Bit(argv[2])).absoluteFilePath());
}
window.show(); window.show();
return QApplication::exec(); return QApplication::exec();
+40 -2
View File
@@ -12,6 +12,7 @@
#include <QTableView> #include <QTableView>
#include <QTabWidget> #include <QTabWidget>
#include <QSplitter> #include <QSplitter>
#include <QTemporaryFile>
#include <cassert> #include <cassert>
@@ -99,11 +100,48 @@ int main(int argc, char **argv)
sendLine(window, QStringLiteral("NAV solve done")); sendLine(window, QStringLiteral("NAV solve done"));
flushUpdates(window); flushUpdates(window);
assert(table->model()->rowCount() == 1); assert(table->model()->rowCount() == 1);
assert(table->model()->data(table->model()->index(0, 1)).toString()
== QStringLiteral("40.78925034214206846173"));
assert(table->model()->data(table->model()->index(0, 2)).toString() assert(table->model()->data(table->model()->index(0, 2)).toString()
== QStringLiteral("40.78925034214206846173"));
assert(table->model()->data(table->model()->index(0, 3)).toString()
== QStringLiteral("29.46277530841727809729")); == QStringLiteral("29.46277530841727809729"));
sendLine(window, QStringLiteral(
"[LOCATION] source=gps lat=40.1 lon=29.1 height=100 gdop=2 pdop=1.5 "
"hdop=1 vdop=1.2 tdop=0.8 tracked=5 used=5"));
sendLine(window, QStringLiteral(
"[LOCATION] source=imu_corrected lat=40.10001 lon=29.10001 height=100.2 "
"gdop=2 pdop=1.5 hdop=1 vdop=1.2 tdop=0.8 tracked=5 used=5"));
flushUpdates(window);
assert(table->model()->rowCount() == 3);
assert(table->model()->data(table->model()->index(1, 1)).toString()
== QStringLiteral("GPS"));
assert(table->model()->data(table->model()->index(2, 1)).toString()
== QStringLiteral("IMU corrected"));
sendLine(window, QStringLiteral(
"[LOCATION] source=trajectory lat=40.10002 lon=29.10002 height=100 tracked=4 used=4"));
flushUpdates(window);
assert(table->model()->rowCount() == 4);
assert(table->model()->data(table->model()->index(3, 1)).toString()
== QStringLiteral("Trajectory (truth)"));
QTemporaryFile replayLog;
assert(replayLog.open());
replayLog.write(
"[LOCATION] source=trajectory lat=40.0 lon=29.0 height=100 tracked=4 used=4\n"
"[LOCATION] source=gps lat=40.0001 lon=29.0001 height=104 tracked=4 used=4\n"
"[LOCATION] source=imu_corrected lat=40.00002 lon=29.00002 height=101 tracked=4 used=4\n");
replayLog.flush();
assert(window.loadTelemetryLog(replayLog.fileName()));
assert(table->model()->rowCount() == 3);
assert(table->model()->data(table->model()->index(0, 1)).toString()
== QStringLiteral("Trajectory (truth)"));
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
sendLine(window, QStringLiteral(
"lat=40.78925034214206846173 long=29.46277530841727809729"));
flushUpdates(window);
assert(table->model()->rowCount() == 1);
referenceLatInput->setText(QStringLiteral("40.78925034214206846173")); referenceLatInput->setText(QStringLiteral("40.78925034214206846173"));
referenceLonInput->setText(QStringLiteral("29.46277530841727809729")); referenceLonInput->setText(QStringLiteral("29.46277530841727809729"));
const bool referenceUpdated = QMetaObject::invokeMethod( const bool referenceUpdated = QMetaObject::invokeMethod(
+28
View File
@@ -179,6 +179,34 @@ int main(int argc, char **argv)
assert(closeTo(*parsed->hheight, 198.80186179187148809433)); assert(closeTo(*parsed->hheight, 198.80186179187148809433));
} }
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[LOCATION] source=imu_corrected lat=40.7356 lon=31.5985 height=812.7 "
"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->satellites == 6);
assert(closeTo(*parsed->dop.hdop, 2.7));
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[LOCATION] source=gps lat=40.7357 lon=31.5986 height=811 tracked=5 used=5"));
assert(parsed.has_value());
assert(parsed->source == LocationSource::Gps);
assert(parsed->satellites == 5);
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[LOCATION] source=trajectory lat=40.7358 lon=31.5987 height=810 tracked=4 used=4"));
assert(parsed.has_value());
assert(parsed->hasLocation());
assert(parsed->source == LocationSource::Trajectory);
assert(parsed->satellites == 4);
}
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);