#include "GlobeWidget.h" #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; } 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")); // 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 &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. const QVector &candidateStart = m_satellites.isEmpty() ? current : m_satellites; QHash 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(m_satelliteMotionClock.elapsed()) / m_satelliteMotionDurationMs, 0.0, 1.0); QVector 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 &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(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(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(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(red * shade), 0, 255), std::clamp(static_cast(green * shade), 0, 255), std::clamp(static_cast(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 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 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(); }