Add polished macOS location plotter experience
This commit is contained in:
+2
-2
@@ -43,7 +43,7 @@ endif()
|
||||
|
||||
qt_add_resources(location_plotter "app_resources"
|
||||
PREFIX "/"
|
||||
FILES resources/ankasys-logo.png resources/land-110m.json
|
||||
FILES resources/ankasys-logo.png resources/land-110m.json resources/bluemarble-2048.png
|
||||
)
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
@@ -84,7 +84,7 @@ add_executable(location_main_window_tests
|
||||
target_link_libraries(location_main_window_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB)
|
||||
qt_add_resources(location_main_window_tests "test_app_resources"
|
||||
PREFIX "/"
|
||||
FILES resources/ankasys-logo.png resources/land-110m.json
|
||||
FILES resources/ankasys-logo.png resources/land-110m.json resources/bluemarble-2048.png
|
||||
)
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_compile_definitions(location_main_window_tests PRIVATE LOCATION_PLOTTER_LINUX_SERIAL=1)
|
||||
|
||||
@@ -214,6 +214,8 @@ without logging out.
|
||||
(10 degrees by default, press Enter after typing);
|
||||
satellites inside the mask get a cyan sight line and halo. Incoming positions update the
|
||||
receiver marker without resetting a globe orientation chosen by dragging.
|
||||
- The globe surface uses NASA/GSFC Scientific Visualization Studio's 2048x1024 Blue Marble
|
||||
mosaic (NASA SVS visualization 2915), projected locally without a runtime network request.
|
||||
- RINEX download starts at every launch, repeats every 15 minutes, and falls back through the
|
||||
previous two UTC days if today's stream file is unavailable. Current and next-minute satellite
|
||||
positions are propagated from the cached ephemeris, then smoothly interpolated at 30 FPS.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
+280
-21
@@ -1,12 +1,14 @@
|
||||
#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>
|
||||
@@ -184,6 +186,15 @@ const QImage &landMask()
|
||||
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();
|
||||
@@ -369,11 +380,28 @@ void GlobeWidget::paintEvent(QPaintEvent *)
|
||||
QPainterPath sphereClip;
|
||||
sphereClip.addEllipse(center, radius, radius);
|
||||
painter.setClipPath(sphereClip);
|
||||
drawContinents(painter);
|
||||
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);
|
||||
@@ -391,6 +419,9 @@ void GlobeWidget::paintEvent(QPaintEvent *)
|
||||
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;
|
||||
@@ -425,6 +456,100 @@ void GlobeWidget::paintEvent(QPaintEvent *)
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -434,8 +559,11 @@ void GlobeWidget::drawContinents(QPainter &painter) const
|
||||
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_landTextureYaw == m_yaw && m_landTexturePitch == m_pitch
|
||||
&& m_landTextureLightingMinute == lightingMinute) {
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
painter.drawImage(target, m_landTexture);
|
||||
return;
|
||||
@@ -447,6 +575,18 @@ void GlobeWidget::drawContinents(QPainter &painter) const
|
||||
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);
|
||||
@@ -467,16 +607,37 @@ void GlobeWidget::drawContinents(QPainter &painter) const
|
||||
|| 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) {
|
||||
if (nearLand) {
|
||||
const int alpha = static_cast<int>(65.0 * std::clamp(depth * 1.5, 0.0, 1.0));
|
||||
scanLine[px] = qRgba(33, 151, 174, alpha);
|
||||
}
|
||||
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 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)
|
||||
@@ -505,31 +666,110 @@ void GlobeWidget::drawContinents(QPainter &painter) const
|
||||
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 = light * (0.86 + relief * 0.14);
|
||||
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), 0, 255),
|
||||
std::clamp(static_cast<int>(green * shade), 0, 255),
|
||||
std::clamp(static_cast<int>(blue * shade), 0, 255), 238);
|
||||
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);
|
||||
@@ -546,6 +786,10 @@ void GlobeWidget::drawStars(QPainter &painter) const
|
||||
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;
|
||||
@@ -662,14 +906,10 @@ void GlobeWidget::drawVisibilityCone(QPainter &painter) const
|
||||
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});
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -808,7 +1048,26 @@ void GlobeWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
|
||||
void GlobeWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
m_zoom = std::clamp(m_zoom + event->angleDelta().y() / 1200.0, 0.72, 1.28);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public slots:
|
||||
void setElevationMask(double degrees);
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
@@ -44,6 +45,8 @@ private:
|
||||
void drawStars(QPainter &painter) const;
|
||||
void drawOrbit(QPainter &painter, double inclination, double node) const;
|
||||
void drawContinents(QPainter &painter) const;
|
||||
void drawBlueMarble(QPainter &painter) const;
|
||||
void drawSatelliteCoverage(QPainter &painter) const;
|
||||
void drawVisibilityCone(QPainter &painter) const;
|
||||
void drawGraticule(QPainter &painter) const;
|
||||
void drawReceiver(QPainter &painter) const;
|
||||
@@ -60,6 +63,7 @@ private:
|
||||
mutable QImage m_landTexture;
|
||||
mutable double m_landTextureYaw = 0.0;
|
||||
mutable double m_landTexturePitch = 0.0;
|
||||
mutable qint64 m_landTextureLightingMinute = -1;
|
||||
QSet<QString> m_observedPrns;
|
||||
QString m_status = QStringLiteral("Konum bekleniyor");
|
||||
bool m_statusError = false;
|
||||
|
||||
+31
-8
@@ -384,6 +384,12 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
connect(m_rinex, &RinexNavigation::statusChanged, this,
|
||||
[this](const QString &status, bool error) {
|
||||
m_globe->setRinexStatus(status, error);
|
||||
if (m_rinexUpdateButton != nullptr) {
|
||||
m_rinexUpdateButton->setEnabled(!m_rinex->isLoading());
|
||||
m_rinexUpdateButton->setText(m_rinex->isLoading()
|
||||
? QStringLiteral("RINEX indiriliyor...")
|
||||
: QStringLiteral("RINEX Guncelle"));
|
||||
}
|
||||
statusBar()->showMessage(status, error ? 8000 : 4000);
|
||||
});
|
||||
m_rinex->refresh();
|
||||
@@ -433,20 +439,19 @@ bool MainWindow::loadTelemetryLog(const QString &filePath)
|
||||
resetTelemetryState();
|
||||
const int replayCapacity = static_cast<int>(std::min<qsizetype>(10000, lines.size()));
|
||||
m_maxPointsSpin->setValue(std::max(10, replayCapacity));
|
||||
int parsedLocations = 0;
|
||||
for (const QByteArray &rawLine : lines) {
|
||||
const QString line = QString::fromUtf8(rawLine).trimmed();
|
||||
if (line.isEmpty() || line.startsWith(QLatin1Char('#'))) {
|
||||
continue;
|
||||
}
|
||||
const auto telemetry = LocationParser::parseLine(line);
|
||||
if (telemetry.has_value() && telemetry->hasLocation()) {
|
||||
++parsedLocations;
|
||||
}
|
||||
handleSerialLine(line);
|
||||
}
|
||||
// Replay files do not always include a final "NAV solve done" marker. If
|
||||
// the final latitude/longitude pair is complete, retain it as a fix.
|
||||
commitPendingTelemetry();
|
||||
flushUiUpdates();
|
||||
m_map->fitToPoints();
|
||||
const int parsedLocations = m_points.size();
|
||||
const QString message = QStringLiteral("Replay yuklendi: %1 konum - %2")
|
||||
.arg(parsedLocations).arg(filePath);
|
||||
m_statusLabel->setText(message);
|
||||
@@ -499,6 +504,14 @@ void MainWindow::handleSerialLine(const QString &line)
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
|
||||
if (solveStarted) {
|
||||
// Never combine a coordinate fragment left by the previous navigation
|
||||
// period with fields from the new one. A complete pending fix is still
|
||||
// useful; an incomplete one must be discarded at the period boundary.
|
||||
if (m_pendingTelemetry.hasLocation()) {
|
||||
commitPendingTelemetry();
|
||||
} else {
|
||||
m_pendingTelemetry = {};
|
||||
}
|
||||
m_observedPrns.clear();
|
||||
}
|
||||
const QSet<QString> linePrns = LocationParser::parseGpsPrns(line);
|
||||
@@ -717,7 +730,10 @@ void MainWindow::buildUi()
|
||||
background: #23445d;
|
||||
border-color: #62b8e8;
|
||||
}
|
||||
QPushButton:pressed, QToolButton:pressed { background: #dbeaf6; }
|
||||
QPushButton:pressed, QToolButton:pressed {
|
||||
color: #102333;
|
||||
background: #dbeaf6;
|
||||
}
|
||||
QCheckBox { spacing: 7px; color: #d5e0e9; }
|
||||
QCheckBox::indicator { width: 17px; height: 17px; }
|
||||
QCheckBox::indicator:unchecked {
|
||||
@@ -794,7 +810,7 @@ void MainWindow::buildUi()
|
||||
|
||||
m_portCombo = new QComboBox(this);
|
||||
m_portCombo->setObjectName(QStringLiteral("portSelector"));
|
||||
m_portCombo->setMinimumWidth(220);
|
||||
m_portCombo->setMinimumWidth(170);
|
||||
m_portCombo->setEditable(true);
|
||||
m_portCombo->setToolTip(QStringLiteral(
|
||||
"USB UART portu. macOS'ta /dev/cu.usb... tercih edilir; /dev/tty.usb... da desteklenir."));
|
||||
@@ -824,6 +840,11 @@ void MainWindow::buildUi()
|
||||
m_openLogButton = new QPushButton(QStringLiteral("Senaryo Logu Ac"), this);
|
||||
m_openLogButton->setObjectName(QStringLiteral("openLogButton"));
|
||||
m_openLogButton->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton));
|
||||
m_rinexUpdateButton = new QPushButton(QStringLiteral("RINEX Guncelle"), this);
|
||||
m_rinexUpdateButton->setObjectName(QStringLiteral("rinexUpdateButton"));
|
||||
m_rinexUpdateButton->setIcon(style()->standardIcon(QStyle::SP_BrowserReload));
|
||||
m_rinexUpdateButton->setToolTip(QStringLiteral(
|
||||
"BKG/IGS arsivinden guncel GPS RINEX navigasyon verisini yeniden indir"));
|
||||
|
||||
m_autoFollowCheck = new QCheckBox(QStringLiteral("Kamerayi takip et"), this);
|
||||
m_autoFollowCheck->setChecked(true);
|
||||
@@ -844,6 +865,7 @@ void MainWindow::buildUi()
|
||||
toolbarLayout->addWidget(m_maxPointsSpin);
|
||||
toolbarLayout->addWidget(m_connectButton);
|
||||
toolbarLayout->addWidget(m_openLogButton);
|
||||
toolbarLayout->addWidget(m_rinexUpdateButton);
|
||||
toolbarLayout->addWidget(m_clearButton);
|
||||
toolbarLayout->addWidget(m_autoFollowCheck);
|
||||
toolbarLayout->addWidget(zoomOutButton);
|
||||
@@ -859,7 +881,7 @@ void MainWindow::buildUi()
|
||||
viewTabs->addTab(m_globe, QStringLiteral("3B Dunya · Uydular"));
|
||||
|
||||
auto *sidePanel = new QWidget(this);
|
||||
sidePanel->setMinimumWidth(390);
|
||||
sidePanel->setMinimumWidth(330);
|
||||
auto *sideOuterLayout = new QVBoxLayout(sidePanel);
|
||||
sideOuterLayout->setContentsMargins(8, 0, 0, 0);
|
||||
sideOuterLayout->setSpacing(0);
|
||||
@@ -1061,6 +1083,7 @@ void MainWindow::buildUi()
|
||||
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
|
||||
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
|
||||
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
|
||||
connect(m_rinexUpdateButton, &QPushButton::clicked, m_rinex, &RinexNavigation::refresh);
|
||||
connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts);
|
||||
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&MainWindow::trimToMaxPoints);
|
||||
|
||||
@@ -74,6 +74,7 @@ private:
|
||||
QPushButton *m_connectButton = nullptr;
|
||||
QPushButton *m_clearButton = nullptr;
|
||||
QPushButton *m_openLogButton = nullptr;
|
||||
QPushButton *m_rinexUpdateButton = nullptr;
|
||||
QCheckBox *m_autoFollowCheck = nullptr;
|
||||
QCheckBox *m_showTelemetryCheck = nullptr;
|
||||
QCheckBox *m_showRecentPointsCheck = nullptr;
|
||||
|
||||
+82
-13
@@ -4,6 +4,7 @@
|
||||
#include <QLabel>
|
||||
#include <QLineF>
|
||||
#include <QMouseEvent>
|
||||
#include <QNativeGestureEvent>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkRequest>
|
||||
@@ -58,7 +59,7 @@ MapWidget::MapWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_network(new QNetworkAccessManager(this))
|
||||
{
|
||||
setMinimumSize(640, 420);
|
||||
setMinimumSize(480, 320);
|
||||
setMouseTracking(true);
|
||||
|
||||
const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
@@ -115,23 +116,55 @@ void MapWidget::fitToPoints()
|
||||
}
|
||||
double minimumLatitude = m_points.first().latitude;
|
||||
double maximumLatitude = minimumLatitude;
|
||||
double minimumLongitude = m_points.first().longitude;
|
||||
double maximumLongitude = minimumLongitude;
|
||||
QVector<double> longitudes;
|
||||
longitudes.reserve(m_points.size());
|
||||
for (const TelemetryPoint &point : m_points) {
|
||||
minimumLatitude = std::min(minimumLatitude, point.latitude);
|
||||
maximumLatitude = std::max(maximumLatitude, point.latitude);
|
||||
minimumLongitude = std::min(minimumLongitude, point.longitude);
|
||||
maximumLongitude = std::max(maximumLongitude, point.longitude);
|
||||
double normalizedLongitude = std::fmod(point.longitude + 360.0, 360.0);
|
||||
if (normalizedLongitude < 0.0) {
|
||||
normalizedLongitude += 360.0;
|
||||
}
|
||||
longitudes.push_back(normalizedLongitude);
|
||||
}
|
||||
std::sort(longitudes.begin(), longitudes.end());
|
||||
|
||||
// Fit the shortest circular longitude interval. A plain min/max makes
|
||||
// points around +180/-180 look almost a full world apart.
|
||||
double arcStart = longitudes.first();
|
||||
double longitudeSpan = 0.0;
|
||||
if (longitudes.size() > 1) {
|
||||
double largestGap = -1.0;
|
||||
int largestGapIndex = 0;
|
||||
for (int i = 0; i < longitudes.size(); ++i) {
|
||||
const double current = longitudes[i];
|
||||
const double next = i + 1 < longitudes.size()
|
||||
? longitudes[i + 1] : longitudes.first() + 360.0;
|
||||
const double gap = next - current;
|
||||
if (gap > largestGap) {
|
||||
largestGap = gap;
|
||||
largestGapIndex = i;
|
||||
}
|
||||
}
|
||||
arcStart = largestGapIndex + 1 < longitudes.size()
|
||||
? longitudes[largestGapIndex + 1] : longitudes.first();
|
||||
longitudeSpan = 360.0 - largestGap;
|
||||
}
|
||||
m_centerLat = 0.5 * (minimumLatitude + maximumLatitude);
|
||||
m_centerLon = 0.5 * (minimumLongitude + maximumLongitude);
|
||||
m_centerLon = std::fmod(arcStart + longitudeSpan * 0.5 + 180.0, 360.0);
|
||||
if (m_centerLon < 0.0) {
|
||||
m_centerLon += 360.0;
|
||||
}
|
||||
m_centerLon -= 180.0;
|
||||
const double availableWidth = std::max(100.0, width() * 0.82);
|
||||
const double availableHeight = std::max(100.0, height() * 0.82);
|
||||
for (int zoom = MaxZoom; zoom >= MinZoom; --zoom) {
|
||||
const QPointF topLeft = coordinateToWorld(maximumLatitude, minimumLongitude, zoom);
|
||||
const QPointF bottomRight = coordinateToWorld(minimumLatitude, maximumLongitude, zoom);
|
||||
if (std::fabs(bottomRight.x() - topLeft.x()) <= availableWidth
|
||||
&& std::fabs(bottomRight.y() - topLeft.y()) <= availableHeight) {
|
||||
const double worldWidth = static_cast<double>(TileSize) * static_cast<double>(1 << zoom);
|
||||
const double longitudeWidth = longitudeSpan / 360.0 * worldWidth;
|
||||
const double top = coordinateToWorld(maximumLatitude, m_centerLon, zoom).y();
|
||||
const double bottom = coordinateToWorld(minimumLatitude, m_centerLon, zoom).y();
|
||||
if (longitudeWidth <= availableWidth
|
||||
&& std::fabs(bottom - top) <= availableHeight) {
|
||||
m_zoom = zoom;
|
||||
break;
|
||||
}
|
||||
@@ -260,16 +293,52 @@ void MapWidget::paintEvent(QPaintEvent *)
|
||||
|
||||
void MapWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
const int delta = event->angleDelta().y();
|
||||
if (delta == 0) {
|
||||
const QPoint pixelDelta = event->pixelDelta();
|
||||
const QPoint angleDelta = event->angleDelta();
|
||||
const double delta = !pixelDelta.isNull()
|
||||
? pixelDelta.y() / 36.0
|
||||
: angleDelta.y() / 120.0;
|
||||
if (qFuzzyIsNull(delta)) {
|
||||
return;
|
||||
}
|
||||
|
||||
disableAutoFollowFromUserAction();
|
||||
setZoomKeepingPoint(m_zoom + (delta > 0 ? 1 : -1), event->position());
|
||||
m_wheelZoomAccumulator += delta;
|
||||
while (std::abs(m_wheelZoomAccumulator) >= 1.0) {
|
||||
const int direction = m_wheelZoomAccumulator > 0.0 ? 1 : -1;
|
||||
setZoomKeepingPoint(m_zoom + direction, event->position());
|
||||
m_wheelZoomAccumulator -= direction;
|
||||
}
|
||||
if (event->phase() == Qt::ScrollEnd) {
|
||||
m_wheelZoomAccumulator = 0.0;
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
bool MapWidget::event(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::NativeGesture) {
|
||||
auto *gesture = static_cast<QNativeGestureEvent *>(event);
|
||||
if (gesture->gestureType() == Qt::ZoomNativeGesture) {
|
||||
disableAutoFollowFromUserAction();
|
||||
// macOS reports pinch magnification as small deltas. Accumulate
|
||||
// them so map-tile zoom remains stable instead of flickering.
|
||||
m_wheelZoomAccumulator += gesture->value() * 5.0;
|
||||
while (std::abs(m_wheelZoomAccumulator) >= 1.0) {
|
||||
const int direction = m_wheelZoomAccumulator > 0.0 ? 1 : -1;
|
||||
setZoomKeepingPoint(m_zoom + direction, gesture->position());
|
||||
m_wheelZoomAccumulator -= direction;
|
||||
}
|
||||
gesture->accept();
|
||||
return true;
|
||||
}
|
||||
if (gesture->gestureType() == Qt::EndNativeGesture) {
|
||||
m_wheelZoomAccumulator = 0.0;
|
||||
}
|
||||
}
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
void MapWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::RightButton) {
|
||||
|
||||
@@ -41,6 +41,7 @@ signals:
|
||||
void referenceLocationSelected(double latitude, double longitude);
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
@@ -89,4 +90,5 @@ private:
|
||||
std::optional<double> m_cepRadiusMeters;
|
||||
bool m_dragging = false;
|
||||
QPoint m_lastMousePos;
|
||||
double m_wheelZoomAccumulator = 0.0;
|
||||
};
|
||||
|
||||
+11
-5
@@ -5,6 +5,7 @@
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimeZone>
|
||||
#include <QUrl>
|
||||
|
||||
#include <zlib.h>
|
||||
@@ -90,7 +91,8 @@ QByteArray gunzip(const QByteArray &input)
|
||||
|
||||
QDateTime gpsEpoch()
|
||||
{
|
||||
return QDateTime(QDate(1980, 1, 6), QTime(0, 0), Qt::UTC);
|
||||
return QDateTime(QDate(1980, 1, 6), QTime(0, 0),
|
||||
QTimeZone::fromSecondsAheadOfUtc(0));
|
||||
}
|
||||
|
||||
SatellitePosition propagate(const Ephemeris &eph, const QDateTime &whenUtc)
|
||||
@@ -205,7 +207,9 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
|
||||
return;
|
||||
}
|
||||
m_loading = false;
|
||||
if (!loadCache()) {
|
||||
emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,12 +228,13 @@ bool RinexNavigation::loadCache()
|
||||
QFile cache(cachePath());
|
||||
if (!cache.open(QIODevice::ReadOnly)) return false;
|
||||
QString error;
|
||||
m_latestRinex = cache.readAll();
|
||||
const QByteArray cachedRinex = cache.readAll();
|
||||
const QVector<SatellitePosition> positions = parseGpsNavigation(
|
||||
m_latestRinex, QDateTime::currentDateTimeUtc(), &error);
|
||||
cachedRinex, QDateTime::currentDateTimeUtc(), &error);
|
||||
if (positions.isEmpty()) return false;
|
||||
m_latestRinex = cachedRinex;
|
||||
publishCurrentMotion();
|
||||
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX").arg(positions.size()), true);
|
||||
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX").arg(positions.size()), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -291,7 +296,8 @@ QVector<SatellitePosition> RinexNavigation::parseGpsNavigation(
|
||||
const int year = epoch[0].toInt(&dateOk);
|
||||
const QDateTime toc(QDate(year, epoch[1].toInt(), epoch[2].toInt()),
|
||||
QTime(epoch[3].toInt(), epoch[4].toInt(),
|
||||
static_cast<int>(epoch[5].toDouble())), Qt::UTC);
|
||||
static_cast<int>(epoch[5].toDouble())),
|
||||
QTimeZone::fromSecondsAheadOfUtc(0));
|
||||
if (!dateOk || !toc.isValid()) { index += 8; continue; }
|
||||
|
||||
Ephemeris eph;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <QLineEdit>
|
||||
#include <QMetaObject>
|
||||
#include <QSpinBox>
|
||||
#include <QPushButton>
|
||||
#include <QTableView>
|
||||
#include <QTabWidget>
|
||||
#include <QSplitter>
|
||||
@@ -54,6 +55,7 @@ int main(int argc, char **argv)
|
||||
auto *map = window.findChild<MapWidget *>();
|
||||
auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs"));
|
||||
auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter"));
|
||||
auto *rinexUpdateButton = window.findChild<QPushButton *>(QStringLiteral("rinexUpdateButton"));
|
||||
assert(table != nullptr);
|
||||
assert(referenceLatInput != nullptr);
|
||||
assert(referenceLonInput != nullptr);
|
||||
@@ -81,6 +83,8 @@ int main(int argc, char **argv)
|
||||
assert(mainViewTabs->count() == 2);
|
||||
assert(sideSplitter != nullptr);
|
||||
assert(sideSplitter->orientation() == Qt::Vertical);
|
||||
assert(rinexUpdateButton != nullptr);
|
||||
assert(!rinexUpdateButton->toolTip().isEmpty());
|
||||
|
||||
assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection,
|
||||
Q_ARG(double, 40.25), Q_ARG(double, 29.5)));
|
||||
@@ -139,6 +143,24 @@ int main(int argc, char **argv)
|
||||
assert(table->model()->data(table->model()->index(0, 1)).toString()
|
||||
== QStringLiteral("Trajectory (truth)"));
|
||||
|
||||
QTemporaryFile splitReplayLog;
|
||||
assert(splitReplayLog.open());
|
||||
splitReplayLog.write("latitude: 40.25\nlongitude: 29.50\n");
|
||||
splitReplayLog.flush();
|
||||
assert(window.loadTelemetryLog(splitReplayLog.fileName()));
|
||||
assert(table->model()->rowCount() == 1);
|
||||
|
||||
// A partial fix from an earlier solve must not be completed using a field
|
||||
// from the following solve.
|
||||
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
|
||||
sendLine(window, QStringLiteral("NAV solve start"));
|
||||
sendLine(window, QStringLiteral("latitude: 40.0"));
|
||||
sendLine(window, QStringLiteral("NAV solve start"));
|
||||
sendLine(window, QStringLiteral("longitude: 29.0"));
|
||||
sendLine(window, QStringLiteral("NAV solve done"));
|
||||
flushUpdates(window);
|
||||
assert(table->model()->rowCount() == 0);
|
||||
|
||||
assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
|
||||
sendLine(window, QStringLiteral(
|
||||
"lat=40.78925034214206846173 long=29.46277530841727809729"));
|
||||
|
||||
Reference in New Issue
Block a user