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
+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();
}