Add fusion trajectory replay view
This commit is contained in:
@@ -24,6 +24,9 @@ VDOP : 2.2370935468
|
||||
TDOP : 2.2526165852
|
||||
tracking PRNs: G03, G08, 14, 21
|
||||
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
|
||||
longitude : 29.46277530841727809729
|
||||
height : -89.05779508221894502640
|
||||
@@ -38,6 +41,9 @@ Accepted location labels:
|
||||
`activeSats`, `satellites_active`, `num_sats`, `uydu`
|
||||
- DOP: `GDOP`, `PDOP`, `HDOP`, `VDOP`, `TDOP`
|
||||
- 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 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
|
||||
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 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.
|
||||
- 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.
|
||||
- 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
|
||||
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
|
||||
|
||||
+20
-1
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <QTableView>
|
||||
#include <QTabWidget>
|
||||
#include <QSplitter>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
@@ -99,11 +100,48 @@ int main(int argc, char **argv)
|
||||
sendLine(window, QStringLiteral("NAV solve done"));
|
||||
flushUpdates(window);
|
||||
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()
|
||||
== QStringLiteral("40.78925034214206846173"));
|
||||
assert(table->model()->data(table->model()->index(0, 3)).toString()
|
||||
== 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"));
|
||||
referenceLonInput->setText(QStringLiteral("29.46277530841727809729"));
|
||||
const bool referenceUpdated = QMetaObject::invokeMethod(
|
||||
|
||||
@@ -179,6 +179,34 @@ int main(int argc, char **argv)
|
||||
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");
|
||||
if (!rinexPath.isEmpty()) {
|
||||
QFile file(rinexPath);
|
||||
|
||||
Reference in New Issue
Block a user