Initial commit: add Location Plotter
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 13:15:54 +03:00
commit 2856c5a560
25 changed files with 4630 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
name: Ubuntu build
on:
push:
pull_request:
jobs:
build-and-test:
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-24.04]
runs-on: ${{ matrix.os }}
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake qt6-base-dev zlib1g-dev
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --parallel 2
- name: Test
run: ctest --test-dir build --output-on-failure
+10
View File
@@ -0,0 +1,10 @@
build/
build-*/
.cache/
.cmake/
.vscode/
.idea/
*.user
*.autosave
*.swp
*~
+84
View File
@@ -0,0 +1,84 @@
cmake_minimum_required(VERSION 3.20)
project(location_plotter VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network)
find_package(ZLIB REQUIRED)
add_executable(location_plotter
src/LocationParser.cpp
src/LocationParser.h
src/GlobeWidget.cpp
src/GlobeWidget.h
src/MainWindow.cpp
src/MainWindow.h
src/MapWidget.cpp
src/MapWidget.h
src/SerialReader.cpp
src/SerialReader.h
src/RinexNavigation.cpp
src/RinexNavigation.h
src/SatelliteData.h
src/TelemetryPoint.h
src/main.cpp
)
target_link_libraries(location_plotter PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
qt_add_resources(location_plotter "app_resources"
PREFIX "/"
FILES resources/ankasys-logo.png resources/land-110m.json
)
if(UNIX AND NOT APPLE)
target_compile_definitions(location_plotter PRIVATE LOCATION_PLOTTER_LINUX_SERIAL=1)
endif()
enable_testing()
add_executable(location_parser_tests
tests/parser_tests.cpp
src/LocationParser.cpp
src/LocationParser.h
src/RinexNavigation.cpp
src/RinexNavigation.h
src/SatelliteData.h
src/TelemetryPoint.h
)
target_link_libraries(location_parser_tests PRIVATE Qt6::Core Qt6::Network ZLIB::ZLIB)
add_test(NAME location_parser_tests COMMAND location_parser_tests)
add_executable(location_main_window_tests
tests/main_window_tests.cpp
src/LocationParser.cpp
src/LocationParser.h
src/MainWindow.cpp
src/MainWindow.h
src/MapWidget.cpp
src/MapWidget.h
src/GlobeWidget.cpp
src/GlobeWidget.h
src/SerialReader.cpp
src/SerialReader.h
src/RinexNavigation.cpp
src/RinexNavigation.h
src/SatelliteData.h
src/TelemetryPoint.h
)
target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
qt_add_resources(location_main_window_tests "test_app_resources"
PREFIX "/"
FILES resources/ankasys-logo.png resources/land-110m.json
)
if(UNIX AND NOT APPLE)
target_compile_definitions(location_main_window_tests PRIVATE LOCATION_PLOTTER_LINUX_SERIAL=1)
endif()
add_test(NAME location_main_window_tests COMMAND location_main_window_tests)
set_tests_properties(location_main_window_tests PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-test-cache")
+156
View File
@@ -0,0 +1,156 @@
# Location Plotter
Qt6/C++ GUI that listens to `/dev/ttyUSB2`, parses normal debug lines, and plots live
locations on a map.
The application also includes an interactive 3D Earth/orbit view. It downloads the current
stream-derived RINEX 3 navigation file from the BKG/IGS archive at startup and every 15
minutes, then recomputes GPS satellite positions for current UTC every minute. The last valid
file is cached for offline startup.
## Parsed fields
The parser accepts labeled values in the same line or in separate lines:
```text
lat=41.0082 long=28.9784 hheight=123.45 sats=7
latitude: 41.0082 longitude: 28.9784 height: 123.45 activeSats: 7
active_sats=8
navigation software running with 5 satellites
GDOP : 3.9576744550
PDOP : 3.2540598660
HDOP : 2.3631161789
VDOP : 2.2370935468
TDOP : 2.2526165852
tracking PRNs: G03, G08, 14, 21
SV=G27
lattitude : 40.78925034214206846173
longitude : 29.46277530841727809729
height : -89.05779508221894502640
```
Accepted location labels:
- Latitude: `lat`, `latitude`, `lattitude`
- Longitude: `long`, `longitude`, `lon`, `lng`
- Height: `hheight`, `height`, `alt`, `altitude`
- Active satellites: `sat`, `sats`, `satellite`, `satellites`, `active_sats`,
`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`
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
to that point.
If DOP values arrive before a location, the latest known values stay visible in the DOP
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.
## Ubuntu'da kurulum
Ubuntu 22.04 ve daha yeni surumlerde gerekli paketleri kurmak, uygulamayi derlemek ve
testleri calistirmak icin:
```bash
git clone <depo-adresi> location_plotter
cd location_plotter
./setup-ubuntu.sh
./run.sh
```
`setup-ubuntu.sh`, gereken Ubuntu paketlerini `apt` ile kurar, Release build olusturur,
testleri calistirir ve kullaniciyi seri port erisimi icin `dialout` grubuna ekler.
Kurulum sirasinda `sudo` parolasi istenebilir.
Depo ZIP olarak indirildiyse de klasore girdikten sonra ayni iki betik calistirilabilir.
## Elle derleme
Gerekli paketler:
```bash
sudo apt update
sudo apt install build-essential cmake qt6-base-dev zlib1g-dev
```
Derleme ve test:
```bash
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
```
Run:
```bash
./run.sh
```
Uygulama Qt tabanli bir masaustu arayuzudur; grafik oturumu olan bir Ubuntu makinede
calistirilmalidir. Harita karolari ve guncel RINEX verisi icin internet erisimi gerekir,
ancak son gecerli RINEX verisi ve harita onbellegiyle cevrimdisi kullanim da desteklenir.
The port selector lists only detected `/dev/ttyUSB*` devices. The default selection is
`/dev/ttyUSB2` and the default baud rate is `115200`.
If the port cannot be opened, make sure the user has serial access:
```bash
sudo usermod -a -G dialout "$USER"
```
Log out and back in after changing group membership.
If the user is already listed in the `dialout` group but the current shell has not picked it
up yet, `./run.sh` starts the GUI through `sg dialout` so it can access `/dev/ttyUSB2`
without logging out.
## Notes
- The map uses OpenStreetMap tiles through Qt Network. If there is no internet connection,
points are still drawn over a fallback grid.
- The `3B Dunya · Uydular` tab appears beside the map. It uses broadcast ephemeris values
from BKG's near-real-time `BRDC00WRD_S` RINEX file to propagate GPS satellites in ECEF.
PRNs parsed from the current navigation solve are green; other RINEX satellites are red.
Drag to rotate the globe and use the mouse wheel to zoom.
- The globe includes a shaded continent layer and a receiver-centred translucent visibility
cone. `Min. elevation` accepts a directly typed decimal value from 0 to 90 degrees
(10 degrees by default, press Enter after typing);
satellites inside the mask get a cyan sight line and halo. Incoming positions update the
receiver marker without resetting a globe orientation chosen by dragging.
- RINEX download starts at every launch, repeats every 15 minutes, and falls back through the
previous two UTC days if today's stream file is unavailable. Current and next-minute satellite
positions are propagated from the cached ephemeris, then smoothly interpolated at 30 FPS.
- The globe uses an embedded Natural Earth coastline mask with terrain-like land colouring,
shallow-water edges, atmospheric shading, and a cached Earth-fixed texture for smooth rotation.
- The main content width and the right-side dashboard/UART height are both adjustable by
dragging their splitter handles. Dashboard cards scroll independently so the UART log can
be enlarged substantially.
- `Max` controls how many recent points stay visible and are included in location
analysis. The default is 10. When the limit is reached, older points are removed
automatically.
- Location analysis shows the average of the plotted samples, the last point's deviation
from that average, mean deviation, RMS scatter, and max deviation in meters. The average
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.
- 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
the optional user reference.
- A user reference can be selected by right-clicking the map. The selected WGS84 coordinates
are transferred to the reference fields with 15 decimal places but do not alter CEP50.
- Latitude and longitude tokens are preserved exactly as received for the table and map
label. Calculations and map projection use IEEE 754 `double` precision.
- The OpenStreetMap Standard raster layer supports zoom levels through 19. At latitude 40.8,
zoom 19 is approximately 0.23 meters per pixel; the marker still uses subpixel coordinates,
but the background tiles cannot add accuracy to the GNSS measurement.
- OpenStreetMap attribution is shown on the map. Tile responses use a 100 MB persistent HTTP
cache in the application's standard cache directory.
- UART log and telemetry views are refreshed in batches so a high-rate serial stream does not
rebuild the complete interface for every received line. The locations table uses a virtual
model and remains usable with a high `Max` value.
- If no new plotted location arrives for 10 seconds while listening, current location
points and analysis values are cleared.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long
Executable
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
print_qt_install_hint() {
echo
echo "Qt6 bulunamadi. Ubuntu'da gerekli paketleri kurmak icin:"
echo " sudo apt update"
echo " sudo apt install build-essential cmake qt6-base-dev"
}
project_dir="$(pwd -P)"
if [[ -f ./build/CMakeCache.txt ]]; then
cached_source="$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' ./build/CMakeCache.txt)"
if [[ -n "$cached_source" && "$cached_source" != "$project_dir" ]]; then
echo "Build klasoru farkli bir proje yoluna ait, yeniden olusturuluyor:"
echo " $cached_source"
rm -rf ./build
fi
fi
if ! cmake -S . -B build; then
print_qt_install_hint
exit 1
fi
cmake --build build
missing_libs="$(ldd ./build/location_plotter 2>/dev/null | awk '/not found/ { print $1 }' || true)"
if [[ -n "$missing_libs" ]]; then
echo "Eksik paylasimli kutuphaneler:"
echo "$missing_libs" | sed 's/^/ /'
print_qt_install_hint
exit 1
fi
if id -nG | tr ' ' '\n' | grep -qx dialout; then
exec ./build/location_plotter "$@"
fi
if getent group dialout | tr ':' '\n' | tail -n 1 | tr ',' '\n' | grep -qx "$(id -un)"; then
exec sg dialout -c "$(printf '%q ' ./build/location_plotter "$@")"
fi
echo "Bu kullanici dialout grubunda degil. Eklemek icin:"
echo " sudo usermod -a -G dialout $(id -un)"
echo "Sonra oturumu kapatip ac veya bu scripti tekrar calistir."
exit 1
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
if [[ ! -r /etc/os-release ]]; then
echo "Ubuntu surumu tespit edilemedi." >&2
exit 1
fi
# shellcheck disable=SC1091
source /etc/os-release
if [[ "${ID:-}" != "ubuntu" ]]; then
echo "Bu kurulum betigi Ubuntu icindir (tespit edilen: ${PRETTY_NAME:-bilinmiyor})." >&2
exit 1
fi
current_user="${SUDO_USER:-${USER:-$(id -un)}}"
if ! dpkg --compare-versions "${VERSION_ID:-0}" ge 22.04; then
echo "Ubuntu 22.04 veya daha yeni bir surum gerekiyor (tespit edilen: ${VERSION_ID:-bilinmiyor})." >&2
exit 1
fi
if ! command -v sudo >/dev/null 2>&1; then
echo "Paketleri kurmak icin sudo komutu gerekiyor." >&2
exit 1
fi
echo "Gerekli Ubuntu paketleri kuruluyor..."
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
qt6-base-dev \
zlib1g-dev
if getent group dialout >/dev/null && ! id -nG "$current_user" | tr ' ' '\n' | grep -qx dialout; then
echo "Seri port erisimi icin $current_user kullanicisi dialout grubuna ekleniyor..."
sudo usermod -a -G dialout "$current_user"
fi
echo "Uygulama derleniyor..."
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel "$(nproc)"
echo "Testler calistiriliyor..."
ctest --test-dir build --output-on-failure
echo
echo "Kurulum tamamlandi. Uygulamayi baslatmak icin:"
echo " ./run.sh"
+814
View File
@@ -0,0 +1,814 @@
#include "GlobeWidget.h"
#include <QLinearGradient>
#include <QHash>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPolygonF>
#include <QRadialGradient>
#include <QWheelEvent>
#include <algorithm>
#include <cmath>
namespace {
constexpr double Pi = 3.14159265358979323846;
constexpr double InitialLongitudeOffset = 0.50;
constexpr double InitialLatitudeOffset = 0.28;
struct Vec3 { double x; double y; double z; };
Vec3 globeVector(double latitude, double longitude)
{
const double lat = latitude * Pi / 180.0;
const double lon = longitude * Pi / 180.0;
return {std::cos(lat) * std::cos(lon), std::cos(lat) * std::sin(lon), std::sin(lat)};
}
const QVector<QPolygonF> &fallbackLandPolygons()
{
// Deliberately simplified Natural-Earth-like coastlines. QPointF is lon/lat.
static const QVector<QPolygonF> polygons = {
// North America
{{-168, 66}, {-155, 72}, {-138, 70}, {-128, 61}, {-123, 50}, {-124, 42},
{-117, 32}, {-106, 24}, {-97, 19}, {-90, 19}, {-83, 24}, {-81, 31},
{-75, 39}, {-66, 45}, {-60, 52}, {-64, 60}, {-78, 63}, {-89, 69},
{-105, 73}, {-125, 72}, {-145, 75}},
// Central America
{{-106, 24}, {-97, 18}, {-91, 16}, {-87, 12}, {-83, 9}, {-79, 8},
{-77, 10}, {-84, 16}, {-91, 20}},
// South America
{{-81, 12}, {-72, 12}, {-61, 8}, {-50, 3}, {-43, -5}, {-35, -8},
{-39, -19}, {-48, -29}, {-53, -36}, {-60, -47}, {-67, -55},
{-74, -50}, {-75, -38}, {-71, -28}, {-70, -18}, {-76, -7}},
// Greenland
{{-73, 60}, {-52, 59}, {-39, 65}, {-20, 76}, {-28, 83}, {-49, 84},
{-65, 78}},
// Europe and Asia
{{-11, 36}, {-10, 44}, {-5, 50}, {4, 54}, {12, 58}, {20, 69},
{34, 71}, {48, 77}, {72, 76}, {96, 78}, {119, 73}, {142, 71},
{165, 66}, {178, 58}, {169, 50}, {151, 46}, {141, 38}, {128, 34},
{121, 24}, {110, 20}, {105, 10}, {98, 7}, {91, 21}, {80, 8},
{72, 18}, {62, 25}, {53, 27}, {45, 38}, {36, 42}, {28, 40},
{20, 35}, {12, 36}, {4, 43}},
// Arabian peninsula
{{35, 31}, {48, 30}, {56, 24}, {51, 15}, {43, 12}, {36, 20}},
// India and southeast Asia
{{67, 25}, {76, 31}, {88, 25}, {92, 22}, {87, 8}, {80, 6},
{74, 15}},
{{95, 22}, {106, 21}, {110, 12}, {105, 1}, {100, 7}},
// Africa
{{-17, 37}, {-5, 37}, {10, 35}, {25, 31}, {34, 22}, {43, 12},
{51, 11}, {48, 2}, {42, -12}, {33, -26}, {25, -35}, {17, -34},
{10, -25}, {2, -9}, {-8, 5}, {-16, 15}},
// Australia
{{112, -11}, {125, -10}, {138, -14}, {153, -27}, {151, -39},
{139, -44}, {126, -34}, {114, -35}, {111, -23}},
// Madagascar
{{47, -13}, {51, -17}, {49, -27}, {44, -25}, {43, -18}},
// Japan
{{130, 31}, {136, 34}, {142, 43}, {145, 44}, {141, 35}},
// Great Britain
{{-8, 50}, {-3, 58}, {1, 57}, {2, 51}},
// New Zealand
{{166, -34}, {174, -41}, {178, -47}, {171, -45}},
};
return polygons;
}
const QVector<QPolygonF> &landPolygons()
{
static const QVector<QPolygonF> polygons = [] {
QFile file(QStringLiteral(":/resources/land-110m.json"));
if (!file.open(QIODevice::ReadOnly)) return fallbackLandPolygons();
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
const QJsonObject root = document.object();
const QJsonObject transform = root.value(QStringLiteral("transform")).toObject();
const QJsonArray scale = transform.value(QStringLiteral("scale")).toArray();
const QJsonArray translate = transform.value(QStringLiteral("translate")).toArray();
if (scale.size() != 2 || translate.size() != 2) return fallbackLandPolygons();
const double scaleX = scale[0].toDouble();
const double scaleY = scale[1].toDouble();
const double translateX = translate[0].toDouble();
const double translateY = translate[1].toDouble();
QVector<QPolygonF> decodedArcs;
const QJsonArray arcs = root.value(QStringLiteral("arcs")).toArray();
decodedArcs.reserve(arcs.size());
for (const QJsonValue &arcValue : arcs) {
int x = 0;
int y = 0;
QPolygonF arc;
for (const QJsonValue &pointValue : arcValue.toArray()) {
const QJsonArray point = pointValue.toArray();
if (point.size() != 2) continue;
x += point[0].toInt();
y += point[1].toInt();
arc.push_back(QPointF(x * scaleX + translateX,
y * scaleY + translateY));
}
decodedArcs.push_back(arc);
}
QVector<QPolygonF> result;
const QJsonObject land = root.value(QStringLiteral("objects")).toObject()
.value(QStringLiteral("land")).toObject();
const QJsonArray geometries = land.value(QStringLiteral("geometries")).toArray();
for (const QJsonValue &geometryValue : geometries) {
const QJsonObject geometry = geometryValue.toObject();
const QJsonArray multiPolygon = geometry.value(QStringLiteral("arcs")).toArray();
for (const QJsonValue &polygonValue : multiPolygon) {
const QJsonArray rings = polygonValue.toArray();
if (rings.isEmpty()) continue;
const QJsonArray ringArcs = rings[0].toArray(); // outer coastline
QPolygonF polygon;
for (const QJsonValue &arcIndexValue : ringArcs) {
const int reference = arcIndexValue.toInt();
const int index = reference >= 0 ? reference : ~reference;
if (index < 0 || index >= decodedArcs.size()) continue;
QPolygonF arc = decodedArcs[index];
if (reference < 0) std::reverse(arc.begin(), arc.end());
for (int i = polygon.isEmpty() ? 0 : 1; i < arc.size(); ++i) {
polygon.push_back(arc[i]);
}
}
if (polygon.size() >= 3) result.push_back(polygon);
}
}
return result.isEmpty() ? fallbackLandPolygons() : result;
}();
return polygons;
}
const QImage &landMask()
{
// A geographic mask decouples coastline quality from the size of the widget.
// It is generated once and remains fixed in Earth coordinates while rotating.
static const QImage mask = [] {
constexpr int Width = 1440;
constexpr int Height = 720;
QImage image(Width, Height, QImage::Format_Grayscale8);
image.fill(0);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::white);
for (const QPolygonF &geographic : landPolygons()) {
QPolygonF projected;
projected.reserve(geographic.size());
double previousLongitude = geographic.first().x();
double unwrappedLongitude = previousLongitude;
for (const QPointF &point : geographic) {
double longitude = point.x();
while (longitude - previousLongitude > 180.0) longitude -= 360.0;
while (longitude - previousLongitude < -180.0) longitude += 360.0;
unwrappedLongitude += longitude - previousLongitude;
previousLongitude = longitude;
projected.push_back(QPointF((unwrappedLongitude + 180.0) / 360.0 * Width,
(90.0 - point.y()) / 180.0 * Height));
}
for (int copy = -1; copy <= 1; ++copy) {
QPolygonF shifted = projected;
shifted.translate(copy * Width, 0.0);
painter.drawPolygon(shifted);
}
}
painter.end();
return image;
}();
return mask;
}
bool maskIsLand(double longitude, double latitude)
{
const QImage &mask = landMask();
longitude = std::fmod(longitude + 180.0, 360.0);
if (longitude < 0.0) longitude += 360.0;
const int x = std::clamp(static_cast<int>(longitude / 360.0 * mask.width()),
0, mask.width() - 1);
const int y = std::clamp(static_cast<int>((90.0 - latitude) / 180.0 * mask.height()),
0, mask.height() - 1);
return mask.constScanLine(y)[x] != 0;
}
double smoothStep(double edge0, double edge1, double value)
{
const double t = std::clamp((value - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
}
GlobeWidget::GlobeWidget(QWidget *parent)
: QWidget(parent)
{
setObjectName(QStringLiteral("globeWidget"));
setMinimumSize(420, 360);
setMouseTracking(true);
setCursor(Qt::OpenHandCursor);
setToolTip(QStringLiteral("Dunyayi dondurmek icin surukleyin, yakinlastirmak icin tekerlegi kullanin"));
m_satelliteAnimationTimer.setInterval(33);
m_satelliteAnimationTimer.setTimerType(Qt::PreciseTimer);
connect(&m_satelliteAnimationTimer, &QTimer::timeout,
this, &GlobeWidget::updateSatelliteAnimation);
}
void GlobeWidget::setSatellites(const QVector<SatellitePosition> &satellites)
{
m_satellites = satellites;
update();
}
void GlobeWidget::setSatelliteMotion(const QVector<SatellitePosition> &current,
const QVector<SatellitePosition> &next,
int durationMs)
{
if (current.isEmpty() || next.isEmpty() || durationMs <= 0) {
setSatellites(current);
m_satelliteAnimationTimer.stop();
return;
}
// Continue from the currently rendered points when a fresh ephemeris arrives,
// avoiding even a small visual jump at the update boundary.
m_satelliteMotionStart = m_satellites.isEmpty() ? current : m_satellites;
m_satelliteMotionTarget = next;
m_satelliteMotionDurationMs = durationMs;
m_satelliteMotionClock.restart();
m_satelliteAnimationTimer.start();
updateSatelliteAnimation();
}
void GlobeWidget::updateSatelliteAnimation()
{
if (!m_satelliteMotionClock.isValid() || m_satelliteMotionDurationMs <= 0) return;
const double progress = std::clamp(
static_cast<double>(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs,
0.0, 1.0);
QHash<QString, SatellitePosition> targets;
targets.reserve(m_satelliteMotionTarget.size());
for (const SatellitePosition &satellite : m_satelliteMotionTarget) {
targets.insert(satellite.prn, satellite);
}
QVector<SatellitePosition> interpolated;
interpolated.reserve(m_satelliteMotionStart.size());
for (const SatellitePosition &start : m_satelliteMotionStart) {
const auto targetIt = targets.constFind(start.prn);
if (targetIt == targets.cend()) continue;
const SatellitePosition &target = targetIt.value();
SatellitePosition point = start;
point.x += (target.x - start.x) * progress;
point.y += (target.y - start.y) * progress;
point.z += (target.z - start.z) * progress;
point.healthy = target.healthy;
interpolated.push_back(point);
}
if (!interpolated.isEmpty()) m_satellites = interpolated;
update();
if (progress >= 1.0) m_satelliteAnimationTimer.stop();
}
void GlobeWidget::setObservedPrns(const QSet<QString> &prns)
{
m_observedPrns = prns;
update();
}
void GlobeWidget::setReceiverLocation(double latitude, double longitude)
{
const bool firstLocation = !m_hasReceiver;
m_hasReceiver = true;
m_receiverLatitude = latitude;
m_receiverLongitude = longitude;
if (firstLocation) {
// Keep the receiver off-centre so its local zenith is visibly directed
// away from Earth. Looking straight down the zenith made the outward
// visibility cone appear to be embedded in the globe.
m_yaw = Pi / 2.0 - longitude * Pi / 180.0 + InitialLongitudeOffset;
m_pitch = std::clamp(-latitude * Pi / 180.0 + InitialLatitudeOffset,
-1.15, 1.15);
}
update();
}
void GlobeWidget::clearReceiverLocation()
{
m_hasReceiver = false;
update();
}
void GlobeWidget::setRinexStatus(const QString &status, bool error)
{
m_status = status;
m_statusError = error;
update();
}
void GlobeWidget::setElevationMask(double degrees)
{
m_elevationMaskDegrees = std::clamp(degrees, 0.0, 90.0);
update();
}
GlobeWidget::ProjectedPoint GlobeWidget::project(double x, double y, double z, double radius) const
{
const double cy = std::cos(m_yaw);
const double sy = std::sin(m_yaw);
const double x1 = cy * x - sy * y;
const double y1 = sy * x + cy * y;
const double cp = std::cos(m_pitch);
const double sp = std::sin(m_pitch);
const double y2 = cp * y1 - sp * z;
const double z2 = sp * y1 + cp * z;
const QPointF center(width() * 0.5, height() * 0.52);
return {{center.x() + x1 * radius, center.y() - z2 * radius}, y2};
}
void GlobeWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
QLinearGradient background(0, 0, width(), height());
background.setColorAt(0.0, QColor(3, 10, 21));
background.setColorAt(0.55, QColor(8, 24, 43));
background.setColorAt(1.0, QColor(2, 8, 17));
painter.fillRect(rect(), background);
drawStars(painter);
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
const QPointF center(width() * 0.5, height() * 0.52);
QRadialGradient atmosphere(center - QPointF(radius * .25, radius * .32), radius * 1.24);
atmosphere.setColorAt(0.66, QColor(19, 113, 177, 0));
atmosphere.setColorAt(0.82, QColor(47, 169, 235, 48));
atmosphere.setColorAt(0.92, QColor(51, 178, 239, 20));
atmosphere.setColorAt(1.0, QColor(35, 126, 205, 0));
painter.setPen(Qt::NoPen);
painter.setBrush(atmosphere);
painter.drawEllipse(center, radius * 1.25, radius * 1.25);
QRadialGradient earth(center - QPointF(radius * .36, radius * .42), radius * 1.42);
earth.setColorAt(0.0, QColor(48, 154, 200));
earth.setColorAt(0.37, QColor(18, 91, 144));
earth.setColorAt(0.72, QColor(8, 47, 86));
earth.setColorAt(1.0, QColor(1, 10, 24));
painter.setBrush(earth);
painter.setPen(QPen(QColor(89, 196, 240, 150), 1.2));
painter.drawEllipse(center, radius, radius);
painter.save();
painter.setClipPath(QPainterPath());
QPainterPath sphereClip;
sphereClip.addEllipse(center, radius, radius);
painter.setClipPath(sphereClip);
drawContinents(painter);
drawGraticule(painter);
painter.restore();
for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0);
if (m_hasReceiver) drawVisibilityCone(painter);
if (m_hasReceiver) drawReceiver(painter);
drawSatellites(painter);
painter.setPen(QColor(229, 240, 249));
QFont titleFont = painter.font();
titleFont.setPixelSize(18);
titleFont.setBold(true);
painter.setFont(titleFont);
painter.drawText(QRect(20, 16, width() - 40, 28), Qt::AlignLeft | Qt::AlignVCenter,
QStringLiteral("GNSS YORUNGE GORUNUMU"));
QFont smallFont = painter.font();
smallFont.setPixelSize(11);
smallFont.setBold(false);
painter.setFont(smallFont);
painter.setPen(m_statusError ? QColor(255, 177, 77) : QColor(132, 177, 204));
painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignLeft | Qt::AlignVCenter, m_status);
const int legendY = height() - 25;
const int legendX = width() / 2 - 125;
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(42, 224, 137));
painter.drawEllipse(QPointF(legendX, legendY - 3), 4, 4);
painter.setPen(QColor(101, 239, 176));
painter.drawText(legendX + 9, legendY + 1, QStringLiteral("BULUNDU"));
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(255, 79, 95));
painter.drawEllipse(QPointF(legendX + 78, legendY - 3), 4, 4);
painter.setPen(QColor(255, 132, 143));
painter.drawText(legendX + 87, legendY + 1, QStringLiteral("BULUNAMADI"));
painter.setPen(QColor(138, 183, 207));
int visibleCount = 0;
for (const auto &satellite : m_satellites) {
if (satelliteElevation(satellite) >= m_elevationMaskDegrees) ++visibleCount;
}
painter.drawText(legendX + 181, legendY + 1,
QStringLiteral("%1/%2 PRN · ELEV≥%3°: %4")
.arg(m_observedPrns.size()).arg(m_satellites.size())
.arg(m_elevationMaskDegrees, 0, 'f', 1).arg(visibleCount));
if (!m_hasReceiver) {
painter.setPen(QColor(219, 232, 241));
QFont waitingFont = painter.font();
waitingFont.setPixelSize(16);
waitingFont.setBold(true);
painter.setFont(waitingFont);
painter.drawText(rect().adjusted(30, 70, -30, -50), Qt::AlignCenter,
QStringLiteral("Ilk gecerli konum bekleniyor"));
}
}
void GlobeWidget::drawContinents(QPainter &painter) const
{
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
const double pixelRatio = std::min(devicePixelRatioF(), 1.5);
const int textureSize = std::clamp(
static_cast<int>(std::ceil(radius * 2.0 * pixelRatio)), 320, 900);
const QPointF center(width() * 0.5, height() * 0.52);
const QRectF target(center.x() - radius, center.y() - radius,
radius * 2.0, radius * 2.0);
if (!m_landTexture.isNull() && m_landTexture.width() == textureSize
&& m_landTextureYaw == m_yaw && m_landTexturePitch == m_pitch) {
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.drawImage(target, m_landTexture);
return;
}
QImage land(textureSize, textureSize, QImage::Format_ARGB32);
land.fill(Qt::transparent);
const double cy = std::cos(m_yaw);
const double sy = std::sin(m_yaw);
const double cp = std::cos(m_pitch);
const double sp = std::sin(m_pitch);
for (int py = 0; py < textureSize; ++py) {
QRgb *scanLine = reinterpret_cast<QRgb *>(land.scanLine(py));
const double screenZ = -((py + 0.5) / textureSize * 2.0 - 1.0);
for (int px = 0; px < textureSize; ++px) {
const double screenX = (px + 0.5) / textureSize * 2.0 - 1.0;
const double radiusSquared = screenX * screenX + screenZ * screenZ;
if (radiusSquared > 1.0) continue;
const double depth = std::sqrt(1.0 - radiusSquared);
const double yawY = cp * depth + sp * screenZ;
const double worldZ = -sp * depth + cp * screenZ;
const double worldX = cy * screenX + sy * yawY;
const double worldY = -sy * screenX + cy * yawY;
const double latitude = std::asin(std::clamp(worldZ, -1.0, 1.0)) * 180.0 / Pi;
const double longitude = std::atan2(worldY, worldX) * 180.0 / Pi;
const bool onLand = maskIsLand(longitude, latitude);
const bool nearLand = maskIsLand(longitude + 0.55, latitude)
|| maskIsLand(longitude - 0.55, latitude)
|| maskIsLand(longitude, latitude + 0.55)
|| maskIsLand(longitude, latitude - 0.55);
if (!onLand) {
if (nearLand) {
const int alpha = static_cast<int>(65.0 * std::clamp(depth * 1.5, 0.0, 1.0));
scanLine[px] = qRgba(33, 151, 174, alpha);
}
continue;
}
const double light = std::clamp(0.30 + 0.70
* (0.35 - screenX * 0.38 + screenZ * 0.48 + depth * 0.38), 0.18, 1.0);
const double lon = longitude * Pi / 180.0;
const double lat = latitude * Pi / 180.0;
const double broadRelief = 0.50 + 0.24 * std::sin(lon * 5.0 + lat * 2.2)
+ 0.16 * std::sin(lon * 13.0 - lat * 7.0)
+ 0.10 * std::cos(lon * 29.0 + lat * 17.0);
const double relief = std::clamp(broadRelief, 0.0, 1.0);
const double aridBelt = std::exp(-std::pow((std::abs(latitude) - 25.0) / 13.0, 2.0));
const double moisture = std::clamp(0.62 - aridBelt * 0.48
+ 0.18 * std::sin(lon * 3.0 - lat * 4.0), 0.0, 1.0);
const double highland = smoothStep(0.67, 0.92, relief);
const double snow = smoothStep(63.0, 78.0, std::abs(latitude))
* (0.65 + highland * 0.35);
const double lushRed = 30.0 + relief * 25.0;
const double lushGreen = 104.0 + relief * 34.0;
const double lushBlue = 63.0 + relief * 20.0;
const double dryRed = 151.0 + relief * 24.0;
const double dryGreen = 126.0 + relief * 18.0;
const double dryBlue = 72.0 + relief * 16.0;
double red = dryRed + (lushRed - dryRed) * moisture;
double green = dryGreen + (lushGreen - dryGreen) * moisture;
double blue = dryBlue + (lushBlue - dryBlue) * moisture;
red = red * (1.0 - highland * 0.30) + 104.0 * highland * 0.30;
green = green * (1.0 - highland * 0.30) + 91.0 * highland * 0.30;
blue = blue * (1.0 - highland * 0.30) + 76.0 * highland * 0.30;
red = red * (1.0 - snow) + 224.0 * snow;
green = green * (1.0 - snow) + 231.0 * snow;
blue = blue * (1.0 - snow) + 224.0 * snow;
const bool coast = !maskIsLand(longitude + 0.55, latitude)
|| !maskIsLand(longitude - 0.55, latitude)
|| !maskIsLand(longitude, latitude + 0.55)
|| !maskIsLand(longitude, latitude - 0.55);
const double shade = light * (0.86 + relief * 0.14);
if (coast) {
red += 28.0;
green += 31.0;
blue += 18.0;
}
scanLine[px] = qRgba(
std::clamp(static_cast<int>(red * shade), 0, 255),
std::clamp(static_cast<int>(green * shade), 0, 255),
std::clamp(static_cast<int>(blue * shade), 0, 255), 238);
}
}
m_landTexture = land;
m_landTextureYaw = m_yaw;
m_landTexturePitch = m_pitch;
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.drawImage(target, m_landTexture);
}
void GlobeWidget::drawStars(QPainter &painter) const
{
painter.setPen(Qt::NoPen);
for (int i = 0; i < 95; ++i) {
const int x = (i * 7919 + 37) % std::max(1, width());
const int y = (i * 3571 + 71) % std::max(1, height());
const int alpha = 45 + (i * 29) % 120;
const double size = 0.7 + (i % 4) * 0.35;
painter.setBrush(QColor(183, 218, 240, alpha));
painter.drawEllipse(QPointF(x, y), size, size);
}
}
void GlobeWidget::drawGraticule(QPainter &painter) const
{
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
painter.setPen(QPen(QColor(100, 193, 221, 48), 0.8));
for (int latitude = -60; latitude <= 60; latitude += 30) {
QPainterPath path;
bool started = false;
for (int longitude = -180; longitude <= 180; longitude += 3) {
const Vec3 v = globeVector(latitude, longitude);
const auto p = project(v.x, v.y, v.z, radius);
if (p.depth >= 0.0) {
if (!started) path.moveTo(p.point); else path.lineTo(p.point);
started = true;
} else started = false;
}
painter.drawPath(path);
}
for (int longitude = -150; longitude < 180; longitude += 30) {
QPainterPath path;
bool started = false;
for (int latitude = -90; latitude <= 90; latitude += 2) {
const Vec3 v = globeVector(latitude, longitude);
const auto p = project(v.x, v.y, v.z, radius);
if (p.depth >= 0.0) {
if (!started) path.moveTo(p.point); else path.lineTo(p.point);
started = true;
} else started = false;
}
painter.drawPath(path);
}
}
void GlobeWidget::drawOrbit(QPainter &painter, double inclination, double node) const
{
const double radius = std::min(width(), height()) * 0.255 * m_zoom * 1.47;
const double inc = inclination * Pi / 180.0;
const double asc = node * Pi / 180.0;
QPainterPath back, front;
bool backStarted = false, frontStarted = false;
for (int degree = 0; degree <= 360; degree += 2) {
const double t = degree * Pi / 180.0;
const double ox = std::cos(t);
const double oy = std::sin(t) * std::cos(inc);
const double oz = std::sin(t) * std::sin(inc);
const double x = ox * std::cos(asc) - oy * std::sin(asc);
const double y = ox * std::sin(asc) + oy * std::cos(asc);
const auto p = project(x, y, oz, radius);
QPainterPath &path = p.depth < 0.0 ? back : front;
bool &started = p.depth < 0.0 ? backStarted : frontStarted;
if (!started) path.moveTo(p.point); else path.lineTo(p.point);
started = true;
if (p.depth < 0.0) frontStarted = false; else backStarted = false;
}
painter.setBrush(Qt::NoBrush);
painter.setPen(QPen(QColor(86, 130, 160, 44), 0.8, Qt::DashLine));
painter.drawPath(back);
painter.setPen(QPen(QColor(89, 173, 214, 90), 1.0));
painter.drawPath(front);
}
void GlobeWidget::drawVisibilityCone(QPainter &painter) const
{
if (!m_hasReceiver) return;
const double earthRadiusPixels = std::min(width(), height()) * 0.255 * m_zoom;
constexpr double physicalOrbitRatio = 4.17;
constexpr double displayOrbitRatio = 1.47;
const Vec3 normal = globeVector(m_receiverLatitude, m_receiverLongitude);
Vec3 reference = std::abs(normal.z) < 0.85 ? Vec3{0.0, 0.0, 1.0} : Vec3{1.0, 0.0, 0.0};
Vec3 tangentA {
reference.y * normal.z - reference.z * normal.y,
reference.z * normal.x - reference.x * normal.z,
reference.x * normal.y - reference.y * normal.x
};
const double tangentLength = std::sqrt(tangentA.x * tangentA.x
+ tangentA.y * tangentA.y
+ tangentA.z * tangentA.z);
tangentA = {tangentA.x / tangentLength, tangentA.y / tangentLength,
tangentA.z / tangentLength};
const Vec3 tangentB {
normal.y * tangentA.z - normal.z * tangentA.y,
normal.z * tangentA.x - normal.x * tangentA.z,
normal.x * tangentA.y - normal.y * tangentA.x
};
const double elevation = m_elevationMaskDegrees * Pi / 180.0;
const double zenith = Pi / 2.0 - elevation;
const double lineDistance = -std::cos(zenith)
+ std::sqrt(physicalOrbitRatio * physicalOrbitRatio
- std::sin(zenith) * std::sin(zenith));
const auto apex = project(normal.x, normal.y, normal.z, earthRadiusPixels * 1.015);
const auto zenithPoint = project(normal.x, normal.y, normal.z,
earthRadiusPixels * displayOrbitRatio);
QVector<ProjectedPoint> ring;
constexpr int Segments = 48;
ring.reserve(Segments + 1);
for (int segment = 0; segment <= Segments; ++segment) {
const double angle = 2.0 * Pi * segment / Segments;
const Vec3 tangent {
tangentA.x * std::cos(angle) + tangentB.x * std::sin(angle),
tangentA.y * std::cos(angle) + tangentB.y * std::sin(angle),
tangentA.z * std::cos(angle) + tangentB.z * std::sin(angle)
};
const Vec3 direction {
normal.x * std::cos(zenith) + tangent.x * std::sin(zenith),
normal.y * std::cos(zenith) + tangent.y * std::sin(zenith),
normal.z * std::cos(zenith) + tangent.z * std::sin(zenith)
};
const Vec3 orbitPoint {
(normal.x + lineDistance * direction.x) / physicalOrbitRatio,
(normal.y + lineDistance * direction.y) / physicalOrbitRatio,
(normal.z + lineDistance * direction.z) / physicalOrbitRatio
};
ring.push_back(project(orbitPoint.x, orbitPoint.y, orbitPoint.z,
earthRadiusPixels * displayOrbitRatio));
}
painter.setPen(Qt::NoPen);
for (int i = 0; i < Segments; ++i) {
const double averageDepth = (apex.depth + ring[i].depth + ring[i + 1].depth) / 3.0;
const int alpha = averageDepth >= 0.0 ? 18 : 7;
painter.setBrush(QColor(48, 207, 218, alpha));
painter.drawPolygon(QPolygonF{apex.point, ring[i].point, ring[i + 1].point});
}
QPainterPath boundary;
boundary.moveTo(ring.first().point);
for (int i = 1; i < ring.size(); ++i) boundary.lineTo(ring[i].point);
painter.setBrush(Qt::NoBrush);
painter.setPen(QPen(QColor(76, 222, 228, 105), 1.0, Qt::DashLine));
painter.drawPath(boundary);
for (int i = 0; i < Segments; i += 8) {
painter.drawLine(apex.point, ring[i].point);
}
painter.setPen(QPen(QColor(119, 239, 241, 165), 1.4));
painter.drawLine(apex.point, zenithPoint.point);
const QPointF axis = zenithPoint.point - apex.point;
const double axisLength = std::hypot(axis.x(), axis.y());
if (axisLength > 1.0) {
const QPointF unit = axis / axisLength;
const QPointF side(-unit.y(), unit.x());
const QPointF arrowTip = zenithPoint.point;
painter.drawLine(arrowTip, arrowTip - unit * 8.0 + side * 4.0);
painter.drawLine(arrowTip, arrowTip - unit * 8.0 - side * 4.0);
}
if (apex.depth >= -0.05) {
painter.setPen(QColor(119, 239, 241, 190));
painter.drawText(apex.point + QPointF(12, 18),
QStringLiteral("GORUS KONISI %1°")
.arg(m_elevationMaskDegrees, 0, 'f', 1));
}
}
void GlobeWidget::drawReceiver(QPainter &painter) const
{
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
const Vec3 v = globeVector(m_receiverLatitude, m_receiverLongitude);
const auto p = project(v.x, v.y, v.z, radius * 1.015);
if (p.depth < -0.05) return;
painter.setPen(QPen(QColor(255, 255, 255, 210), 1.5));
painter.setBrush(QColor(255, 196, 61));
painter.drawEllipse(p.point, 5.5, 5.5);
painter.setPen(QPen(QColor(255, 196, 61, 110), 1.2));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(p.point, 11, 11);
painter.setPen(QColor(255, 224, 147));
painter.drawText(p.point + QPointF(10, -8), QStringLiteral("ALICI"));
}
void GlobeWidget::drawSatellites(QPainter &painter) const
{
if (!m_hasReceiver) return;
const double orbitRadius = std::min(width(), height()) * 0.255 * m_zoom * 1.47;
struct Item { SatellitePosition satellite; ProjectedPoint projected; };
QVector<Item> items;
for (const auto &satellite : m_satellites) {
const double length = std::sqrt(satellite.x * satellite.x + satellite.y * satellite.y
+ satellite.z * satellite.z);
if (length <= 0.0) continue;
items.push_back({satellite, project(satellite.x / length, satellite.y / length,
satellite.z / length, orbitRadius)});
}
std::sort(items.begin(), items.end(), [](const Item &a, const Item &b) {
return a.projected.depth < b.projected.depth;
});
for (const Item &item : items) {
const bool found = m_observedPrns.contains(item.satellite.prn);
const bool inElevationMask = satelliteElevation(item.satellite) >= m_elevationMaskDegrees;
QColor color = found ? QColor(42, 224, 137) : QColor(255, 79, 95);
if (item.projected.depth < 0.0) color.setAlpha(75);
const double markerRadius = found ? 6.5 : 5.0;
if (inElevationMask) {
const double earthRadius = std::min(width(), height()) * 0.255 * m_zoom;
const Vec3 receiver = globeVector(m_receiverLatitude, m_receiverLongitude);
const auto receiverPoint = project(receiver.x, receiver.y, receiver.z,
earthRadius * 1.02);
QColor sightLine(70, 224, 231, item.projected.depth < 0.0 ? 25 : 80);
painter.setPen(QPen(sightLine, 0.8, Qt::DashLine));
painter.drawLine(receiverPoint.point, item.projected.point);
painter.setPen(QPen(QColor(90, 236, 239, sightLine.alpha()), 1.2));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(item.projected.point, markerRadius + 3.0, markerRadius + 3.0);
}
painter.setPen(QPen(QColor(235, 248, 255, color.alpha()), 1.0));
painter.setBrush(color);
painter.drawEllipse(item.projected.point, markerRadius, markerRadius);
if (item.projected.depth >= -0.05) {
painter.setPen(color.lighter(135));
painter.drawText(item.projected.point + QPointF(7, -6), item.satellite.prn);
}
}
}
double GlobeWidget::satelliteElevation(const SatellitePosition &satellite) const
{
if (!m_hasReceiver) return -90.0;
constexpr double EarthRadiusMeters = 6378137.0;
const Vec3 receiverUnit = globeVector(m_receiverLatitude, m_receiverLongitude);
const Vec3 lineOfSight {
satellite.x - receiverUnit.x * EarthRadiusMeters,
satellite.y - receiverUnit.y * EarthRadiusMeters,
satellite.z - receiverUnit.z * EarthRadiusMeters
};
const double length = std::sqrt(lineOfSight.x * lineOfSight.x
+ lineOfSight.y * lineOfSight.y
+ lineOfSight.z * lineOfSight.z);
if (length <= 0.0) return -90.0;
const double sineElevation = (lineOfSight.x * receiverUnit.x
+ lineOfSight.y * receiverUnit.y
+ lineOfSight.z * receiverUnit.z) / length;
return std::asin(std::clamp(sineElevation, -1.0, 1.0)) * 180.0 / Pi;
}
void GlobeWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_dragging = true;
m_lastMousePosition = event->pos();
setCursor(Qt::ClosedHandCursor);
}
}
void GlobeWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!m_dragging) return;
const QPoint delta = event->pos() - m_lastMousePosition;
m_lastMousePosition = event->pos();
m_yaw += delta.x() * 0.008;
m_pitch = std::clamp(m_pitch - delta.y() * 0.008, -1.45, 1.45);
update();
}
void GlobeWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_dragging = false;
setCursor(Qt::OpenHandCursor);
}
}
void GlobeWidget::wheelEvent(QWheelEvent *event)
{
m_zoom = std::clamp(m_zoom + event->angleDelta().y() / 1200.0, 0.72, 1.28);
update();
event->accept();
}
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include "SatelliteData.h"
#include <QElapsedTimer>
#include <QImage>
#include <QPoint>
#include <QSet>
#include <QTimer>
#include <QWidget>
class GlobeWidget : public QWidget {
Q_OBJECT
public:
explicit GlobeWidget(QWidget *parent = nullptr);
void setSatellites(const QVector<SatellitePosition> &satellites);
void setSatelliteMotion(const QVector<SatellitePosition> &current,
const QVector<SatellitePosition> &next,
int durationMs);
void setObservedPrns(const QSet<QString> &prns);
void setReceiverLocation(double latitude, double longitude);
void clearReceiverLocation();
void setRinexStatus(const QString &status, bool error);
public slots:
void setElevationMask(double degrees);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
private:
struct ProjectedPoint {
QPointF point;
double depth = 0.0;
};
ProjectedPoint project(double x, double y, double z, double radius) const;
void drawStars(QPainter &painter) const;
void drawOrbit(QPainter &painter, double inclination, double node) const;
void drawContinents(QPainter &painter) const;
void drawVisibilityCone(QPainter &painter) const;
void drawGraticule(QPainter &painter) const;
void drawReceiver(QPainter &painter) const;
void drawSatellites(QPainter &painter) const;
double satelliteElevation(const SatellitePosition &satellite) const;
void updateSatelliteAnimation();
QVector<SatellitePosition> m_satellites;
QVector<SatellitePosition> m_satelliteMotionStart;
QVector<SatellitePosition> m_satelliteMotionTarget;
QElapsedTimer m_satelliteMotionClock;
QTimer m_satelliteAnimationTimer;
int m_satelliteMotionDurationMs = 0;
mutable QImage m_landTexture;
mutable double m_landTextureYaw = 0.0;
mutable double m_landTexturePitch = 0.0;
QSet<QString> m_observedPrns;
QString m_status = QStringLiteral("Konum bekleniyor");
bool m_statusError = false;
bool m_hasReceiver = false;
double m_receiverLatitude = 0.0;
double m_receiverLongitude = 0.0;
double m_yaw = -0.55;
double m_pitch = -0.28;
double m_zoom = 1.0;
double m_elevationMaskDegrees = 10.0;
bool m_dragging = false;
QPoint m_lastMousePosition;
};
+227
View File
@@ -0,0 +1,227 @@
#include "LocationParser.h"
#include <QRegularExpression>
#include <cmath>
namespace {
double toDouble(QString value, bool *ok)
{
value.replace(',', '.');
return value.toDouble(ok);
}
QString normalizedCoordinateText(QString value)
{
value.replace(',', '.');
if (value.startsWith('+')) {
value.remove(0, 1);
}
return value;
}
QString normalizeKey(QString key)
{
key = key.toLower();
key.remove('_');
key.remove('-');
key.remove(' ');
if (key == "lat" || key == "latitude" || key == "lattitude") {
return "latitude";
}
if (key == "long" || key == "longitude" || key == "lon" || key == "lng") {
return "longitude";
}
if (key == "hheight" || key == "height" || key == "alt" || key == "altitude") {
return "hheight";
}
if (key == "sat" || key == "sats" || key == "satellite" || key == "satellites"
|| key == "activesat" || key == "activesats" || key == "activesatellite"
|| key == "activesatellites" || key == "numsat" || key == "numsats"
|| key == "numbersat" || key == "numbersats" || key == "satactive"
|| key == "satsactive" || key == "satelliteactive" || key == "satellitesactive"
|| key == "uydu" || key == "uydular") {
return "satellites";
}
if (key == "gdop" || key == "pdop" || key == "hdop" || key == "vdop" || key == "tdop") {
return key;
}
return {};
}
bool isValidLatitude(double latitude)
{
return std::isfinite(latitude) && latitude >= -90.0 && latitude <= 90.0;
}
bool isValidLongitude(double longitude)
{
return std::isfinite(longitude) && longitude >= -180.0 && longitude <= 180.0;
}
} // namespace
std::optional<ParsedTelemetry> LocationParser::parseLine(const QString &line)
{
const QString trimmed = line.trimmed();
if (trimmed.isEmpty()) {
return std::nullopt;
}
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+)?))"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression satelliteCountPhraseRegex(
QStringLiteral(R"((^|[^A-Za-z0-9_\.])(?<value>\d+)\s+(?:active\s+)?(?:sat(?:ellite)?s?|uydu(?:lar)?)(?=$|[\s,.;)\]]))"),
QRegularExpression::CaseInsensitiveOption);
ParsedTelemetry telemetry;
telemetry.rawLine = trimmed;
bool hadCoordinateField = false;
bool hadLatitudeField = false;
bool hadLongitudeField = false;
auto matches = labeledValueRegex.globalMatch(trimmed);
while (matches.hasNext()) {
const QRegularExpressionMatch match = matches.next();
const QString field = normalizeKey(match.captured(QStringLiteral("key")));
if (field.isEmpty()) {
continue;
}
bool ok = false;
const double value = toDouble(match.captured(QStringLiteral("value")), &ok);
if (!ok || !std::isfinite(value)) {
continue;
}
if (field == "latitude") {
hadCoordinateField = true;
hadLatitudeField = true;
telemetry.latitude = value;
telemetry.latitudeText = normalizedCoordinateText(
match.captured(QStringLiteral("value")));
} else if (field == "longitude") {
hadCoordinateField = true;
hadLongitudeField = true;
telemetry.longitude = value;
telemetry.longitudeText = normalizedCoordinateText(
match.captured(QStringLiteral("value")));
} else if (field == "hheight") {
telemetry.hheight = value;
} else if (field == "satellites") {
const int count = static_cast<int>(std::llround(value));
if (count >= 0) {
telemetry.satellites = count;
}
} else if (field == "gdop") {
if (value >= 0.0) {
telemetry.dop.gdop = value;
}
} else if (field == "pdop") {
if (value >= 0.0) {
telemetry.dop.pdop = value;
}
} else if (field == "hdop") {
if (value >= 0.0) {
telemetry.dop.hdop = value;
}
} else if (field == "vdop") {
if (value >= 0.0) {
telemetry.dop.vdop = value;
}
} else if (field == "tdop") {
if (value >= 0.0) {
telemetry.dop.tdop = value;
}
}
}
auto satellitePhraseMatches = satelliteCountPhraseRegex.globalMatch(trimmed);
while (satellitePhraseMatches.hasNext()) {
const QRegularExpressionMatch match = satellitePhraseMatches.next();
bool ok = false;
const int count = match.captured(QStringLiteral("value")).toInt(&ok);
if (ok && count >= 0) {
telemetry.satellites = count;
}
}
if (telemetry.latitude.has_value() && !isValidLatitude(*telemetry.latitude)) {
telemetry.latitude.reset();
telemetry.latitudeText.clear();
}
if (telemetry.longitude.has_value() && !isValidLongitude(*telemetry.longitude)) {
telemetry.longitude.reset();
telemetry.longitudeText.clear();
}
if (hadLatitudeField && hadLongitudeField
&& (!telemetry.hasLocation()
|| !isValidCoordinate(*telemetry.latitude, *telemetry.longitude))) {
telemetry.latitude.reset();
telemetry.longitude.reset();
telemetry.latitudeText.clear();
telemetry.longitudeText.clear();
telemetry.hheight.reset();
} else if (hadCoordinateField && !telemetry.latitude.has_value()
&& !telemetry.longitude.has_value()) {
telemetry.hheight.reset();
}
if (!telemetry.hasAnyValue()) {
return std::nullopt;
}
return telemetry;
}
bool LocationParser::isValidCoordinate(double latitude, double longitude)
{
return std::isfinite(latitude) && std::isfinite(longitude)
&& latitude >= -90.0 && latitude <= 90.0
&& longitude >= -180.0 && longitude <= 180.0;
}
QSet<QString> LocationParser::parseGpsPrns(const QString &line)
{
QSet<QString> result;
const auto addPrn = [&result](QString value, bool zeroBased = false) {
value = value.trimmed().toUpper();
if (value.startsWith(QLatin1Char('G'))) value.remove(0, 1);
bool ok = false;
int number = value.toInt(&ok);
if (ok && zeroBased) ++number;
if (ok && number >= 1 && number <= 63) {
result.insert(QStringLiteral("G%1").arg(number, 2, 10, QLatin1Char('0')));
}
};
static const QRegularExpression gpsToken(
QStringLiteral(R"((?:^|[^A-Za-z0-9])G(?<prn>\d{1,2})(?=$|[^0-9]))"),
QRegularExpression::CaseInsensitiveOption);
auto gpsMatches = gpsToken.globalMatch(line);
while (gpsMatches.hasNext()) addPrn(gpsMatches.next().captured(QStringLiteral("prn")));
static const QRegularExpression labeledList(
QStringLiteral(R"((?<label>\bPRNS?\b|\bSVS?\b|\bSVNS?\b|\bSVIDS?\b)\s*(?:=|:|->)?\s*[\[(]?\s*(?<list>(?:G?\d{1,2})(?:\s*[,;/| ]\s*G?\d{1,2})*)\s*[\])]?)"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression number(QStringLiteral(R"(G?(\d{1,2}))"),
QRegularExpression::CaseInsensitiveOption);
auto listMatches = labeledList.globalMatch(line);
while (listMatches.hasNext()) {
const QRegularExpressionMatch listMatch = listMatches.next();
const bool zeroBased = listMatch.captured(QStringLiteral("label")).startsWith(
QStringLiteral("SVN"), Qt::CaseInsensitive);
auto values = number.globalMatch(listMatch.captured(QStringLiteral("list")));
while (values.hasNext()) addPrn(values.next().captured(1), zeroBased);
}
return result;
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "TelemetryPoint.h"
#include <QString>
#include <QSet>
#include <optional>
class LocationParser {
public:
static std::optional<ParsedTelemetry> parseLine(const QString &line);
static QSet<QString> parseGpsPrns(const QString &line);
private:
static bool isValidCoordinate(double latitude, double longitude);
};
+1161
View File
File diff suppressed because it is too large Load Diff
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include "TelemetryPoint.h"
#include <QMainWindow>
#include <QStringList>
#include <QSet>
#include <QVector>
#include <optional>
class QLabel;
class QCheckBox;
class QComboBox;
class QLineEdit;
class QPlainTextEdit;
class QPushButton;
class QSpinBox;
class QTableView;
class QTimer;
class MapWidget;
class GlobeWidget;
class RinexNavigation;
class SerialReader;
class TelemetryTableModel;
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
private slots:
void toggleSerial();
void clearPoints();
void handleSerialLine(const QString &line);
void handleSerialStatus(const QString &status);
void handleSerialError(const QString &message);
void handleLocationTimeout();
void trimToMaxPoints();
void updateReferenceLocation();
void flushUiUpdates();
private:
void buildUi();
void updateConnectionControls();
void addTelemetryPoint(const ParsedTelemetry &telemetry);
void refreshTable();
void updateSummaryLabels();
void updateDopLabels();
void updateAnalysis();
void appendLogLine(const QString &line, bool parsed);
void mergePendingTelemetry(const ParsedTelemetry &telemetry);
void commitPendingTelemetry();
void resetTelemetryState();
void scheduleTelemetryRefresh();
void refreshTelemetryViews();
void flushPendingLogLines();
int selectedBaudRate() const;
SerialReader *m_serial = nullptr;
MapWidget *m_map = nullptr;
GlobeWidget *m_globe = nullptr;
RinexNavigation *m_rinex = nullptr;
QComboBox *m_portCombo = nullptr;
QComboBox *m_baudCombo = nullptr;
QSpinBox *m_maxPointsSpin = nullptr;
QLineEdit *m_referenceLatEdit = nullptr;
QLineEdit *m_referenceLonEdit = nullptr;
QPushButton *m_connectButton = nullptr;
QPushButton *m_clearButton = nullptr;
QCheckBox *m_autoFollowCheck = nullptr;
QCheckBox *m_showTelemetryCheck = nullptr;
QCheckBox *m_showRecentPointsCheck = nullptr;
QLabel *m_statusLabel = nullptr;
QLabel *m_satelliteLabel = nullptr;
QLabel *m_satelliteQualityLabel = nullptr;
QLabel *m_gdopLabel = nullptr;
QLabel *m_hdopLabel = nullptr;
QLabel *m_vdopLabel = nullptr;
QLabel *m_tdopLabel = nullptr;
QLabel *m_countLabel = nullptr;
QLabel *m_meanLocationLabel = nullptr;
QLabel *m_lastDeviationLabel = nullptr;
QLabel *m_meanDeviationLabel = nullptr;
QLabel *m_rmsScatterLabel = nullptr;
QLabel *m_maxScatterLabel = nullptr;
QLabel *m_cepLabel = nullptr;
QCheckBox *m_showAverageCheck = nullptr;
QTableView *m_table = nullptr;
TelemetryTableModel *m_tableModel = nullptr;
QPlainTextEdit *m_log = nullptr;
QTimer *m_locationTimeoutTimer = nullptr;
QTimer *m_uiRefreshTimer = nullptr;
QVector<TelemetryPoint> m_points;
QStringList m_pendingLogLines;
std::optional<int> m_lastSatelliteCount;
QSet<QString> m_observedPrns;
std::optional<double> m_referenceLatitude;
std::optional<double> m_referenceLongitude;
DopValues m_lastDop;
ParsedTelemetry m_pendingTelemetry;
bool m_telemetryRefreshPending = false;
};
+623
View File
@@ -0,0 +1,623 @@
#include "MapWidget.h"
#include <QDir>
#include <QLabel>
#include <QLineF>
#include <QMouseEvent>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QPainter>
#include <QPainterPath>
#include <QResizeEvent>
#include <QStandardPaths>
#include <QStringList>
#include <QUrl>
#include <QWheelEvent>
#include <algorithm>
#include <cmath>
namespace {
constexpr int TileSize = 256;
constexpr int MinZoom = 2;
constexpr int MaxZoom = 19;
constexpr int MaxMemoryTileCount = 512;
constexpr double Pi = 3.14159265358979323846;
constexpr double WebMercatorRadiusMeters = 6378137.0;
constexpr double MaxMercatorLatitude = 85.05112878;
QString tileKey(int zoom, int tileX, int tileY)
{
return QStringLiteral("%1/%2/%3").arg(zoom).arg(tileX).arg(tileY);
}
double clampLatitude(double latitude)
{
return std::clamp(latitude, -MaxMercatorLatitude, MaxMercatorLatitude);
}
int positiveModulo(int value, int modulo)
{
const int result = value % modulo;
return result < 0 ? result + modulo : result;
}
double metersPerPixel(double latitude, int zoom)
{
const double scale = static_cast<double>(TileSize) * static_cast<double>(1 << zoom);
const double latitudeScale = std::cos(latitude * Pi / 180.0);
return latitudeScale * 2.0 * Pi * WebMercatorRadiusMeters / scale;
}
} // namespace
MapWidget::MapWidget(QWidget *parent)
: QWidget(parent)
, m_network(new QNetworkAccessManager(this))
{
setMinimumSize(640, 420);
setMouseTracking(true);
const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
if (!cacheRoot.isEmpty()) {
auto *diskCache = new QNetworkDiskCache(m_network);
const QString cacheDirectory = cacheRoot + QStringLiteral("/map-tiles");
QDir().mkpath(cacheDirectory);
diskCache->setCacheDirectory(cacheDirectory);
diskCache->setMaximumCacheSize(100 * 1024 * 1024);
m_network->setCache(diskCache);
}
m_attributionLabel = new QLabel(this);
m_attributionLabel->setText(
QStringLiteral("<a style=\"color:#174b7a;text-decoration:none\" "
"href=\"https://www.openstreetmap.org/copyright\">")
+ QString::fromUtf8("\xC2\xA9 OpenStreetMap contributors")
+ QStringLiteral("</a>"));
m_attributionLabel->setOpenExternalLinks(true);
m_attributionLabel->setStyleSheet(QStringLiteral(
"QLabel { background: rgba(255, 255, 255, 220); color: #26313d; "
"border: 1px solid rgba(120, 130, 140, 90); padding: 3px 6px; }"));
m_attributionLabel->adjustSize();
}
void MapWidget::setPoints(const QVector<TelemetryPoint> &points)
{
const bool wasEmpty = m_points.isEmpty();
m_points = points;
if (!m_points.isEmpty() && m_autoFollow) {
const TelemetryPoint &last = m_points.last();
m_centerLat = last.latitude;
m_centerLon = last.longitude;
if (wasEmpty) {
m_zoom = MaxZoom;
}
}
update();
}
void MapWidget::clearPoints()
{
m_points.clear();
m_hasAverageMarker = false;
update();
}
void MapWidget::setAverageMarker(double latitude, double longitude, double radiusMeters)
{
m_hasAverageMarker = true;
m_averageLat = latitude;
m_averageLon = longitude;
m_averageRadiusMeters = std::max(0.0, radiusMeters);
update();
}
void MapWidget::clearAverageMarker()
{
if (!m_hasAverageMarker) {
return;
}
m_hasAverageMarker = false;
update();
}
void MapWidget::setCepMarker(double latitude, double longitude,
const std::optional<double> &radiusMeters)
{
m_hasCepMarker = true;
m_cepLatitude = latitude;
m_cepLongitude = longitude;
if (m_points.isEmpty()) {
m_centerLat = latitude;
m_centerLon = longitude;
m_zoom = MaxZoom;
}
if (radiusMeters.has_value()) {
m_cepRadiusMeters = std::max(0.0, *radiusMeters);
} else {
m_cepRadiusMeters.reset();
}
update();
}
void MapWidget::clearCepMarker()
{
if (!m_hasCepMarker) {
return;
}
m_hasCepMarker = false;
m_cepRadiusMeters.reset();
update();
}
void MapWidget::setAutoFollow(bool enabled)
{
if (m_autoFollow == enabled) {
return;
}
m_autoFollow = enabled;
if (m_autoFollow && !m_points.isEmpty()) {
const TelemetryPoint &last = m_points.last();
m_centerLat = last.latitude;
m_centerLon = last.longitude;
update();
}
emit autoFollowChanged(m_autoFollow);
}
void MapWidget::setAverageOverlayEnabled(bool enabled)
{
if (m_showAverageOverlay == enabled) {
return;
}
m_showAverageOverlay = enabled;
update();
}
void MapWidget::setTelemetryOverlayEnabled(bool enabled)
{
if (m_showTelemetryOverlay == enabled) {
return;
}
m_showTelemetryOverlay = enabled;
update();
}
void MapWidget::setRecentPointsEnabled(bool enabled)
{
if (m_showRecentPoints == enabled) {
return;
}
m_showRecentPoints = enabled;
update();
}
void MapWidget::zoomIn()
{
setZoomKeepingPoint(m_zoom + 1, QPointF(width() / 2.0, height() / 2.0));
}
void MapWidget::zoomOut()
{
setZoomKeepingPoint(m_zoom - 1, QPointF(width() / 2.0, height() / 2.0));
}
void MapWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.fillRect(rect(), QColor(219, 225, 220));
drawTiles(painter);
drawTelemetry(painter);
if (m_points.isEmpty()) {
drawEmptyState(painter);
}
}
void MapWidget::wheelEvent(QWheelEvent *event)
{
const int delta = event->angleDelta().y();
if (delta == 0) {
return;
}
disableAutoFollowFromUserAction();
setZoomKeepingPoint(m_zoom + (delta > 0 ? 1 : -1), event->position());
event->accept();
}
void MapWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::RightButton) {
const QPointF coordinate = screenToCoordinate(event->position());
double longitude = std::fmod(coordinate.y() + 180.0, 360.0);
if (longitude < 0.0) {
longitude += 360.0;
}
longitude -= 180.0;
emit referenceLocationSelected(coordinate.x(), longitude);
event->accept();
return;
}
if (event->button() == Qt::LeftButton) {
disableAutoFollowFromUserAction();
m_dragging = true;
m_lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
event->accept();
}
}
void MapWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!m_dragging) {
return;
}
const QPoint delta = event->pos() - m_lastMousePos;
m_lastMousePos = event->pos();
const QPointF centerWorld = coordinateToWorld(m_centerLat, m_centerLon, m_zoom);
const QPointF newCenterWorld = centerWorld - QPointF(delta);
const QPointF coordinate = worldToCoordinate(newCenterWorld, m_zoom);
m_centerLat = coordinate.x();
m_centerLon = coordinate.y();
update();
event->accept();
}
void MapWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && m_dragging) {
m_dragging = false;
unsetCursor();
event->accept();
}
}
void MapWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (m_attributionLabel != nullptr) {
m_attributionLabel->adjustSize();
m_attributionLabel->move(width() - m_attributionLabel->width() - 8,
height() - m_attributionLabel->height() - 8);
m_attributionLabel->raise();
}
}
QPointF MapWidget::coordinateToWorld(double latitude, double longitude, int zoom) const
{
latitude = clampLatitude(latitude);
const double sinLat = std::sin(latitude * Pi / 180.0);
const double scale = static_cast<double>(TileSize) * static_cast<double>(1 << zoom);
const double x = (longitude + 180.0) / 360.0 * scale;
const double y = (0.5 - std::log((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Pi)) * scale;
return QPointF(x, y);
}
QPointF MapWidget::worldToCoordinate(const QPointF &world, int zoom) const
{
const double scale = static_cast<double>(TileSize) * static_cast<double>(1 << zoom);
const double longitude = world.x() / scale * 360.0 - 180.0;
const double n = Pi - 2.0 * Pi * world.y() / scale;
const double latitude = 180.0 / Pi * std::atan(0.5 * (std::exp(n) - std::exp(-n)));
return QPointF(clampLatitude(latitude), longitude);
}
QPointF MapWidget::coordinateToScreen(double latitude, double longitude) const
{
const QPointF world = coordinateToWorld(latitude, longitude, m_zoom);
const QPointF centerWorld = coordinateToWorld(m_centerLat, m_centerLon, m_zoom);
const double worldWidth = static_cast<double>(TileSize) * static_cast<double>(1 << m_zoom);
double deltaX = world.x() - centerWorld.x();
if (deltaX > worldWidth / 2.0) {
deltaX -= worldWidth;
} else if (deltaX < -worldWidth / 2.0) {
deltaX += worldWidth;
}
return QPointF(width() / 2.0 + deltaX, height() / 2.0 + world.y() - centerWorld.y());
}
QPointF MapWidget::screenToCoordinate(const QPointF &screen) const
{
return worldToCoordinate(topLeftWorld() + screen, m_zoom);
}
QPointF MapWidget::topLeftWorld() const
{
return coordinateToWorld(m_centerLat, m_centerLon, m_zoom)
- QPointF(width() / 2.0, height() / 2.0);
}
void MapWidget::drawTiles(QPainter &painter)
{
const int tileCount = 1 << m_zoom;
const QPointF topLeft = topLeftWorld();
const int firstTileX = static_cast<int>(std::floor(topLeft.x() / TileSize));
const int lastTileX = static_cast<int>(std::floor((topLeft.x() + width()) / TileSize));
const int firstTileY = static_cast<int>(std::floor(topLeft.y() / TileSize));
const int lastTileY = static_cast<int>(std::floor((topLeft.y() + height()) / TileSize));
painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
for (int tileY = firstTileY; tileY <= lastTileY; ++tileY) {
if (tileY < 0 || tileY >= tileCount) {
continue;
}
for (int tileX = firstTileX; tileX <= lastTileX; ++tileX) {
const int wrappedTileX = positiveModulo(tileX, tileCount);
const QString key = tileKey(m_zoom, wrappedTileX, tileY);
const QRectF target(
tileX * TileSize - topLeft.x(),
tileY * TileSize - topLeft.y(),
TileSize,
TileSize);
const auto tileIt = m_tileCache.constFind(key);
if (tileIt != m_tileCache.constEnd()) {
painter.drawPixmap(target.toRect(), *tileIt);
} else {
painter.fillRect(target, QColor(230, 234, 230));
painter.setPen(QColor(198, 205, 200));
painter.drawRect(target);
requestTile(wrappedTileX, tileY, m_zoom);
}
}
}
}
void MapWidget::drawTelemetry(QPainter &painter)
{
if (m_points.isEmpty() || !m_showTelemetryOverlay) {
return;
}
painter.setRenderHint(QPainter::Antialiasing, true);
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);
}
}
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);
}
drawAverageOverlay(painter);
drawCepMarker(painter);
const bool drawNumbers = m_points.size() <= 150;
const int firstPoint = m_showRecentPoints ? 0 : m_points.size() - 1;
for (int i = firstPoint; i < m_points.size(); ++i) {
const bool isLast = i == m_points.size() - 1;
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
const double radius = isLast ? 8.0 : 5.0;
painter.setPen(QPen(Qt::white, isLast ? 3.0 : 2.0));
painter.setBrush(isLast ? QColor(31, 150, 82) : QColor(37, 99, 235));
painter.drawEllipse(screen, radius, radius);
if (drawNumbers) {
painter.setPen(Qt::white);
painter.setFont(QFont(painter.font().family(), 8, QFont::Bold));
painter.drawText(QRectF(screen.x() - 13, screen.y() - 9, 26, 18), Qt::AlignCenter,
QString::number(i + 1));
}
}
const TelemetryPoint &last = m_points.last();
const QPointF lastScreen = coordinateToScreen(last.latitude, last.longitude);
QStringList lines;
const QString latitudeText = last.latitudeText.isEmpty()
? QString::number(last.latitude, 'f', 15) : last.latitudeText;
const QString longitudeText = last.longitudeText.isEmpty()
? QString::number(last.longitude, 'f', 15) : last.longitudeText;
lines << QStringLiteral("Lat: %1").arg(latitudeText);
lines << QStringLiteral("Long: %1").arg(longitudeText);
if (last.hheight.has_value()) {
lines << QStringLiteral("HHeight: %1").arg(*last.hheight, 0, 'f', 2);
}
if (last.satellites.has_value()) {
lines << QStringLiteral("Aktif uydu: %1").arg(*last.satellites);
}
if (m_cepRadiusMeters.has_value()) {
lines << QStringLiteral("CEP50: %1 m").arg(*m_cepRadiusMeters, 0, 'f', 2);
}
painter.setFont(QFont(painter.font().family(), 10));
const QString label = lines.join('\n');
const QRect textRect = painter.fontMetrics().boundingRect(QRect(0, 0, 300, 150),
Qt::TextWordWrap, label);
QRectF bubble(lastScreen + QPointF(14, -textRect.height() - 12),
QSizeF(textRect.width() + 18, textRect.height() + 14));
if (bubble.right() > width() - 8) {
bubble.moveRight(width() - 8);
}
if (bubble.top() < 8) {
bubble.moveTop(8);
}
painter.setPen(QPen(QColor(50, 58, 65, 180), 1.0));
painter.setBrush(QColor(255, 255, 255, 232));
painter.drawRoundedRect(bubble, 6, 6);
painter.setPen(QColor(25, 32, 38));
painter.drawText(bubble.adjusted(9, 7, -9, -7), Qt::AlignLeft | Qt::AlignVCenter, label);
}
void MapWidget::drawCepOverlay(QPainter &painter)
{
if (!m_hasCepMarker || !m_cepRadiusMeters.has_value() || *m_cepRadiusMeters <= 0.0) {
return;
}
const QPointF screen = coordinateToScreen(m_cepLatitude, m_cepLongitude);
const double resolution = metersPerPixel(m_cepLatitude, m_zoom);
if (!std::isfinite(resolution) || resolution <= 0.0) {
return;
}
const double radiusPixels = std::min(*m_cepRadiusMeters / resolution, 2500.0);
if (radiusPixels < 1.0) {
return;
}
painter.setPen(QPen(QColor(8, 111, 104, 210), 2.0));
painter.setBrush(QColor(13, 148, 136, 42));
painter.drawEllipse(screen, radiusPixels, radiusPixels);
}
void MapWidget::drawCepMarker(QPainter &painter)
{
if (!m_hasCepMarker) {
return;
}
const QPointF screen = coordinateToScreen(m_cepLatitude, m_cepLongitude);
painter.setPen(QPen(QColor(8, 111, 104), 2.5));
painter.setBrush(QColor(255, 255, 255, 230));
painter.drawEllipse(screen, 7.0, 7.0);
painter.drawLine(QPointF(screen.x() - 11.0, screen.y()),
QPointF(screen.x() + 11.0, screen.y()));
painter.drawLine(QPointF(screen.x(), screen.y() - 11.0),
QPointF(screen.x(), screen.y() + 11.0));
const bool overlapsLast = !m_points.isEmpty()
&& QLineF(screen, coordinateToScreen(m_points.last().latitude,
m_points.last().longitude)).length() < 30.0;
if (!overlapsLast) {
painter.setFont(QFont(painter.font().family(), 9, QFont::Bold));
painter.setPen(QColor(20, 83, 78));
painter.drawText(QRectF(screen + QPointF(12.0, -10.0), QSizeF(110.0, 20.0)),
Qt::AlignLeft | Qt::AlignVCenter, QStringLiteral("Bilinen konum"));
}
}
void MapWidget::drawAverageOverlay(QPainter &painter)
{
if (!m_showAverageOverlay || !m_hasAverageMarker) {
return;
}
const QPointF screen = coordinateToScreen(m_averageLat, m_averageLon);
const double resolution = metersPerPixel(m_averageLat, m_zoom);
if (std::isfinite(resolution) && resolution > 0.0 && m_averageRadiusMeters > 0.0) {
const double radiusPixels = std::min(m_averageRadiusMeters / resolution, 2500.0);
if (radiusPixels >= 2.0) {
painter.setPen(QPen(QColor(124, 58, 237, 150), 2.0, Qt::DashLine));
painter.setBrush(QColor(124, 58, 237, 30));
painter.drawEllipse(screen, radiusPixels, radiusPixels);
}
}
painter.setPen(QPen(Qt::white, 3.0));
painter.setBrush(QColor(124, 58, 237));
painter.drawEllipse(screen, 9.0, 9.0);
painter.setPen(QPen(QColor(25, 32, 38), 2.0));
painter.drawLine(QPointF(screen.x() - 12.0, screen.y()), QPointF(screen.x() + 12.0, screen.y()));
painter.drawLine(QPointF(screen.x(), screen.y() - 12.0), QPointF(screen.x(), screen.y() + 12.0));
const bool overlapsLast = !m_points.isEmpty()
&& QLineF(screen, coordinateToScreen(m_points.last().latitude,
m_points.last().longitude)).length() < 30.0;
if (!overlapsLast) {
painter.setFont(QFont(painter.font().family(), 9, QFont::Bold));
const QRectF labelRect(screen + QPointF(12.0, 8.0), QSizeF(72.0, 20.0));
painter.setPen(QColor(25, 32, 38));
painter.drawText(labelRect, Qt::AlignLeft | Qt::AlignVCenter, QStringLiteral("Ortalama"));
}
}
void MapWidget::drawEmptyState(QPainter &painter)
{
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(QColor(83, 96, 110));
painter.setFont(QFont(painter.font().family(), 13));
painter.drawText(rect(), Qt::AlignCenter,
QStringLiteral("Seri porttan lat / long / hheight bekleniyor"));
}
void MapWidget::requestTile(int tileX, int tileY, int zoom)
{
const QString key = tileKey(zoom, tileX, tileY);
if (m_tileCache.contains(key) || m_pendingTiles.contains(key)) {
return;
}
m_pendingTiles.insert(key);
const QUrl url(QStringLiteral("https://tile.openstreetmap.org/%1/%2/%3.png")
.arg(zoom)
.arg(tileX)
.arg(tileY));
QNetworkRequest request(url);
request.setRawHeader("User-Agent", "RocketLocationPlotter/0.2 (Qt desktop application)");
QNetworkReply *reply = m_network->get(request);
reply->setProperty("tileKey", key);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QString key = reply->property("tileKey").toString();
m_pendingTiles.remove(key);
if (reply->error() == QNetworkReply::NoError) {
QPixmap pixmap;
if (pixmap.loadFromData(reply->readAll())) {
if (!m_tileCache.contains(key) && m_tileCache.size() >= MaxMemoryTileCount) {
m_tileCache.erase(m_tileCache.begin());
}
m_tileCache.insert(key, pixmap);
update();
}
}
reply->deleteLater();
});
}
void MapWidget::setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor)
{
newZoom = std::clamp(newZoom, MinZoom, MaxZoom);
if (newZoom == m_zoom) {
return;
}
const QPointF coordinateAtAnchor = screenToCoordinate(screenAnchor);
m_zoom = newZoom;
const QPointF anchorWorld = coordinateToWorld(coordinateAtAnchor.x(), coordinateAtAnchor.y(), m_zoom);
const QPointF newCenterWorld = anchorWorld - (screenAnchor - QPointF(width() / 2.0, height() / 2.0));
const QPointF newCenter = worldToCoordinate(newCenterWorld, m_zoom);
m_centerLat = newCenter.x();
m_centerLon = newCenter.y();
update();
}
void MapWidget::disableAutoFollowFromUserAction()
{
if (!m_autoFollow) {
return;
}
m_autoFollow = false;
emit autoFollowChanged(false);
}
+90
View File
@@ -0,0 +1,90 @@
#pragma once
#include "TelemetryPoint.h"
#include <QHash>
#include <QPixmap>
#include <QPoint>
#include <QSet>
#include <QWidget>
#include <optional>
class QNetworkAccessManager;
class QLabel;
class MapWidget : public QWidget {
Q_OBJECT
public:
explicit MapWidget(QWidget *parent = nullptr);
void setPoints(const QVector<TelemetryPoint> &points);
void clearPoints();
void setAverageMarker(double latitude, double longitude, double radiusMeters);
void clearAverageMarker();
void setCepMarker(double latitude, double longitude,
const std::optional<double> &radiusMeters);
void clearCepMarker();
public slots:
void setAutoFollow(bool enabled);
void setTelemetryOverlayEnabled(bool enabled);
void setAverageOverlayEnabled(bool enabled);
void setRecentPointsEnabled(bool enabled);
void zoomIn();
void zoomOut();
signals:
void autoFollowChanged(bool enabled);
void referenceLocationSelected(double latitude, double longitude);
protected:
void paintEvent(QPaintEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
QPointF coordinateToWorld(double latitude, double longitude, int zoom) const;
QPointF worldToCoordinate(const QPointF &world, int zoom) const;
QPointF coordinateToScreen(double latitude, double longitude) const;
QPointF screenToCoordinate(const QPointF &screen) const;
QPointF topLeftWorld() const;
void drawTiles(QPainter &painter);
void drawTelemetry(QPainter &painter);
void drawAverageOverlay(QPainter &painter);
void drawCepOverlay(QPainter &painter);
void drawCepMarker(QPainter &painter);
void drawEmptyState(QPainter &painter);
void requestTile(int tileX, int tileY, int zoom);
void setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor);
void disableAutoFollowFromUserAction();
QVector<TelemetryPoint> m_points;
QNetworkAccessManager *m_network = nullptr;
QLabel *m_attributionLabel = nullptr;
QHash<QString, QPixmap> m_tileCache;
QSet<QString> m_pendingTiles;
double m_centerLat = 39.0;
double m_centerLon = 35.0;
int m_zoom = 6;
bool m_autoFollow = true;
bool m_showTelemetryOverlay = true;
bool m_showAverageOverlay = true;
bool m_showRecentPoints = true;
bool m_hasAverageMarker = false;
double m_averageLat = 0.0;
double m_averageLon = 0.0;
double m_averageRadiusMeters = 0.0;
bool m_hasCepMarker = false;
double m_cepLatitude = 0.0;
double m_cepLongitude = 0.0;
std::optional<double> m_cepRadiusMeters;
bool m_dragging = false;
QPoint m_lastMousePos;
};
+339
View File
@@ -0,0 +1,339 @@
#include "RinexNavigation.h"
#include <QDir>
#include <QFile>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QStandardPaths>
#include <QUrl>
#include <zlib.h>
#include <algorithm>
#include <cmath>
#include <limits>
namespace {
constexpr double GpsMu = 3.986005e14;
constexpr double EarthRotation = 7.2921151467e-5;
constexpr double GpsWeekSeconds = 604800.0;
constexpr double HalfGpsWeek = GpsWeekSeconds / 2.0;
constexpr double CurrentGpsUtcOffsetSeconds = 18.0;
constexpr int PositionUpdateIntervalMs = 60000;
struct Ephemeris {
QString prn;
QDateTime toc;
double crs = 0.0;
double deltaN = 0.0;
double m0 = 0.0;
double cuc = 0.0;
double eccentricity = 0.0;
double cus = 0.0;
double sqrtA = 0.0;
double toe = 0.0;
double cic = 0.0;
double omega0 = 0.0;
double cis = 0.0;
double i0 = 0.0;
double crc = 0.0;
double omega = 0.0;
double omegaDot = 0.0;
double idot = 0.0;
int gpsWeek = 0;
bool healthy = true;
};
double parseField(const QString &line, int field)
{
QString text = line.mid(4 + field * 19, 19).trimmed();
text.replace(QChar('D'), QChar('E'), Qt::CaseInsensitive);
bool ok = false;
const double value = text.toDouble(&ok);
return ok ? value : std::numeric_limits<double>::quiet_NaN();
}
bool finiteFields(std::initializer_list<double> values)
{
return std::all_of(values.begin(), values.end(), [](double value) {
return std::isfinite(value);
});
}
QByteArray gunzip(const QByteArray &input)
{
if (input.size() < 2 || static_cast<unsigned char>(input[0]) != 0x1f
|| static_cast<unsigned char>(input[1]) != 0x8b) {
return input;
}
z_stream stream{};
stream.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(input.constData()));
stream.avail_in = static_cast<uInt>(input.size());
if (inflateInit2(&stream, 16 + MAX_WBITS) != Z_OK) {
return {};
}
QByteArray output;
char buffer[64 * 1024];
int result = Z_OK;
while (result == Z_OK) {
stream.next_out = reinterpret_cast<Bytef *>(buffer);
stream.avail_out = sizeof(buffer);
result = inflate(&stream, Z_NO_FLUSH);
output.append(buffer, static_cast<int>(sizeof(buffer) - stream.avail_out));
}
inflateEnd(&stream);
return result == Z_STREAM_END ? output : QByteArray{};
}
QDateTime gpsEpoch()
{
return QDateTime(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC);
}
SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc)
{
const double gpsSeconds = gpsEpoch().msecsTo(whenUtc) / 1000.0
+ CurrentGpsUtcOffsetSeconds;
const double absoluteToe = eph.gpsWeek * GpsWeekSeconds + eph.toe;
double tk = gpsSeconds - absoluteToe;
while (tk > HalfGpsWeek) tk -= GpsWeekSeconds;
while (tk < -HalfGpsWeek) tk += GpsWeekSeconds;
const double a = eph.sqrtA * eph.sqrtA;
const double meanMotion = std::sqrt(GpsMu / (a * a * a)) + eph.deltaN;
const double meanAnomaly = eph.m0 + meanMotion * tk;
double eccentricAnomaly = meanAnomaly;
for (int iteration = 0; iteration < 12; ++iteration) {
eccentricAnomaly = meanAnomaly + eph.eccentricity * std::sin(eccentricAnomaly);
}
const double trueAnomaly = std::atan2(
std::sqrt(1.0 - eph.eccentricity * eph.eccentricity) * std::sin(eccentricAnomaly),
std::cos(eccentricAnomaly) - eph.eccentricity);
const double phi = trueAnomaly + eph.omega;
const double twoPhi = 2.0 * phi;
const double u = phi + eph.cus * std::sin(twoPhi) + eph.cuc * std::cos(twoPhi);
const double radius = a * (1.0 - eph.eccentricity * std::cos(eccentricAnomaly))
+ eph.crs * std::sin(twoPhi) + eph.crc * std::cos(twoPhi);
const double inclination = eph.i0 + eph.idot * tk
+ eph.cis * std::sin(twoPhi) + eph.cic * std::cos(twoPhi);
const double orbitalX = radius * std::cos(u);
const double orbitalY = radius * std::sin(u);
const double ascendingNode = eph.omega0 + (eph.omegaDot - EarthRotation) * tk
- EarthRotation * eph.toe;
SatellitePosition result;
result.prn = eph.prn;
result.x = orbitalX * std::cos(ascendingNode)
- orbitalY * std::cos(inclination) * std::sin(ascendingNode);
result.y = orbitalX * std::sin(ascendingNode)
+ orbitalY * std::cos(inclination) * std::cos(ascendingNode);
result.z = orbitalY * std::sin(inclination);
result.healthy = eph.healthy;
return result;
}
} // namespace
RinexNavigation::RinexNavigation(QObject *parent)
: QObject(parent)
{
m_positionTimer.setInterval(PositionUpdateIntervalMs);
connect(&m_positionTimer, &QTimer::timeout,
this, &RinexNavigation::updateCurrentPositions);
m_positionTimer.start();
}
void RinexNavigation::refresh()
{
if (m_loading) return;
if (m_latestRinex.isEmpty()) {
loadCache();
}
m_loading = true;
emit statusChanged(QStringLiteral("Guncel RINEX efemerisi indiriliyor..."), false);
requestDay(QDateTime::currentDateTimeUtc().date(), 0);
}
void RinexNavigation::requestDay(const QDate &date, int fallbackIndex)
{
const QString year = QString::number(date.year());
const QString day = QStringLiteral("%1").arg(date.dayOfYear(), 3, 10, QLatin1Char('0'));
// BKG's stream-derived file is updated during the UTC day; the final IGS
// combination is normally published later and is therefore unsuitable here.
const QString file = QStringLiteral("BRDC00WRD_S_") + year + day
+ QStringLiteral("0000_01D_MN.rnx.gz");
const QUrl url(QStringLiteral("https://igs.bkg.bund.de/root_ftp/IGS/BRDC/%1/%2/%3")
.arg(year, day, file));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::UserAgentHeader,
QStringLiteral("ANKASYS-LocationPlotter/1.0 (RINEX client)"));
request.setTransferTimeout(15000);
QNetworkReply *reply = m_network.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, date, fallbackIndex] {
const bool ok = reply->error() == QNetworkReply::NoError;
const QByteArray data = ok ? reply->readAll() : QByteArray{};
reply->deleteLater();
if (!ok || data.isEmpty()) {
if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1);
} else {
m_loading = false;
if (!loadCache()) {
emit statusChanged(QStringLiteral("RINEX indirilemedi; internet baglantisini kontrol edin"), true);
}
}
return;
}
handleDownload(data, date, fallbackIndex);
});
}
void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &date,
int fallbackIndex)
{
const QByteArray rinex = gunzip(compressed);
QString error;
const QVector<SatellitePosition> positions = parseGpsNavigation(
rinex, QDateTime::currentDateTimeUtc(), &error);
if (positions.isEmpty()) {
if (fallbackIndex < 2) {
requestDay(date.addDays(-1), fallbackIndex + 1);
return;
}
m_loading = false;
emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true);
return;
}
QFile cache(cachePath());
QDir().mkpath(QFileInfo(cache).absolutePath());
if (cache.open(QIODevice::WriteOnly)) cache.write(rinex);
m_latestRinex = rinex;
m_loading = false;
publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · RINEX %2 (UTC)")
.arg(positions.size()).arg(date.toString(QStringLiteral("dd.MM.yyyy"))), false);
}
bool RinexNavigation::loadCache()
{
QFile cache(cachePath());
if (!cache.open(QIODevice::ReadOnly)) return false;
QString error;
m_latestRinex = cache.readAll();
const QVector<SatellitePosition> positions = parseGpsNavigation(
m_latestRinex, QDateTime::currentDateTimeUtc(), &error);
if (positions.isEmpty()) return false;
publishCurrentMotion();
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX").arg(positions.size()), true);
return true;
}
QString RinexNavigation::cachePath() const
{
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+ QStringLiteral("/latest-navigation.rnx");
}
void RinexNavigation::updateCurrentPositions()
{
if (m_latestRinex.isEmpty()) return;
publishCurrentMotion();
}
void RinexNavigation::publishCurrentMotion()
{
if (m_latestRinex.isEmpty()) return;
const QDateTime now = QDateTime::currentDateTimeUtc();
const QVector<SatellitePosition> current = parseGpsNavigation(m_latestRinex, now);
if (current.isEmpty()) return;
const QVector<SatellitePosition> next = parseGpsNavigation(
m_latestRinex, now.addMSecs(PositionUpdateIntervalMs));
emit satellitesUpdated(current);
if (!next.isEmpty()) {
emit satelliteMotionUpdated(current, next, PositionUpdateIntervalMs);
}
}
QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
const QByteArray &rinex, const QDateTime &whenUtc, QString *error)
{
const QString text = QString::fromLatin1(rinex);
const QStringList lines = text.split('\n');
int index = 0;
while (index < lines.size() && !lines[index].contains(QStringLiteral("END OF HEADER"))) ++index;
if (index >= lines.size()) {
if (error) *error = QStringLiteral("END OF HEADER bulunamadi");
return {};
}
++index;
QHash<QString, Ephemeris> closest;
QHash<QString, qint64> closestAge;
while (index < lines.size()) {
const QString first = lines[index];
if (first.size() < 3) { ++index; continue; }
const QChar system = first[0];
if (!system.isLetter()) { ++index; continue; }
const int blockLines = (system == QLatin1Char('R') || system == QLatin1Char('S')) ? 4 : 8;
if (index + blockLines > lines.size()) break;
if (system != QLatin1Char('G')) { index += blockLines; continue; }
const QStringList epoch = first.mid(3, 20).simplified().split(' ');
if (epoch.size() < 6) { index += 8; continue; }
bool dateOk = false;
const int year = epoch[0].toInt(&dateOk);
const QDateTime toc(QDate(year, epoch[1].toInt(), epoch[2].toInt()),
QTime(epoch[3].toInt(), epoch[4].toInt(),
static_cast<int>(epoch[5].toDouble())), Qt::UTC);
if (!dateOk || !toc.isValid()) { index += 8; continue; }
Ephemeris eph;
eph.prn = first.left(3).trimmed().toUpper();
eph.toc = toc;
eph.crs = parseField(lines[index + 1], 1);
eph.deltaN = parseField(lines[index + 1], 2);
eph.m0 = parseField(lines[index + 1], 3);
eph.cuc = parseField(lines[index + 2], 0);
eph.eccentricity = parseField(lines[index + 2], 1);
eph.cus = parseField(lines[index + 2], 2);
eph.sqrtA = parseField(lines[index + 2], 3);
eph.toe = parseField(lines[index + 3], 0);
eph.cic = parseField(lines[index + 3], 1);
eph.omega0 = parseField(lines[index + 3], 2);
eph.cis = parseField(lines[index + 3], 3);
eph.i0 = parseField(lines[index + 4], 0);
eph.crc = parseField(lines[index + 4], 1);
eph.omega = parseField(lines[index + 4], 2);
eph.omegaDot = parseField(lines[index + 4], 3);
eph.idot = parseField(lines[index + 5], 0);
eph.gpsWeek = static_cast<int>(std::llround(parseField(lines[index + 5], 2)));
const double health = parseField(lines[index + 6], 1);
eph.healthy = !std::isfinite(health) || std::llround(health) == 0;
if (finiteFields({eph.crs, eph.deltaN, eph.m0, eph.cuc, eph.eccentricity,
eph.cus, eph.sqrtA, eph.toe, eph.cic, eph.omega0, eph.cis,
eph.i0, eph.crc, eph.omega, eph.omegaDot, eph.idot})
&& eph.sqrtA > 4000.0 && eph.gpsWeek > 0) {
const qint64 age = std::abs(toc.secsTo(whenUtc));
if (!closestAge.contains(eph.prn) || age < closestAge[eph.prn]) {
closest[eph.prn] = eph;
closestAge[eph.prn] = age;
}
}
index += 8;
}
QVector<SatellitePosition> result;
result.reserve(closest.size());
for (const Ephemeris &eph : closest) result.push_back(propagate(eph, whenUtc));
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return a.prn < b.prn; });
if (result.isEmpty() && error) *error = QStringLiteral("GPS efemerisi bulunamadi");
return result;
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include "SatelliteData.h"
#include <QDateTime>
#include <QNetworkAccessManager>
#include <QObject>
#include <QVector>
#include <QTimer>
class RinexNavigation : public QObject {
Q_OBJECT
public:
explicit RinexNavigation(QObject *parent = nullptr);
void refresh();
[[nodiscard]] bool isLoading() const { return m_loading; }
static QVector<SatellitePosition> parseGpsNavigation(
const QByteArray &rinex, const QDateTime &whenUtc, QString *error = nullptr);
signals:
void satellitesUpdated(const QVector<SatellitePosition> &satellites);
void satelliteMotionUpdated(const QVector<SatellitePosition> &current,
const QVector<SatellitePosition> &next,
int durationMs);
void statusChanged(const QString &status, bool error);
private:
void requestDay(const QDate &date, int fallbackIndex);
void handleDownload(const QByteArray &compressed, const QDate &date, int fallbackIndex);
bool loadCache();
QString cachePath() const;
void updateCurrentPositions();
void publishCurrentMotion();
QNetworkAccessManager m_network;
QTimer m_positionTimer;
QByteArray m_latestRinex;
bool m_loading = false;
};
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <QString>
struct SatellitePosition {
QString prn;
double x = 0.0; // Earth-centred, Earth-fixed metres
double y = 0.0;
double z = 0.0;
bool healthy = true;
};
+266
View File
@@ -0,0 +1,266 @@
#include "SerialReader.h"
#include <QSocketNotifier>
#include <QTimer>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
namespace {
constexpr qsizetype MaxBytesPerReadCycle = 64 * 1024;
speed_t baudToSpeed(int baudRate)
{
switch (baudRate) {
case 1200:
return B1200;
case 2400:
return B2400;
case 4800:
return B4800;
case 9600:
return B9600;
case 19200:
return B19200;
case 38400:
return B38400;
case 57600:
return B57600;
case 115200:
return B115200;
case 230400:
return B230400;
case 460800:
return B460800;
case 921600:
return B921600;
default:
return 0;
}
}
} // namespace
SerialReader::SerialReader(QObject *parent)
: QObject(parent)
{
m_pollTimer = new QTimer(this);
m_pollTimer->setInterval(20);
connect(m_pollTimer, &QTimer::timeout, this, &SerialReader::handleReadyRead);
m_partialLineTimer = new QTimer(this);
m_partialLineTimer->setSingleShot(true);
m_partialLineTimer->setInterval(250);
connect(m_partialLineTimer, &QTimer::timeout, this, &SerialReader::flushPartialLine);
}
SerialReader::~SerialReader()
{
closePort();
}
bool SerialReader::isOpen() const
{
return m_fd >= 0;
}
QString SerialReader::portName() const
{
return m_portName;
}
bool SerialReader::openPort(const QString &path, int baudRate)
{
closePort();
const QByteArray localPath = path.toLocal8Bit();
m_fd = ::open(localPath.constData(), O_RDWR | O_NOCTTY | O_NONBLOCK);
if (m_fd < 0) {
emitSystemError(QStringLiteral("Port acilamadi"));
return false;
}
m_portName = path;
if (!configurePort(baudRate)) {
closePort();
return false;
}
setModemControlLines();
m_notifier = new QSocketNotifier(m_fd, QSocketNotifier::Read, this);
connect(m_notifier, &QSocketNotifier::activated, this, &SerialReader::handleReadyRead);
m_pollTimer->start();
emit statusChanged(QStringLiteral("%1 dinleniyor (%2 baud)").arg(path).arg(baudRate));
return true;
}
void SerialReader::closePort()
{
if (m_notifier != nullptr) {
m_notifier->setEnabled(false);
m_notifier->deleteLater();
m_notifier = nullptr;
}
m_pollTimer->stop();
m_partialLineTimer->stop();
if (m_fd >= 0) {
::close(m_fd);
m_fd = -1;
}
m_buffer.clear();
if (!m_portName.isEmpty()) {
emit statusChanged(QStringLiteral("%1 kapatildi").arg(m_portName));
}
m_portName.clear();
}
void SerialReader::handleReadyRead()
{
if (m_fd < 0) {
return;
}
char data[4096];
qsizetype bytesProcessed = 0;
while (true) {
const ssize_t bytesRead = ::read(m_fd, data, sizeof(data));
if (bytesRead > 0) {
bytesProcessed += static_cast<qsizetype>(bytesRead);
m_buffer.append(data, static_cast<qsizetype>(bytesRead));
processBuffer();
if (m_buffer.size() > 64 * 1024) {
m_buffer.clear();
emit errorOccurred(QStringLiteral("Satir sonu gelmeyen seri veri tamponu temizlendi"));
}
if (bytesProcessed >= MaxBytesPerReadCycle) {
QTimer::singleShot(0, this, &SerialReader::handleReadyRead);
return;
}
continue;
}
if (bytesRead == 0 || errno == EAGAIN || errno == EWOULDBLOCK) {
return;
}
emitSystemError(QStringLiteral("Seri port okuma hatasi"));
closePort();
return;
}
}
void SerialReader::flushPartialLine()
{
if (m_buffer.isEmpty()) {
return;
}
QByteArray line = m_buffer;
m_buffer.clear();
while (line.endsWith('\r') || line.endsWith('\n')) {
line.chop(1);
}
if (!line.isEmpty()) {
emit lineReceived(QString::fromUtf8(line));
}
}
bool SerialReader::configurePort(int baudRate)
{
const speed_t speed = baudToSpeed(baudRate);
if (speed == 0) {
emit errorOccurred(QStringLiteral("Desteklenmeyen baud rate: %1").arg(baudRate));
return false;
}
termios options {};
if (::tcgetattr(m_fd, &options) != 0) {
emitSystemError(QStringLiteral("Port ayarlari okunamadi"));
return false;
}
::cfmakeraw(&options);
::cfsetispeed(&options, speed);
::cfsetospeed(&options, speed);
options.c_cflag |= CLOCAL | CREAD;
options.c_cflag &= ~HUPCL;
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 0;
if (::tcsetattr(m_fd, TCSANOW, &options) != 0) {
emitSystemError(QStringLiteral("Port ayarlari uygulanamadi"));
return false;
}
::tcflush(m_fd, TCIFLUSH);
return true;
}
void SerialReader::processBuffer()
{
bool emittedLine = false;
while (true) {
qsizetype delimiterIndex = -1;
for (qsizetype i = 0; i < m_buffer.size(); ++i) {
if (m_buffer.at(i) == '\n' || m_buffer.at(i) == '\r') {
delimiterIndex = i;
break;
}
}
if (delimiterIndex < 0) {
break;
}
QByteArray line = m_buffer.left(delimiterIndex);
qsizetype removeCount = delimiterIndex + 1;
while (removeCount < m_buffer.size()
&& (m_buffer.at(removeCount) == '\n' || m_buffer.at(removeCount) == '\r')) {
++removeCount;
}
m_buffer.remove(0, removeCount);
if (!line.isEmpty()) {
emittedLine = true;
emit lineReceived(QString::fromUtf8(line));
}
}
if (!m_buffer.isEmpty()) {
m_partialLineTimer->start();
} else if (emittedLine) {
m_partialLineTimer->stop();
}
}
void SerialReader::setModemControlLines()
{
int status = 0;
if (::ioctl(m_fd, TIOCMGET, &status) != 0) {
return;
}
status |= TIOCM_DTR;
status |= TIOCM_RTS;
::ioctl(m_fd, TIOCMSET, &status);
}
void SerialReader::emitSystemError(const QString &prefix)
{
emit errorOccurred(QStringLiteral("%1: %2").arg(prefix, QString::fromLocal8Bit(std::strerror(errno))));
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <QObject>
#include <QString>
class QSocketNotifier;
class QTimer;
class SerialReader : public QObject {
Q_OBJECT
public:
explicit SerialReader(QObject *parent = nullptr);
~SerialReader() override;
[[nodiscard]] bool isOpen() const;
[[nodiscard]] QString portName() const;
public slots:
bool openPort(const QString &path, int baudRate);
void closePort();
signals:
void lineReceived(const QString &line);
void statusChanged(const QString &status);
void errorOccurred(const QString &message);
private slots:
void handleReadyRead();
void flushPartialLine();
private:
bool configurePort(int baudRate);
void processBuffer();
void setModemControlLines();
void emitSystemError(const QString &prefix);
int m_fd = -1;
QSocketNotifier *m_notifier = nullptr;
QTimer *m_pollTimer = nullptr;
QTimer *m_partialLineTimer = nullptr;
QByteArray m_buffer;
QString m_portName;
};
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <QDateTime>
#include <QString>
#include <optional>
struct DopValues {
std::optional<double> gdop;
std::optional<double> pdop;
std::optional<double> hdop;
std::optional<double> vdop;
std::optional<double> tdop;
[[nodiscard]] bool hasAnyValue() const
{
return gdop.has_value() || pdop.has_value() || hdop.has_value()
|| vdop.has_value() || tdop.has_value();
}
};
struct TelemetryPoint {
double latitude = 0.0;
double longitude = 0.0;
QString latitudeText;
QString longitudeText;
std::optional<double> hheight;
std::optional<int> satellites;
DopValues dop;
QDateTime receivedAt = QDateTime::currentDateTimeUtc();
QString rawLine;
};
struct ParsedTelemetry {
std::optional<double> latitude;
std::optional<double> longitude;
QString latitudeText;
QString longitudeText;
std::optional<double> hheight;
std::optional<int> satellites;
DopValues dop;
QString rawLine;
[[nodiscard]] bool hasLocation() const
{
return latitude.has_value() && longitude.has_value();
}
[[nodiscard]] bool hasAnyValue() const
{
return latitude.has_value() || longitude.has_value() || hheight.has_value()
|| satellites.has_value() || dop.hasAnyValue();
}
};
+15
View File
@@ -0,0 +1,15 @@
#include "MainWindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QApplication::setApplicationName(QStringLiteral("location_plotter"));
QApplication::setOrganizationName(QStringLiteral("rocket"));
MainWindow window;
window.show();
return QApplication::exec();
}
+164
View File
@@ -0,0 +1,164 @@
#include "../src/MainWindow.h"
#include "../src/MapWidget.h"
#include <QApplication>
#include <QCheckBox>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QLabel>
#include <QLineEdit>
#include <QMetaObject>
#include <QSpinBox>
#include <QTableView>
#include <QTabWidget>
#include <QSplitter>
#include <cassert>
namespace {
void sendLine(MainWindow &window, const QString &line)
{
const bool invoked = QMetaObject::invokeMethod(
&window, "handleSerialLine", Qt::DirectConnection, Q_ARG(QString, line));
assert(invoked);
}
void flushUpdates(MainWindow &window)
{
const bool invoked = QMetaObject::invokeMethod(
&window, "flushUiUpdates", Qt::DirectConnection);
assert(invoked);
}
} // namespace
int main(int argc, char **argv)
{
QApplication app(argc, argv);
MainWindow window;
auto *table = window.findChild<QTableView *>();
auto *referenceLatInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLatInput"));
auto *referenceLonInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLonInput"));
auto *cepValue = window.findChild<QLabel *>(QStringLiteral("cepValue"));
auto *maxPointsInput = window.findChild<QSpinBox *>(QStringLiteral("maxPointsInput"));
auto *elevationMaskSpin = window.findChild<QDoubleSpinBox *>(QStringLiteral("elevationMaskSpin"));
auto *showTelemetry = window.findChild<QCheckBox *>(QStringLiteral("showTelemetryCheck"));
auto *showRecentPoints = window.findChild<QCheckBox *>(QStringLiteral("showRecentPointsCheck"));
auto *showAverage = window.findChild<QCheckBox *>(QStringLiteral("showAverageCheck"));
auto *portSelector = window.findChild<QComboBox *>(QStringLiteral("portSelector"));
auto *map = window.findChild<MapWidget *>();
auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs"));
auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter"));
assert(table != nullptr);
assert(referenceLatInput != nullptr);
assert(referenceLonInput != nullptr);
assert(cepValue != nullptr);
assert(maxPointsInput != nullptr);
assert(elevationMaskSpin != nullptr);
assert(elevationMaskSpin->value() == 10);
assert(elevationMaskSpin->minimum() == 0);
assert(elevationMaskSpin->maximum() == 90.0);
elevationMaskSpin->setValue(12.5);
assert(elevationMaskSpin->value() == 12.5);
assert(showTelemetry != nullptr);
assert(showRecentPoints != nullptr);
assert(showAverage != nullptr);
assert(portSelector != nullptr);
assert(!portSelector->isEditable());
assert(portSelector->count() >= 1);
for (int i = 0; i < portSelector->count(); ++i) {
assert(portSelector->itemText(i).startsWith(QStringLiteral("/dev/ttyUSB")));
assert(!portSelector->itemText(i).trimmed().isEmpty());
}
assert(map != nullptr);
assert(mainViewTabs != nullptr);
assert(mainViewTabs->count() == 2);
assert(sideSplitter != nullptr);
assert(sideSplitter->orientation() == Qt::Vertical);
assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection,
Q_ARG(double, 40.25), Q_ARG(double, 29.5)));
assert(referenceLatInput->text() == QStringLiteral("40.250000000000000"));
assert(referenceLonInput->text() == QStringLiteral("29.500000000000000"));
referenceLatInput->clear();
referenceLonInput->clear();
assert(QMetaObject::invokeMethod(&window, "updateReferenceLocation", Qt::DirectConnection));
showTelemetry->setChecked(false);
assert(!showRecentPoints->isEnabled());
assert(!showAverage->isEnabled());
showTelemetry->setChecked(true);
assert(showRecentPoints->isEnabled());
assert(showAverage->isEnabled());
sendLine(window, QStringLiteral("lattitude : 40.78925034214206846173"));
sendLine(window, QStringLiteral("longitude : 29.46277530841727809729"));
assert(table->model()->rowCount() == 0);
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("29.46277530841727809729"));
referenceLatInput->setText(QStringLiteral("40.78925034214206846173"));
referenceLonInput->setText(QStringLiteral("29.46277530841727809729"));
const bool referenceUpdated = QMetaObject::invokeMethod(
&window, "updateReferenceLocation", Qt::DirectConnection);
assert(referenceUpdated);
assert(cepValue->text().startsWith(QStringLiteral("0.00 m")));
assert(cepValue->text().contains(QStringLiteral("N=1")));
const bool cleared = QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection);
assert(cleared);
assert(table->model()->rowCount() == 0);
assert(referenceLatInput->text() == QStringLiteral("40.78925034214206846173"));
assert(referenceLonInput->text() == QStringLiteral("29.46277530841727809729"));
assert(cepValue->text() == QStringLiteral("-"));
referenceLatInput->setText(QStringLiteral("40.0"));
referenceLonInput->setText(QStringLiteral("29.0"));
assert(QMetaObject::invokeMethod(&window, "updateReferenceLocation", Qt::DirectConnection));
sendLine(window, QStringLiteral("lat=40.000 long=29.000"));
sendLine(window, QStringLiteral("lat=40.001 long=29.000"));
sendLine(window, QStringLiteral("lat=40.002 long=29.000"));
flushUpdates(window);
assert(cepValue->text().startsWith(QStringLiteral("111 m")));
assert(cepValue->text().contains(QStringLiteral("N=3")));
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
referenceLatInput->setText(QStringLiteral("40.78925034214206846173"));
referenceLonInput->setText(QStringLiteral("29.46277530841727809729"));
assert(QMetaObject::invokeMethod(&window, "updateReferenceLocation", Qt::DirectConnection));
maxPointsInput->setValue(10000);
for (int i = 0; i < 10000; ++i) {
sendLine(window, QStringLiteral("lat=40.78925 long=29.46277 sats=7"));
}
flushUpdates(window);
assert(table->model()->rowCount() == 10000);
assert(cepValue->text().contains(QStringLiteral("N=10000")));
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
maxPointsInput->setValue(10);
const QByteArray screenshotPath = qgetenv("LOCATION_PLOTTER_TEST_SCREENSHOT");
if (!screenshotPath.isEmpty()) {
sendLine(window, QStringLiteral("NAV solve start sats:5"));
sendLine(window, QStringLiteral("tracking PRNs: G03, G08, G14, G21, G27"));
sendLine(window, QStringLiteral(
"lat=40.78925034214206846173 long=29.46277530841727809729 "
"hheight=-89.05779508221894502640 sats=7 PDOP=1.235"));
flushUpdates(window);
mainViewTabs->setCurrentIndex(1);
window.show();
app.processEvents();
window.repaint();
app.processEvents();
assert(window.grab().save(QString::fromLocal8Bit(screenshotPath), "PNG"));
}
return 0;
}
+199
View File
@@ -0,0 +1,199 @@
#include "../src/LocationParser.h"
#include "../src/RinexNavigation.h"
#include <QCoreApplication>
#include <QFile>
#include <cassert>
#include <cmath>
namespace {
bool closeTo(double left, double right)
{
return std::fabs(left - right) < 0.000001;
}
} // namespace
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("DBG lat=41.0082000 long=28.9784000 hheight=123.45 sats=7"));
assert(parsed.has_value());
assert(parsed->hasLocation());
assert(closeTo(*parsed->latitude, 41.0082));
assert(closeTo(*parsed->longitude, 28.9784));
assert(closeTo(*parsed->hheight, 123.45));
assert(parsed->satellites == 7);
}
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("latitude: -12,5 longitude: 33,25 height: 99 activeSats: 4"));
assert(parsed.has_value());
assert(parsed->hasLocation());
assert(closeTo(*parsed->latitude, -12.5));
assert(closeTo(*parsed->longitude, 33.25));
assert(parsed->latitudeText == QStringLiteral("-12.5"));
assert(parsed->longitudeText == QStringLiteral("33.25"));
assert(closeTo(*parsed->hheight, 99.0));
assert(parsed->satellites == 4);
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral("satellites_active=11"));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(parsed->satellites == 11);
}
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("lat=200 long=28 hheight=100 active_sats=8"));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(parsed->latitudeText.isEmpty());
assert(parsed->longitudeText.isEmpty());
assert(!parsed->hheight.has_value());
assert(parsed->satellites == 8);
}
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("lattitude : 40.78925034214206846173"));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(closeTo(*parsed->latitude, 40.78925034214206846173));
assert(parsed->latitudeText == QStringLiteral("40.78925034214206846173"));
}
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("longitude : 29.46277530841727809729"));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(closeTo(*parsed->longitude, 29.46277530841727809729));
assert(parsed->longitudeText == QStringLiteral("29.46277530841727809729"));
}
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("height : -89.05779508221894502640 "));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(closeTo(*parsed->hheight, -89.05779508221894502640));
}
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("NAV solve start sats:4 mode:periodic abs_span_ms:2.176"));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(parsed->satellites == 4);
}
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("[NAV] navigation software running with 5 satellites"));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(parsed->satellites == 5);
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral("GDOP : 3.9576744550"));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(closeTo(*parsed->dop.gdop, 3.9576744550));
}
{
const auto parsed = LocationParser::parseLine(
QStringLiteral("PDOP : 3.2540598660 HDOP : 2.3631161789 VDOP : 2.2370935468 TDOP : 2.2526165852"));
assert(parsed.has_value());
assert(!parsed->hasLocation());
assert(closeTo(*parsed->dop.pdop, 3.2540598660));
assert(closeTo(*parsed->dop.hdop, 2.3631161789));
assert(closeTo(*parsed->dop.vdop, 2.2370935468));
assert(closeTo(*parsed->dop.tdop, 2.2526165852));
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral("plain debug noise"));
assert(!parsed.has_value());
}
{
const QSet<QString> prns = LocationParser::parseGpsPrns(
QStringLiteral("tracking PRNs: 3, G08, 14; SV=G21 SVID: 27"));
assert(prns.contains(QStringLiteral("G03")));
assert(prns.contains(QStringLiteral("G08")));
assert(prns.contains(QStringLiteral("G14")));
assert(prns.contains(QStringLiteral("G21")));
assert(prns.contains(QStringLiteral("G27")));
assert(prns.size() == 5);
}
{
const QString line = QStringLiteral(
"[OK] [TRK] tracked satellites=5 svn=[2,3,5,8,30]");
const auto parsed = LocationParser::parseLine(line);
assert(parsed.has_value());
assert(parsed->satellites == 5);
const QSet<QString> prns = LocationParser::parseGpsPrns(line);
assert(prns == QSet<QString>({QStringLiteral("G03"), QStringLiteral("G04"),
QStringLiteral("G06"), QStringLiteral("G09"),
QStringLiteral("G31")}));
}
{
const QSet<QString> prns = LocationParser::parseGpsPrns(
QStringLiteral("[TRK] tracked satellites=2 svn=[0,31]"));
assert(prns == QSet<QString>({QStringLiteral("G01"), QStringLiteral("G32")}));
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[OK] [NAV] DOP GDOP=7.0907891622 PDOP=5.6333862175 "
"HDOP=2.9819528958 VDOP=4.7794348204 TDOP=4.3063035967"));
assert(parsed.has_value());
assert(closeTo(*parsed->dop.gdop, 7.0907891622));
assert(closeTo(*parsed->dop.pdop, 5.6333862175));
assert(closeTo(*parsed->dop.hdop, 2.9819528958));
assert(closeTo(*parsed->dop.vdop, 4.7794348204));
assert(closeTo(*parsed->dop.tdop, 4.3063035967));
}
{
const auto parsed = LocationParser::parseLine(QStringLiteral(
"[OK] [NAV] position lat=40.77831900791127850425 "
"lon=29.43579105837637044374 height=198.80186179187148809433"));
assert(parsed.has_value());
assert(parsed->hasLocation());
assert(closeTo(*parsed->latitude, 40.77831900791127850425));
assert(closeTo(*parsed->longitude, 29.43579105837637044374));
assert(closeTo(*parsed->hheight, 198.80186179187148809433));
}
const QString rinexPath = qEnvironmentVariable("LOCATION_PLOTTER_TEST_RINEX");
if (!rinexPath.isEmpty()) {
QFile file(rinexPath);
assert(file.open(QIODevice::ReadOnly));
QString error;
const auto satellites = RinexNavigation::parseGpsNavigation(
file.readAll(), QDateTime::currentDateTimeUtc(), &error);
assert(satellites.size() >= 20);
for (const auto &satellite : satellites) {
assert(satellite.prn.startsWith(QStringLiteral("G")));
assert(std::isfinite(satellite.x));
assert(std::isfinite(satellite.y));
assert(std::isfinite(satellite.z));
}
}
return 0;
}