Files
location_plotter/src/GlobeWidget.cpp
T

871 lines
36 KiB
C++

#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>
#include <utility>
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"));
// GNSS satellites move only a few screen pixels per minute. Ten frames per
// second is visually smooth here and avoids repainting a static globe at 30 FPS.
m_satelliteAnimationTimer.setInterval(100);
m_satelliteAnimationTimer.setTimerType(Qt::CoarseTimer);
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.
const QVector<SatellitePosition> &candidateStart =
m_satellites.isEmpty() ? current : m_satellites;
QHash<QString, SatellitePosition> starts;
starts.reserve(candidateStart.size());
for (const SatellitePosition &satellite : candidateStart) {
starts.insert(satellite.prn, satellite);
}
m_satelliteMotionStart.clear();
m_satelliteMotionTarget.clear();
m_satelliteMotionStart.reserve(next.size());
m_satelliteMotionTarget.reserve(next.size());
for (const SatellitePosition &target : next) {
const auto start = starts.constFind(target.prn);
if (start == starts.cend()) continue;
m_satelliteMotionStart.push_back(start.value());
m_satelliteMotionTarget.push_back(target);
}
if (m_satelliteMotionStart.isEmpty()) {
setSatellites(current);
m_satelliteAnimationTimer.stop();
return;
}
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);
QVector<SatellitePosition> interpolated;
interpolated.reserve(m_satelliteMotionStart.size());
for (qsizetype i = 0; i < m_satelliteMotionStart.size(); ++i) {
const SatellitePosition &start = m_satelliteMotionStart[i];
const SatellitePosition &target = m_satelliteMotionTarget[i];
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);
const qreal devicePixelRatio = devicePixelRatioF();
if (m_staticLayer.isNull() || m_staticLayerSize != size()
|| !qFuzzyCompare(m_staticLayerDevicePixelRatio, devicePixelRatio)
|| m_staticLayerYaw != m_yaw || m_staticLayerPitch != m_pitch
|| m_staticLayerZoom != m_zoom) {
rebuildStaticLayer();
}
painter.drawPixmap(0, 0, m_staticLayer);
if (m_hasReceiver) drawVisibilityCone(painter);
if (m_hasReceiver) drawReceiver(painter);
const int visibleCount = 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));
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::rebuildStaticLayer()
{
const qreal devicePixelRatio = devicePixelRatioF();
const QSize pixelSize(
std::max(1, qRound(width() * devicePixelRatio)),
std::max(1, qRound(height() * devicePixelRatio)));
QPixmap layer(pixelSize);
layer.setDevicePixelRatio(devicePixelRatio);
layer.fill(Qt::transparent);
QPainter painter(&layer);
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);
painter.end();
m_staticLayer = std::move(layer);
m_staticLayerSize = size();
m_staticLayerDevicePixelRatio = devicePixelRatio;
m_staticLayerYaw = m_yaw;
m_staticLayerPitch = m_pitch;
m_staticLayerZoom = m_zoom;
}
void GlobeWidget::drawContinents(QPainter &painter) const
{
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
const double pixelRatio = m_dragging ? 0.75 : std::min(devicePixelRatioF(), 1.5);
const int textureSize = std::clamp(
static_cast<int>(std::ceil(radius * 2.0 * pixelRatio)),
m_dragging ? 240 : 320, m_dragging ? 480 : 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"));
}
int GlobeWidget::drawSatellites(QPainter &painter) const
{
if (!m_hasReceiver) return 0;
const double orbitRadius = std::min(width(), height()) * 0.255 * m_zoom * 1.47;
struct Item {
SatellitePosition satellite;
ProjectedPoint projected;
double elevation = -90.0;
};
QVector<Item> items;
items.reserve(m_satellites.size());
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),
satelliteElevation(satellite)});
}
std::sort(items.begin(), items.end(), [](const Item &a, const Item &b) {
return a.projected.depth < b.projected.depth;
});
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);
int visibleCount = 0;
for (const Item &item : items) {
const bool found = m_observedPrns.contains(item.satellite.prn);
const bool inElevationMask = item.elevation >= m_elevationMaskDegrees;
if (inElevationMask) ++visibleCount;
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) {
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);
}
}
return visibleCount;
}
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;
m_staticLayer = QPixmap();
setCursor(Qt::OpenHandCursor);
update();
}
}
void GlobeWidget::wheelEvent(QWheelEvent *event)
{
m_zoom = std::clamp(m_zoom + event->angleDelta().y() / 1200.0, 0.72, 1.28);
update();
event->accept();
}