1074 lines
47 KiB
C++
1074 lines
47 KiB
C++
#include "GlobeWidget.h"
|
|
|
|
#include <QLinearGradient>
|
|
#include <QDateTime>
|
|
#include <QHash>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QMouseEvent>
|
|
#include <QNativeGestureEvent>
|
|
#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;
|
|
}
|
|
|
|
const QImage &blueMarbleTexture()
|
|
{
|
|
static const QImage texture = [] {
|
|
QImage image(QStringLiteral(":/resources/bluemarble-2048.png"));
|
|
return image.convertToFormat(QImage::Format_RGB32);
|
|
}();
|
|
return texture;
|
|
}
|
|
|
|
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> ¤t,
|
|
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);
|
|
drawBlueMarble(painter);
|
|
drawGraticule(painter);
|
|
painter.restore();
|
|
|
|
// A subtle limb shadow and ocean highlight add depth without hiding the
|
|
// geographic texture or telemetry overlays.
|
|
painter.save();
|
|
QPainterPath lightingClip;
|
|
lightingClip.addEllipse(center, radius, radius);
|
|
painter.setClipPath(lightingClip);
|
|
QRadialGradient limb(center - QPointF(radius * .30, radius * .34), radius * 1.30);
|
|
limb.setColorAt(0.0, QColor(255, 255, 255, 18));
|
|
limb.setColorAt(0.50, QColor(70, 175, 225, 5));
|
|
limb.setColorAt(0.82, QColor(0, 8, 20, 35));
|
|
limb.setColorAt(1.0, QColor(0, 3, 12, 155));
|
|
painter.setPen(Qt::NoPen);
|
|
painter.setBrush(limb);
|
|
painter.drawEllipse(center, radius, radius);
|
|
painter.restore();
|
|
|
|
for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0);
|
|
drawSatelliteCoverage(painter);
|
|
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);
|
|
painter.setPen(QColor(105, 143, 166, 145));
|
|
painter.drawText(QRect(20, 43, width() - 40, 22), Qt::AlignRight | Qt::AlignVCenter,
|
|
QStringLiteral("Earth: NASA/GSFC Blue Marble"));
|
|
|
|
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::drawBlueMarble(QPainter &painter) const
|
|
{
|
|
const QImage &source = blueMarbleTexture();
|
|
if (source.isNull()) {
|
|
drawContinents(painter);
|
|
return;
|
|
}
|
|
|
|
const double radius = std::min(width(), height()) * 0.255 * m_zoom;
|
|
const double pixelRatio = std::min(devicePixelRatioF(), 2.0);
|
|
const int textureSize = std::clamp(
|
|
static_cast<int>(std::ceil(radius * 2.0 * pixelRatio)), 420, 1200);
|
|
const QPointF center(width() * 0.5, height() * 0.52);
|
|
const QRectF target(center.x() - radius, center.y() - radius,
|
|
radius * 2.0, radius * 2.0);
|
|
const QDateTime nowUtc = QDateTime::currentDateTimeUtc();
|
|
const qint64 lightingMinute = nowUtc.toSecsSinceEpoch() / 60;
|
|
if (!m_landTexture.isNull() && m_landTexture.width() == textureSize
|
|
&& m_landTextureYaw == m_yaw && m_landTexturePitch == m_pitch
|
|
&& m_landTextureLightingMinute == lightingMinute) {
|
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
painter.drawImage(target, m_landTexture);
|
|
return;
|
|
}
|
|
|
|
QImage globe(textureSize, textureSize, QImage::Format_ARGB32_Premultiplied);
|
|
globe.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);
|
|
const double utcHours = nowUtc.time().msecsSinceStartOfDay() / 3600000.0;
|
|
const double declination = 23.44 * std::sin(
|
|
2.0 * Pi * (284.0 + nowUtc.date().dayOfYear()) / 365.25);
|
|
const Vec3 sun = globeVector(declination, (12.0 - utcHours) * 15.0);
|
|
const Vec3 camera {sy * cp, cy * cp, -sp};
|
|
const double halfLength = std::sqrt(std::pow(sun.x + camera.x, 2.0)
|
|
+ std::pow(sun.y + camera.y, 2.0) + std::pow(sun.z + camera.z, 2.0));
|
|
const Vec3 halfVector {(sun.x + camera.x) / halfLength,
|
|
(sun.y + camera.y) / halfLength,
|
|
(sun.z + camera.z) / halfLength};
|
|
|
|
for (int py = 0; py < textureSize; ++py) {
|
|
QRgb *destination = reinterpret_cast<QRgb *>(globe.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));
|
|
const double longitude = std::atan2(worldY, worldX);
|
|
int sourceX = static_cast<int>((longitude / (2.0 * Pi) + 0.5) * source.width());
|
|
sourceX = (sourceX % source.width() + source.width()) % source.width();
|
|
const int sourceY = std::clamp(static_cast<int>((0.5 - latitude / Pi)
|
|
* source.height()), 0, source.height() - 1);
|
|
const QRgb sample = reinterpret_cast<const QRgb *>(source.constScanLine(sourceY))[sourceX];
|
|
|
|
double red = qRed(sample);
|
|
double green = qGreen(sample);
|
|
double blue = qBlue(sample);
|
|
const double solarDot = worldX * sun.x + worldY * sun.y + worldZ * sun.z;
|
|
const double daylight = smoothStep(-0.10, 0.07, solarDot);
|
|
const double diffuse = 0.16 + 0.84 * std::max(0.0, solarDot);
|
|
const bool ocean = blue > green * 1.18 && blue > red * 1.35;
|
|
if (ocean) {
|
|
const double halfDot = std::max(0.0, worldX * halfVector.x
|
|
+ worldY * halfVector.y + worldZ * halfVector.z);
|
|
const double specular = std::pow(halfDot, 80.0) * daylight * 115.0;
|
|
red += specular;
|
|
green += specular;
|
|
blue += specular * 0.92;
|
|
}
|
|
const double limb = smoothStep(0.02, 0.34, depth);
|
|
const double lighting = (0.08 + diffuse * 0.92) * (0.45 + limb * 0.55);
|
|
destination[px] = qRgba(
|
|
std::clamp(static_cast<int>(red * lighting), 0, 255),
|
|
std::clamp(static_cast<int>(green * lighting), 0, 255),
|
|
std::clamp(static_cast<int>(blue * lighting), 0, 255), 255);
|
|
}
|
|
}
|
|
|
|
m_landTexture = globe;
|
|
m_landTextureYaw = m_yaw;
|
|
m_landTexturePitch = m_pitch;
|
|
m_landTextureLightingMinute = lightingMinute;
|
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
painter.drawImage(target, m_landTexture);
|
|
}
|
|
|
|
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);
|
|
const QDateTime nowUtc = QDateTime::currentDateTimeUtc();
|
|
const qint64 lightingMinute = nowUtc.toSecsSinceEpoch() / 60;
|
|
if (!m_landTexture.isNull() && m_landTexture.width() == textureSize
|
|
&& m_landTextureYaw == m_yaw && m_landTexturePitch == m_pitch
|
|
&& m_landTextureLightingMinute == lightingMinute) {
|
|
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);
|
|
const double utcHours = nowUtc.time().msecsSinceStartOfDay() / 3600000.0;
|
|
const double solarDeclination = 23.44 * std::sin(
|
|
2.0 * Pi * (284.0 + nowUtc.date().dayOfYear()) / 365.25);
|
|
const double subsolarLongitude = (12.0 - utcHours) * 15.0;
|
|
const Vec3 sun = globeVector(solarDeclination, subsolarLongitude);
|
|
const Vec3 camera {sy * cp, cy * cp, -sp};
|
|
const double halfLength = std::sqrt(
|
|
std::pow(sun.x + camera.x, 2.0) + std::pow(sun.y + camera.y, 2.0)
|
|
+ std::pow(sun.z + camera.z, 2.0));
|
|
const Vec3 halfVector {(sun.x + camera.x) / halfLength,
|
|
(sun.y + camera.y) / halfLength,
|
|
(sun.z + camera.z) / halfLength};
|
|
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);
|
|
const double cloudNoise = 0.50
|
|
+ 0.24 * std::sin(longitude * Pi / 22.0 + latitude * Pi / 19.0)
|
|
+ 0.18 * std::sin(longitude * Pi / 7.0 - latitude * Pi / 13.0)
|
|
+ 0.12 * std::cos(longitude * Pi / 3.7 + latitude * Pi / 8.0);
|
|
const double cloud = smoothStep(0.72, 0.94, cloudNoise)
|
|
* smoothStep(0.03, 0.25, depth);
|
|
const double solarDot = worldX * sun.x + worldY * sun.y + worldZ * sun.z;
|
|
const double daylight = smoothStep(-0.11, 0.08, solarDot);
|
|
const double diffuse = 0.12 + 0.88 * std::max(0.0, solarDot);
|
|
if (!onLand) {
|
|
const double oceanPattern = 0.5 + 0.25 * std::sin(longitude * Pi / 31.0)
|
|
+ 0.18 * std::cos(latitude * Pi / 17.0 + longitude * Pi / 47.0);
|
|
double red = nearLand ? 18.0 : 3.0 + oceanPattern * 5.0;
|
|
double green = nearLand ? 104.0 : 32.0 + oceanPattern * 16.0;
|
|
double blue = nearLand ? 132.0 : 72.0 + oceanPattern * 26.0;
|
|
const double halfDot = std::max(0.0, worldX * halfVector.x
|
|
+ worldY * halfVector.y + worldZ * halfVector.z);
|
|
const double specular = std::pow(halfDot, 72.0) * daylight * 150.0;
|
|
const double shade = 0.18 + diffuse * 0.82;
|
|
red = red * shade + specular;
|
|
green = green * shade + specular;
|
|
blue = blue * shade + specular * 0.92;
|
|
red = red * (1.0 - cloud * 0.58) + 226.0 * cloud * 0.58;
|
|
green = green * (1.0 - cloud * 0.58) + 237.0 * cloud * 0.58;
|
|
blue = blue * (1.0 - cloud * 0.58) + 243.0 * cloud * 0.58;
|
|
scanLine[px] = qRgba(std::clamp(static_cast<int>(red), 0, 255),
|
|
std::clamp(static_cast<int>(green), 0, 255),
|
|
std::clamp(static_cast<int>(blue), 0, 255), 248);
|
|
continue;
|
|
}
|
|
|
|
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;
|
|
red = red * (1.0 - cloud * 0.62) + 232.0 * cloud * 0.62;
|
|
green = green * (1.0 - cloud * 0.62) + 241.0 * cloud * 0.62;
|
|
blue = blue * (1.0 - cloud * 0.62) + 244.0 * cloud * 0.62;
|
|
|
|
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 = (0.15 + diffuse * 0.85) * (0.86 + relief * 0.14);
|
|
if (coast) {
|
|
red += 28.0;
|
|
green += 31.0;
|
|
blue += 18.0;
|
|
}
|
|
const double settlementNoise = 0.5 + 0.28 * std::sin(lon * 41.0 + lat * 19.0)
|
|
+ 0.22 * std::cos(lon * 73.0 - lat * 31.0);
|
|
const double cityLights = smoothStep(0.82, 0.98, settlementNoise)
|
|
* (1.0 - daylight) * smoothStep(68.0, 42.0, std::abs(latitude));
|
|
scanLine[px] = qRgba(
|
|
std::clamp(static_cast<int>(red * shade + cityLights * 235.0), 0, 255),
|
|
std::clamp(static_cast<int>(green * shade + cityLights * 155.0), 0, 255),
|
|
std::clamp(static_cast<int>(blue * shade + cityLights * 52.0), 0, 255), 248);
|
|
}
|
|
}
|
|
|
|
m_landTexture = land;
|
|
m_landTextureYaw = m_yaw;
|
|
m_landTexturePitch = m_pitch;
|
|
m_landTextureLightingMinute = lightingMinute;
|
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
painter.drawImage(target, m_landTexture);
|
|
}
|
|
|
|
void GlobeWidget::drawSatelliteCoverage(QPainter &painter) const
|
|
{
|
|
if (m_satellites.isEmpty()) return;
|
|
|
|
constexpr double EarthRadiusMeters = 6378137.0;
|
|
const double earthRadiusPixels = std::min(width(), height()) * 0.255 * m_zoom;
|
|
const double elevation = m_elevationMaskDegrees * Pi / 180.0;
|
|
|
|
for (const SatellitePosition &satellite : m_satellites) {
|
|
const double orbitRadius = std::sqrt(satellite.x * satellite.x
|
|
+ satellite.y * satellite.y
|
|
+ satellite.z * satellite.z);
|
|
if (orbitRadius <= EarthRadiusMeters) continue;
|
|
|
|
const Vec3 normal {satellite.x / orbitRadius, satellite.y / orbitRadius,
|
|
satellite.z / orbitRadius};
|
|
const double footprintAngle = std::acos(std::clamp(
|
|
EarthRadiusMeters / orbitRadius * std::cos(elevation), -1.0, 1.0)) - elevation;
|
|
Vec3 reference = std::abs(normal.z) < 0.88
|
|
? 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);
|
|
if (tangentLength <= 0.0) continue;
|
|
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 bool tracked = m_observedPrns.contains(satellite.prn);
|
|
QColor coverageColor = tracked ? QColor(69, 232, 157, 78)
|
|
: QColor(83, 188, 230, 25);
|
|
painter.setPen(QPen(coverageColor, tracked ? 1.15 : 0.65, Qt::DashLine));
|
|
painter.setBrush(Qt::NoBrush);
|
|
QPainterPath visibleSegments;
|
|
bool drawing = false;
|
|
constexpr int Segments = 72;
|
|
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 surface {
|
|
normal.x * std::cos(footprintAngle) + tangent.x * std::sin(footprintAngle),
|
|
normal.y * std::cos(footprintAngle) + tangent.y * std::sin(footprintAngle),
|
|
normal.z * std::cos(footprintAngle) + tangent.z * std::sin(footprintAngle)
|
|
};
|
|
const ProjectedPoint point = project(surface.x, surface.y, surface.z,
|
|
earthRadiusPixels * 1.006);
|
|
if (point.depth >= -0.015) {
|
|
if (!drawing) visibleSegments.moveTo(point.point);
|
|
else visibleSegments.lineTo(point.point);
|
|
drawing = true;
|
|
} else {
|
|
drawing = false;
|
|
}
|
|
}
|
|
painter.drawPath(visibleSegments);
|
|
}
|
|
}
|
|
|
|
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;
|
|
// Paths are open grid lines. Inheriting the radial Earth brush makes
|
|
// QPainter implicitly close and fill them, which produced the black
|
|
// rectangular/polygonal artifacts on the globe.
|
|
painter.setBrush(Qt::NoBrush);
|
|
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));
|
|
}
|
|
|
|
// Do not fill the projected cone with a triangle fan. When part of the
|
|
// ring is behind the globe that fan self-intersects and produces dark
|
|
// rectangular/polygonal artifacts over the Earth texture. The boundary,
|
|
// spokes and sight lines communicate the same mask without obscuring it.
|
|
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)
|
|
{
|
|
const QPoint pixelDelta = event->pixelDelta();
|
|
const double delta = !pixelDelta.isNull()
|
|
? pixelDelta.y() / 420.0
|
|
: event->angleDelta().y() / 1200.0;
|
|
if (qFuzzyIsNull(delta)) return;
|
|
m_zoom = std::clamp(m_zoom + delta, 0.62, 1.42);
|
|
update();
|
|
event->accept();
|
|
}
|
|
|
|
bool GlobeWidget::event(QEvent *event)
|
|
{
|
|
if (event->type() == QEvent::NativeGesture) {
|
|
auto *gesture = static_cast<QNativeGestureEvent *>(event);
|
|
if (gesture->gestureType() == Qt::ZoomNativeGesture) {
|
|
m_zoom = std::clamp(m_zoom + gesture->value() * 0.85, 0.62, 1.42);
|
|
update();
|
|
gesture->accept();
|
|
return true;
|
|
}
|
|
}
|
|
return QWidget::event(event);
|
|
}
|