Add fusion trajectory replay view
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user