#include "GlobeWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 &fallbackLandPolygons() { // Deliberately simplified Natural-Earth-like coastlines. QPointF is lon/lat. static const QVector 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 &landPolygons() { static const QVector 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 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 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(longitude / 360.0 * mask.width()), 0, mask.width() - 1); const int y = std::clamp(static_cast((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 &satellites) { m_satellites = satellites; update(); } void GlobeWidget::setSatelliteMotion(const QVector ¤t, const QVector &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(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs, 0.0, 1.0); QHash targets; targets.reserve(m_satelliteMotionTarget.size()); for (const SatellitePosition &satellite : m_satelliteMotionTarget) { targets.insert(satellite.prn, satellite); } QVector 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 &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(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(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((longitude / (2.0 * Pi) + 0.5) * source.width()); sourceX = (sourceX % source.width() + source.width()) % source.width(); const int sourceY = std::clamp(static_cast((0.5 - latitude / Pi) * source.height()), 0, source.height() - 1); const QRgb sample = reinterpret_cast(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(red * lighting), 0, 255), std::clamp(static_cast(green * lighting), 0, 255), std::clamp(static_cast(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(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(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(red), 0, 255), std::clamp(static_cast(green), 0, 255), std::clamp(static_cast(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(red * shade + cityLights * 235.0), 0, 255), std::clamp(static_cast(green * shade + cityLights * 155.0), 0, 255), std::clamp(static_cast(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 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 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(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); }